From e96f052c1f9e50eb1d35a099d7f37cc1ed47fb86 Mon Sep 17 00:00:00 2001 From: steve Date: Fri, 10 Aug 2018 21:11:36 +0100 Subject: [PATCH] o debugger/input - parser package renamed to input - tokeniser moved to input - command templates are now specified using a simple markup language --- debugger/breakpoints.go | 28 ++++- debugger/colorterm/colorterm.go | 6 +- debugger/commands.go | 103 +++++++++--------- debugger/debugger.go | 46 ++++----- debugger/input/compiled.go | 52 ++++++++++ debugger/{parser => input}/tabcompletion.go | 33 +++--- debugger/input/template.go | 75 ++++++++++++++ debugger/input/tokeniser.go | 90 ++++++++++++++++ debugger/input/validation.go | 53 ++++++++++ debugger/machineinfo.go | 4 +- debugger/parser/commands.go | 109 -------------------- debugger/print.go | 2 +- debugger/targets.go | 7 +- debugger/tokens.go | 68 ------------ debugger/traps.go | 7 +- debugger/ui/plainterminal.go | 4 +- debugger/ui/ui.go | 4 +- 17 files changed, 403 insertions(+), 288 deletions(-) create mode 100644 debugger/input/compiled.go rename debugger/{parser => input}/tabcompletion.go (83%) create mode 100644 debugger/input/template.go create mode 100644 debugger/input/tokeniser.go create mode 100644 debugger/input/validation.go delete mode 100644 debugger/parser/commands.go delete mode 100644 debugger/tokens.go diff --git a/debugger/breakpoints.go b/debugger/breakpoints.go index 8ad5ab4e..dc604bf8 100644 --- a/debugger/breakpoints.go +++ b/debugger/breakpoints.go @@ -5,7 +5,10 @@ package debugger import ( + "fmt" + "gopher2600/debugger/input" "gopher2600/debugger/ui" + "gopher2600/errors" "strconv" ) @@ -88,21 +91,28 @@ func (bp breakpoints) list() { } } -func (bp *breakpoints) parseBreakpoint(tokens *tokens) error { +func (bp *breakpoints) parseBreakpoint(tokens *input.Tokens) error { var tgt target + // resolvedTarget notes whether a target has been used correctly + var resolvedTarget bool + // default target of CPU PC. meaning that "BREAK n" will cause a breakpoint // being set on the PC. breaking on PC is probably the most common type of // breakpoint. the target will change value when the input string sees // something appropriate tgt = bp.dbg.vcs.MC.PC + // resolvedTarget is true to begin with so that the initial target of PC + // can be changed immediately + resolvedTarget = true + // loop over tokens. if token is a number then add the breakpoint for the // current target. if it is not a number, look for a keyword that changes // the target (or run a BREAK meta-command) // // note that this method of looping allows the user to chain break commands - a, present := tokens.get() + a, present := tokens.Get() for present { val, err := strconv.ParseUint(a, 0, 16) if err == nil { @@ -118,15 +128,25 @@ func (bp *breakpoints) parseBreakpoint(tokens *tokens) error { if addNewBreak { bp.breaks = append(bp.breaks, breaker{target: tgt, value: int(val)}) } + resolvedTarget = true } else { - tokens.unget() + if !resolvedTarget { + return errors.NewGopherError(errors.InputTooFewArgs, fmt.Errorf("need a value to break on (%s)", tgt.Label())) + } + + tokens.Unget() tgt, err = parseTarget(bp.dbg, tokens) if err != nil { return err } + resolvedTarget = false } - a, present = tokens.get() + a, present = tokens.Get() + } + + if !resolvedTarget { + return errors.NewGopherError(errors.InputTooFewArgs, fmt.Errorf("need a value to break on (%s)", tgt.Label())) } return nil diff --git a/debugger/colorterm/colorterm.go b/debugger/colorterm/colorterm.go index 391dc53b..57bc6d7a 100644 --- a/debugger/colorterm/colorterm.go +++ b/debugger/colorterm/colorterm.go @@ -3,7 +3,7 @@ package colorterm import ( "bufio" "gopher2600/debugger/colorterm/easyterm" - "gopher2600/debugger/parser" + "gopher2600/debugger/input" "os" ) @@ -14,7 +14,7 @@ type ColorTerminal struct { reader *bufio.Reader commandHistory []command - tabCompleter *parser.TabCompletion + tabCompleter *input.TabCompletion } type command struct { @@ -43,6 +43,6 @@ func (ct *ColorTerminal) CleanUp() { // RegisterTabCompleter adds an implementation of TabCompleter to the // ColorTerminal -func (ct *ColorTerminal) RegisterTabCompleter(tc *parser.TabCompletion) { +func (ct *ColorTerminal) RegisterTabCompleter(tc *input.TabCompletion) { ct.tabCompleter = tc } diff --git a/debugger/commands.go b/debugger/commands.go index afe8fa44..f54f580a 100644 --- a/debugger/commands.go +++ b/debugger/commands.go @@ -1,6 +1,9 @@ package debugger -import "gopher2600/debugger/parser" +import ( + "fmt" + "gopher2600/debugger/input" +) // debugger keywords. not a useful data structure but we can use these to form // the more useful DebuggerCommands and Help structures @@ -38,58 +41,8 @@ const ( KeywordDisplay = "DISPLAY" KeywordScript = "SCRIPT" KeywordDisassemble = "DISASSEMBLE" - - SubKeywordBreaks = "BREAKS" - SubKeywordTraps = "TRAPS" - SubKeywordVideo = "VIDEO" - SubKeywordCPU = "CPU" ) -// DebuggerCommands provides: -// - the list of debugger commands (keys to the map) -// - the tab completion method for each argument for each command -var DebuggerCommands = parser.Commands{ - KeywordInsert: parser.CommandArgs{parser.Arg{Typ: parser.ArgFile, Req: true}}, - KeywordSymbol: parser.CommandArgs{parser.Arg{Typ: parser.ArgString, Req: true}}, - KeywordBreak: parser.CommandArgs{parser.Arg{Typ: parser.ArgTarget, Req: true}, parser.Arg{Typ: parser.ArgValue, Req: false}, parser.Arg{Typ: parser.ArgIndeterminate}}, - KeywordTrap: parser.CommandArgs{parser.Arg{Typ: parser.ArgTarget, Req: true}, parser.Arg{Typ: parser.ArgIndeterminate}}, - KeywordList: parser.CommandArgs{parser.Arg{Typ: parser.ArgKeyword, Req: true, Vals: parser.Keywords{SubKeywordBreaks, SubKeywordTraps}}}, - KeywordClear: parser.CommandArgs{parser.Arg{Typ: parser.ArgKeyword, Req: true, Vals: parser.Keywords{SubKeywordBreaks, SubKeywordTraps}}}, - KeywordOnHalt: parser.CommandArgs{parser.Arg{Typ: parser.ArgIndeterminate}}, - KeywordOnStep: parser.CommandArgs{parser.Arg{Typ: parser.ArgIndeterminate}}, - KeywordLast: parser.CommandArgs{}, - KeywordMemMap: parser.CommandArgs{}, - KeywordQuit: parser.CommandArgs{}, - KeywordReset: parser.CommandArgs{}, - KeywordRun: parser.CommandArgs{}, - KeywordStep: parser.CommandArgs{}, - KeywordStepMode: parser.CommandArgs{parser.Arg{Typ: parser.ArgKeyword, Req: false, Vals: parser.Keywords{SubKeywordCPU, SubKeywordVideo}}}, - KeywordTerse: parser.CommandArgs{}, - KeywordVerbose: parser.CommandArgs{}, - KeywordVerbosity: parser.CommandArgs{}, - KeywordDebuggerState: parser.CommandArgs{}, - KeywordCPU: parser.CommandArgs{}, - KeywordPeek: parser.CommandArgs{parser.Arg{Typ: parser.ArgValue | parser.ArgString, Req: true}, parser.Arg{Typ: parser.ArgIndeterminate}}, - KeywordRAM: parser.CommandArgs{}, - KeywordRIOT: parser.CommandArgs{}, - KeywordTIA: parser.CommandArgs{}, - KeywordTV: parser.CommandArgs{}, - KeywordPlayer: parser.CommandArgs{}, - KeywordMissile: parser.CommandArgs{}, - KeywordBall: parser.CommandArgs{}, - KeywordPlayfield: parser.CommandArgs{}, - KeywordDisplay: parser.CommandArgs{parser.Arg{Typ: parser.ArgValue, Req: false}}, - KeywordScript: parser.CommandArgs{parser.Arg{Typ: parser.ArgFile, Req: true}}, - KeywordDisassemble: parser.CommandArgs{}, -} - -func init() { - // add the help command. we can't add the complete definition for the - // command in the DebuggerCommands declaration because the list of Keywords - // refers to DebuggerCommands itself - DebuggerCommands[KeywordHelp] = parser.CommandArgs{parser.Arg{Typ: parser.ArgKeyword, Req: false, Vals: &DebuggerCommands}} -} - // Help contains the help text for the debugger's top level commands var Help = map[string]string{ KeywordHelp: "Lists commands and provides help for individual debugger commands", @@ -126,3 +79,51 @@ var Help = map[string]string{ KeywordScript: "Run commands from specified file", KeywordDisassemble: "Print the full cartridge disassembly", } + +var commandTemplate = input.CommandTemplate{ + KeywordInsert: "%F", + KeywordSymbol: "%V", + KeywordBreak: "%*", + KeywordTrap: "%*", + KeywordList: "[BREAKS|TRAPS]", + KeywordClear: "[BREAKS|TRAPS]", + KeywordOnHalt: "%*", + KeywordOnStep: "%*", + KeywordLast: "", + KeywordMemMap: "", + KeywordQuit: "", + KeywordReset: "", + KeywordRun: "", + KeywordStep: "", + KeywordStepMode: "[CPU|VIDEO]", + KeywordTerse: "", + KeywordVerbose: "", + KeywordVerbosity: "", + KeywordDebuggerState: "", + KeywordCPU: "", + KeywordPeek: "%*", + KeywordRAM: "", + KeywordRIOT: "", + KeywordTIA: "", + KeywordTV: "", + KeywordPlayer: "", + KeywordMissile: "", + KeywordBall: "", + KeywordPlayfield: "", + KeywordDisplay: "[|OFF]", + KeywordScript: "%F", + KeywordDisassemble: "", +} + +// DebuggerCommands is the tree of valid commands +var DebuggerCommands input.Commands + +func init() { + var err error + + // parse command template + DebuggerCommands, err = input.CompileCommandTemplate(commandTemplate, KeywordHelp) + if err != nil { + panic(fmt.Errorf("error compiling command template: %s", err)) + } +} diff --git a/debugger/debugger.go b/debugger/debugger.go index ee9621b7..f7e5cc39 100644 --- a/debugger/debugger.go +++ b/debugger/debugger.go @@ -2,7 +2,7 @@ package debugger import ( "fmt" - "gopher2600/debugger/parser" + "gopher2600/debugger/input" "gopher2600/debugger/ui" "gopher2600/disassembly" "gopher2600/errors" @@ -126,7 +126,7 @@ func (dbg *Debugger) Start(interf ui.UserInterface, filename string, initScript } defer dbg.ui.CleanUp() - dbg.ui.RegisterTabCompleter(parser.NewTabCompletion(DebuggerCommands)) + dbg.ui.RegisterTabCompleter(input.NewTabCompletion(DebuggerCommands)) err = dbg.loadCartridge(filename) if err != nil { @@ -408,15 +408,15 @@ func (dbg *Debugger) parseInput(input string) (bool, error) { // that cause the emulation to move forward (RUN, STEP) return true for the // first return value. other commands return false and act upon the command // immediately. note that the empty string is the same as the STEP command -func (dbg *Debugger) parseCommand(input string) (bool, error) { +func (dbg *Debugger) parseCommand(userInput string) (bool, error) { // TODO: categorise commands into script-safe and non-script-safe // tokenise input - tokens := tokeniseInput(input) + tokens := input.TokeniseInput(userInput) // check validity of input -- this allows us to catch errors early and in // many cases to ignore the "success" flag when calling tokens.item() - if err := DebuggerCommands.ValidateInput(tokens.tokens); err != nil { + if err := DebuggerCommands.ValidateInput(tokens); err != nil { switch err := err.(type) { case errors.GopherError: switch err.Errno { @@ -431,8 +431,8 @@ func (dbg *Debugger) parseCommand(input string) (bool, error) { // most commands do not cause the emulator to step forward stepNext := false - tokens.reset() - command, _ := tokens.get() + tokens.Reset() + command, _ := tokens.Get() command = strings.ToUpper(command) switch command { default: @@ -440,7 +440,7 @@ func (dbg *Debugger) parseCommand(input string) (bool, error) { // control of the debugger case KeywordHelp: - keyword, present := tokens.get() + keyword, present := tokens.Get() if present { s := strings.ToUpper(keyword) txt, prs := Help[s] @@ -456,7 +456,7 @@ func (dbg *Debugger) parseCommand(input string) (bool, error) { } case KeywordInsert: - cart, _ := tokens.get() + cart, _ := tokens.Get() err := dbg.loadCartridge(cart) if err != nil { return false, err @@ -464,7 +464,7 @@ func (dbg *Debugger) parseCommand(input string) (bool, error) { dbg.print(ui.Feedback, "machine reset with new cartridge (%s)", cart) case KeywordScript: - script, _ := tokens.get() + script, _ := tokens.Get() err := dbg.RunScript(script, false) if err != nil { return false, err @@ -474,7 +474,7 @@ func (dbg *Debugger) parseCommand(input string) (bool, error) { dbg.print(ui.CPUStep, dbg.disasm.Dump()) case KeywordSymbol: - symbol, _ := tokens.get() + symbol, _ := tokens.Get() address, err := dbg.disasm.Symtable.SearchLocation(symbol) if err != nil { switch err := err.(type) { @@ -501,7 +501,7 @@ func (dbg *Debugger) parseCommand(input string) (bool, error) { } case KeywordList: - list, _ := tokens.get() + list, _ := tokens.Get() switch strings.ToUpper(list) { case "BREAKS": dbg.breakpoints.list() @@ -510,7 +510,7 @@ func (dbg *Debugger) parseCommand(input string) (bool, error) { } case KeywordClear: - clear, _ := tokens.get() + clear, _ := tokens.Get() switch strings.ToUpper(clear) { case "BREAKS": dbg.breakpoints.clear() @@ -521,10 +521,10 @@ func (dbg *Debugger) parseCommand(input string) (bool, error) { } case KeywordOnHalt: - if tokens.remaining() == 0 { + if tokens.Remaining() == 0 { dbg.commandOnHalt = dbg.commandOnHaltStored } else { - option, _ := tokens.peek() + option, _ := tokens.Peek() if strings.ToUpper(option) == "OFF" { dbg.commandOnHalt = "" dbg.print(ui.Feedback, "no auto-command on halt") @@ -532,7 +532,7 @@ func (dbg *Debugger) parseCommand(input string) (bool, error) { } // use remaininder of command line to form the ONHALT command sequence - dbg.commandOnHalt = tokens.remainder() + dbg.commandOnHalt = tokens.Remainder() // we can't use semi-colons when specifying the sequence so allow use of // commas to act as an alternative @@ -549,10 +549,10 @@ func (dbg *Debugger) parseCommand(input string) (bool, error) { return false, err case KeywordOnStep: - if tokens.remaining() == 0 { + if tokens.Remaining() == 0 { dbg.commandOnStep = dbg.commandOnStepStored } else { - option, _ := tokens.peek() + option, _ := tokens.Peek() if strings.ToUpper(option) == "OFF" { dbg.commandOnStep = "" dbg.print(ui.Feedback, "no auto-command on step") @@ -560,7 +560,7 @@ func (dbg *Debugger) parseCommand(input string) (bool, error) { } // use remaininder of command line to form the ONSTEP command sequence - dbg.commandOnStep = tokens.remainder() + dbg.commandOnStep = tokens.Remainder() // we can't use semi-colons when specifying the sequence so allow use of // commas to act as an alternative @@ -615,7 +615,7 @@ func (dbg *Debugger) parseCommand(input string) (bool, error) { stepNext = true case KeywordStepMode: - mode, _ := tokens.get() + mode, _ := tokens.Get() mode = strings.ToUpper(mode) switch mode { case "CPU": @@ -654,7 +654,7 @@ func (dbg *Debugger) parseCommand(input string) (bool, error) { dbg.printMachineInfo(dbg.vcs.MC) case KeywordPeek: - a, present := tokens.get() + a, present := tokens.Get() for present { var addr interface{} var msg string @@ -687,7 +687,7 @@ func (dbg *Debugger) parseCommand(input string) (bool, error) { } dbg.print(ui.MachineInfo, msg) - a, present = tokens.get() + a, present = tokens.Get() } case KeywordRAM: @@ -723,7 +723,7 @@ func (dbg *Debugger) parseCommand(input string) (bool, error) { case KeywordDisplay: visibility := true - action, present := tokens.get() + action, present := tokens.Get() if present { switch strings.ToUpper(action) { case "OFF": diff --git a/debugger/input/compiled.go b/debugger/input/compiled.go new file mode 100644 index 00000000..99cab0c1 --- /dev/null +++ b/debugger/input/compiled.go @@ -0,0 +1,52 @@ +package input + +// Commands is the root of the argument "tree" +type Commands map[string]commandArgList + +// argType defines the expected argument type +type argType int + +// the possible values for argType +const ( + argKeyword argType = iota + argFile + argValue + argString + argIndeterminate +) + +// commandArg specifies the type and properties of an individual argument +type commandArg struct { + typ argType + required bool + values interface{} +} + +// commandArgList is the list of commandArgList for each command +type commandArgList []commandArg + +// maximumLen returns the maximum number of arguments allowed for a given +// command +func (a commandArgList) maximumLen() int { + if len(a) == 0 { + return 0 + } + if a[len(a)-1].typ == argIndeterminate { + // return the maximum value allowed for an integer + return int(^uint(0) >> 1) + } + return len(a) +} + +// requiredLen returns the number of arguments required for a given command. +// in other words, the command may allow more but it must have at least the +// returned numnber. +func (a commandArgList) requiredLen() (m int) { + for i := 0; i < len(a); i++ { + if !a[i].required { + return + } + m++ + } + return +} diff --git a/debugger/parser/tabcompletion.go b/debugger/input/tabcompletion.go similarity index 83% rename from debugger/parser/tabcompletion.go rename to debugger/input/tabcompletion.go index 510c3a28..c7d4f2e4 100644 --- a/debugger/parser/tabcompletion.go +++ b/debugger/input/tabcompletion.go @@ -1,4 +1,4 @@ -package parser +package input import ( "strings" @@ -9,7 +9,7 @@ const cycleDuration = 500 * time.Millisecond // TabCompletion keeps track of the most recent tab completion attempt type TabCompletion struct { - baseOptions Commands + commands Commands options []string lastOption int @@ -22,19 +22,18 @@ type TabCompletion struct { lastCompletionTime time.Time } -// NewTabCompletion is the preferred method of initialisation for TabCompletion -func NewTabCompletion(baseOptions Commands) *TabCompletion { +// NewTabCompletion initialises a new TabCompletion instance +func NewTabCompletion(commands Commands) *TabCompletion { tc := new(TabCompletion) - tc.baseOptions = baseOptions - tc.options = make([]string, 0, len(tc.baseOptions)) + tc.commands = commands + tc.options = make([]string, 0, len(tc.commands)) return tc } // GuessWord transforms the input such that the last word in the input is // expanded to meet the closest match in the list of allowed strings. func (tc *TabCompletion) GuessWord(input string) string { - // split input into words - p := strings.Fields(input) + p := tokeniseInput(input) if len(p) == 0 { return input } @@ -68,33 +67,33 @@ func (tc *TabCompletion) GuessWord(input string) string { tc.lastOption = 0 // get args for command - var arg Arg + var arg commandArg - argList, ok := tc.baseOptions[strings.ToUpper(p[0])] + argList, ok := tc.commands[strings.ToUpper(p[0])] if ok && len(input) > len(p[0]) { if len(argList) == 0 { return input } arg = argList[len(p)-2] } else { - arg.Typ = ArgKeyword - arg.Vals = &tc.baseOptions + arg.typ = argKeyword + arg.values = &tc.commands } - switch arg.Typ { - case ArgKeyword: + switch arg.typ { + case argKeyword: // trigger is the word we're trying to complete on trigger := strings.ToUpper(p[len(p)-1]) p = p[:len(p)-1] - switch kw := arg.Vals.(type) { + switch kw := arg.values.(type) { case *Commands: for k := range *kw { if len(trigger) <= len(k) && trigger == k[:len(trigger)] { tc.options = append(tc.options, k) } } - case Keywords: + case []string: for _, k := range kw { if len(trigger) <= len(k) && trigger == k[:len(trigger)] { tc.options = append(tc.options, k) @@ -104,7 +103,7 @@ func (tc *TabCompletion) GuessWord(input string) string { tc.options = append(tc.options, "unhandled argument type") } - case ArgFile: + case argFile: // TODO: filename completion tc.options = append(tc.options, "") } diff --git a/debugger/input/template.go b/debugger/input/template.go new file mode 100644 index 00000000..50a48a3a --- /dev/null +++ b/debugger/input/template.go @@ -0,0 +1,75 @@ +package input + +import ( + "fmt" + "strings" +) + +// CommandTemplate is the root of the argument "tree" +type CommandTemplate map[string]string + +// CompileCommandTemplate creates a new instance of Commands from an instance +// of CommandTemplate. if no help is command is required, use the empty-string +// to for the helpKeyword argument +func CompileCommandTemplate(template CommandTemplate, helpKeyword string) (Commands, error) { + commands := Commands{} + for k, v := range template { + commands[k] = commandArgList{} + + placeholder := false + + for i := 0; i < len(v); i++ { + switch v[i] { + case '%': + placeholder = true + case '[': + // find end of option list + j := strings.Index(v[i:], "]") + if j == -1 { + return commands, fmt.Errorf("unclosed option list (%s)", k) + } + + options := strings.Split(v[i+1:j], "|") + if len(options) == 1 { + // note: Split() returns a slice of the input string, if + // the seperator ("|") cannot be found. the length of an + // empty option list is therefore 1. + return commands, fmt.Errorf("empty option list (%s)", k) + } + + // decide whether the option is required + req := true + for m := 0; m < len(options); m++ { + if options[m] == "" { + req = false + break + } + } + + // add a new argument for current keyword with the options + // we've found + commands[k] = append(commands[k], commandArg{typ: argKeyword, required: req, values: options}) + + default: + if placeholder { + switch v[i] { + case 'F': + commands[k] = append(commands[k], commandArg{typ: argFile, required: true}) + case 'S': + commands[k] = append(commands[k], commandArg{typ: argString, required: true}) + case 'V': + commands[k] = append(commands[k], commandArg{typ: argValue, required: true}) + case '*': + commands[k] = append(commands[k], commandArg{typ: argIndeterminate, required: true}) + } + } + } + } + } + + if helpKeyword != "" { + commands[helpKeyword] = commandArgList{commandArg{typ: argKeyword, required: false, values: &commands}} + } + + return commands, nil +} diff --git a/debugger/input/tokeniser.go b/debugger/input/tokeniser.go new file mode 100644 index 00000000..11440a2d --- /dev/null +++ b/debugger/input/tokeniser.go @@ -0,0 +1,90 @@ +package input + +import ( + "fmt" + "strings" +) + +// Tokens represents tokenised input. This can be used to walk through the +// input string (using get()) for eas(ier) parsing +type Tokens struct { + tokens []string + curr int +} + +// Reset begins the token traversal process from the beginning +func (tk *Tokens) Reset() { + tk.curr = 0 +} + +// Remainder returns the remaining tokens as a string +func (tk Tokens) Remainder() string { + return strings.Join(tk.tokens[tk.curr:], " ") +} + +// Remaining returns the count of reminaing tokens in the token list +func (tk Tokens) Remaining() int { + return len(tk.tokens) - tk.curr +} + +// Total returns the total count of tokens +func (tk Tokens) Total() int { + return len(tk.tokens) +} + +// Get returns the next token in the list, and a success boolean - if the end +// of the token list has been reached, the function returns false instead of +// true. +func (tk *Tokens) Get() (string, bool) { + if tk.curr >= len(tk.tokens) { + return "", false + } + tk.curr++ + return tk.tokens[tk.curr-1], true +} + +// Unget walks backwards in the token list. +func (tk *Tokens) Unget() { + if tk.curr > 0 { + tk.curr-- + } +} + +// Peek returns the next token in the list (without advancing the list), and a +// success boolean - if the end of the token list has been reached, the +// function returns false instead of true. +func (tk Tokens) Peek() (string, bool) { + if tk.curr >= len(tk.tokens) { + return "", false + } + return tk.tokens[tk.curr], true +} + +// TokeniseInput creates and returns a new Tokens instance +func TokeniseInput(input string) *Tokens { + tk := new(Tokens) + + // remove leading/trailing space + input = strings.TrimSpace(input) + + // divide user input into tokens + tk.tokens = tokeniseInput(input) + + // normalise variations in syntax + for i := 0; i < len(tk.tokens); i++ { + // normalise hex notation + if tk.tokens[i][0] == '$' { + tk.tokens[i] = fmt.Sprintf("0x%s", tk.tokens[i][1:]) + } + } + + return tk +} + +// tokeniseInput is the "raw" tokenising function (without normalisation or +// wrapping everything up in a Tokens instance). used by the fancier +// TokeniseInput and anywhere else where we need to divide input into tokens +// (eg. TabCompletion.GuessWord()) +func tokeniseInput(input string) []string { + return strings.Fields(input) +} diff --git a/debugger/input/validation.go b/debugger/input/validation.go new file mode 100644 index 00000000..cf7dcfa1 --- /dev/null +++ b/debugger/input/validation.go @@ -0,0 +1,53 @@ +package input + +import ( + "fmt" + "gopher2600/errors" + "strings" +) + +// ValidateInput checks whether input is correct according to the +// command definitions +func (options Commands) ValidateInput(newInput *Tokens) error { + var args commandArgList + + tokens := newInput.tokens + + // if tokens is empty then return + if len(tokens) == 0 { + return errors.NewGopherError(errors.InputEmpty) + } + + tokens[0] = strings.ToUpper(tokens[0]) + + // basic check for whether command is recognised + var ok bool + if args, ok = options[tokens[0]]; !ok { + return errors.NewGopherError(errors.InputInvalidCommand, fmt.Sprintf("%s is not a debugging command", tokens[0])) + } + + // too *many* arguments have been supplied + if len(tokens)-1 > args.maximumLen() { + return errors.NewGopherError(errors.InputTooManyArgs, fmt.Sprintf("too many arguments for %s", tokens[0])) + } + + // too *few* arguments have been supplied + if len(tokens)-1 < args.requiredLen() { + switch args[len(tokens)-1].typ { + case argKeyword: + return errors.NewGopherError(errors.InputTooFewArgs, fmt.Sprintf("keyword required for %s", tokens[0])) + case argFile: + return errors.NewGopherError(errors.InputTooFewArgs, fmt.Sprintf("filename required for %s", tokens[0])) + case argValue: + return errors.NewGopherError(errors.InputTooFewArgs, fmt.Sprintf("numeric argument required for %s", tokens[0])) + case argString: + return errors.NewGopherError(errors.InputTooFewArgs, fmt.Sprintf("string argument required for %s", tokens[0])) + default: + // TODO: argument types can be OR'd together. breakdown these types + // to give more useful information + return errors.NewGopherError(errors.InputTooFewArgs, fmt.Sprintf("too few arguments for %s", tokens[0])) + } + } + + return nil +} diff --git a/debugger/machineinfo.go b/debugger/machineinfo.go index c960e74a..444a3745 100644 --- a/debugger/machineinfo.go +++ b/debugger/machineinfo.go @@ -11,11 +11,11 @@ type machineInfo interface { MachineInfoTerse() string } -func (dbg Debugger) printMachineInfo(mi machineInfo) { +func (dbg *Debugger) printMachineInfo(mi machineInfo) { dbg.print(ui.MachineInfo, "%s", dbg.sprintMachineInfo(mi)) } -func (dbg Debugger) sprintMachineInfo(mi machineInfo) string { +func (dbg *Debugger) sprintMachineInfo(mi machineInfo) string { if dbg.machineInfoVerbose { return mi.MachineInfo() } diff --git a/debugger/parser/commands.go b/debugger/parser/commands.go deleted file mode 100644 index 72c9f428..00000000 --- a/debugger/parser/commands.go +++ /dev/null @@ -1,109 +0,0 @@ -package parser - -import ( - "fmt" - "gopher2600/errors" - "strings" -) - -// ArgType defines the expected argument type -type ArgType int - -// the possible values for ArgType -const ( - ArgKeyword ArgType = 1 << iota - ArgFile - ArgTarget - ArgValue - ArgString - ArgAddress - ArgIndeterminate -) - -// Commands is the root of the argument "tree" -type Commands map[string]CommandArgs - -// Arg specifies the type and properties of an individual argument -type Arg struct { - Typ ArgType - Req bool - Vals AllowedVals -} - -// CommandArgs is the list of Args for each command -type CommandArgs []Arg - -// AllowedVals can take a number of types, useful for tab completion -type AllowedVals interface{} - -// Keywords can be used for specifying a list of keywords -// -- satisfies the AllowedVals interface -type Keywords []string - -func (a CommandArgs) maxLen() int { - if len(a) == 0 { - return 0 - } - if a[len(a)-1].Typ == ArgIndeterminate { - return int(^uint(0) >> 1) - } - return len(a) -} - -func (a CommandArgs) minLen() (m int) { - for i := 0; i < len(a); i++ { - if !a[i].Req { - return - } - m++ - } - return -} - -// ValidateInput checks whether input is correct according to the -// command definitions -func (options Commands) ValidateInput(input []string) error { - var args CommandArgs - - // if input is empty then return - if len(input) == 0 { - return errors.NewGopherError(errors.InputEmpty) - } - - input[0] = strings.ToUpper(input[0]) - - // basic check for whether command is recognised - var ok bool - if args, ok = options[input[0]]; !ok { - return errors.NewGopherError(errors.InputInvalidCommand, fmt.Sprintf("%s is not a debugging command", input[0])) - } - - // too *many* arguments have been supplied - if len(input)-1 > args.maxLen() { - return errors.NewGopherError(errors.InputTooManyArgs, fmt.Sprintf("too many arguments for %s", input[0])) - } - - // too *few* arguments have been supplied - if len(input)-1 < args.minLen() { - switch args[len(input)-1].Typ { - case ArgKeyword: - return errors.NewGopherError(errors.InputTooFewArgs, fmt.Sprintf("keyword required for %s", input[0])) - case ArgFile: - return errors.NewGopherError(errors.InputTooFewArgs, fmt.Sprintf("filename required for %s", input[0])) - case ArgAddress: - return errors.NewGopherError(errors.InputTooFewArgs, fmt.Sprintf("address required for %s", input[0])) - case ArgTarget: - return errors.NewGopherError(errors.InputTooFewArgs, fmt.Sprintf("emulation target required for %s", input[0])) - case ArgValue: - return errors.NewGopherError(errors.InputTooFewArgs, fmt.Sprintf("numeric argument required for %s", input[0])) - case ArgString: - return errors.NewGopherError(errors.InputTooFewArgs, fmt.Sprintf("string argument required for %s", input[0])) - default: - // TODO: argument types can be OR'd together. breakdown these types - // to give more useful information - return errors.NewGopherError(errors.InputTooFewArgs, fmt.Sprintf("too few arguments for %s", input[0])) - } - } - - return nil -} diff --git a/debugger/print.go b/debugger/print.go index 8b8bcac1..32e13f22 100644 --- a/debugger/print.go +++ b/debugger/print.go @@ -9,7 +9,7 @@ import ( // before passing to the real UserPrint. it also allows us to easily obey // directives such as the silent directive without passing the burden onto UI // implementors -func (dbg Debugger) print(pp ui.PrintProfile, s string, a ...interface{}) { +func (dbg *Debugger) print(pp ui.PrintProfile, s string, a ...interface{}) { if dbg.uiSilent && pp != ui.Error { return } diff --git a/debugger/targets.go b/debugger/targets.go index 7eec88c9..70dfbd95 100644 --- a/debugger/targets.go +++ b/debugger/targets.go @@ -2,6 +2,7 @@ package debugger import ( "fmt" + "gopher2600/debugger/input" "gopher2600/errors" "gopher2600/television" "strings" @@ -41,11 +42,11 @@ func (trg genericTarget) Value() interface{} { } // parseTarget uses a keyword to decide which part of the vcs to target -func parseTarget(dbg *Debugger, tokens *tokens) (target, error) { +func parseTarget(dbg *Debugger, tokens *input.Tokens) (target, error) { var trg target var err error - keyword, present := tokens.get() + keyword, present := tokens.Get() if present { keyword = strings.ToUpper(keyword) switch keyword { @@ -70,7 +71,7 @@ func parseTarget(dbg *Debugger, tokens *tokens) (target, error) { // help investigate a bug in the emulation. I don't think it's much use // but it was an instructive exercise and may come in useful one day. case "INSTRUCTION", "INS": - subkey, present := tokens.get() + subkey, present := tokens.Get() if present { subkey = strings.ToUpper(subkey) switch subkey { diff --git a/debugger/tokens.go b/debugger/tokens.go deleted file mode 100644 index 0ab908db..00000000 --- a/debugger/tokens.go +++ /dev/null @@ -1,68 +0,0 @@ -package debugger - -import ( - "fmt" - "strings" -) - -type tokens struct { - tokens []string - curr int -} - -func (tk *tokens) reset() { - tk.curr = 0 -} - -func (tk tokens) remainder() string { - return strings.Join(tk.tokens[tk.curr:], " ") -} - -func (tk tokens) remaining() int { - return len(tk.tokens) - tk.curr -} - -func (tk tokens) num() int { - return len(tk.tokens) -} - -func (tk *tokens) get() (string, bool) { - if tk.curr >= len(tk.tokens) { - return "", false - } - tk.curr++ - return tk.tokens[tk.curr-1], true -} - -func (tk *tokens) unget() { - if tk.curr > 0 { - tk.curr-- - } -} - -func (tk tokens) peek() (string, bool) { - if tk.curr >= len(tk.tokens) { - return "", false - } - return tk.tokens[tk.curr], true -} - -func tokeniseInput(input string) *tokens { - tk := new(tokens) - - // remove leading/trailing space - input = strings.TrimSpace(input) - - // divide user input into tokens - tk.tokens = strings.Fields(input) - - // normalise variations in syntax - for i := 0; i < len(tk.tokens); i++ { - // normalise hex notation - if tk.tokens[i][0] == '$' { - tk.tokens[i] = fmt.Sprintf("0x%s", tk.tokens[i][1:]) - } - } - - return tk -} diff --git a/debugger/traps.go b/debugger/traps.go index 540b0f6c..20363148 100644 --- a/debugger/traps.go +++ b/debugger/traps.go @@ -6,6 +6,7 @@ package debugger import ( "fmt" + "gopher2600/debugger/input" "gopher2600/debugger/ui" ) @@ -64,8 +65,8 @@ func (tr traps) list() { } } -func (tr *traps) parseTrap(tokens *tokens) error { - _, present := tokens.peek() +func (tr *traps) parseTrap(tokens *input.Tokens) error { + _, present := tokens.Peek() for present { tgt, err := parseTarget(tr.dbg, tokens) if err != nil { @@ -85,7 +86,7 @@ func (tr *traps) parseTrap(tokens *tokens) error { tr.traps = append(tr.traps, trapper{target: tgt, origValue: tgt.Value()}) } - _, present = tokens.peek() + _, present = tokens.Peek() } return nil diff --git a/debugger/ui/plainterminal.go b/debugger/ui/plainterminal.go index 7b90f260..7fbaa51c 100644 --- a/debugger/ui/plainterminal.go +++ b/debugger/ui/plainterminal.go @@ -2,7 +2,7 @@ package ui import ( "fmt" - "gopher2600/debugger/parser" + "gopher2600/debugger/input" "os" ) @@ -20,7 +20,7 @@ func (pt *PlainTerminal) CleanUp() { } // RegisterTabCompleter adds an implementation of TabCompleter to the terminal -func (pt *PlainTerminal) RegisterTabCompleter(tc *parser.TabCompletion) { +func (pt *PlainTerminal) RegisterTabCompleter(tc *input.TabCompletion) { } // UserPrint is the plain terminal print routine diff --git a/debugger/ui/ui.go b/debugger/ui/ui.go index b96131a5..9a7be721 100644 --- a/debugger/ui/ui.go +++ b/debugger/ui/ui.go @@ -1,12 +1,12 @@ package ui -import "gopher2600/debugger/parser" +import "gopher2600/debugger/input" // UserInterface defines the user interface operations required by the debugger type UserInterface interface { Initialise() error CleanUp() - RegisterTabCompleter(*parser.TabCompletion) + RegisterTabCompleter(*input.TabCompletion) UserPrint(PrintProfile, string, ...interface{}) UserRead([]byte, string) (int, error) }