o debugger/input

- parser package renamed to input
	- tokeniser moved to input
	- command templates are now specified using a simple markup
	language
This commit is contained in:
steve 2018-08-10 21:11:36 +01:00
parent d744e6229a
commit e96f052c1f
17 changed files with 403 additions and 288 deletions

View file

@ -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

View file

@ -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
}

View file

@ -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))
}
}

View file

@ -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":

View file

@ -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
}

View file

@ -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, "<TODO: file-completion>")
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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)
}