o replaced debugger/input package with debugger/commandline

- improved how command template are compiled
    - verification and tab completion is now more robust
    - missing repeat groups

o debugger
    - extended PLAYER and MISSILE commands
    - combined STICK1 and STICK2 into one STICK command
This commit is contained in:
steve 2019-04-03 12:54:32 +01:00
parent a9b313bbcc
commit 46b8173b7b
26 changed files with 1634 additions and 558 deletions

View file

@ -6,8 +6,8 @@ package debugger
import (
"fmt"
"gopher2600/debugger/commandline"
"gopher2600/debugger/console"
"gopher2600/debugger/input"
"gopher2600/errors"
"strconv"
"strings"
@ -171,7 +171,7 @@ func (bp breakpoints) list() {
// & SL 100 HP 0 | X 10
//
// TODO: more sophisticated breakpoints parser
func (bp *breakpoints) parseBreakpoint(tokens *input.Tokens) error {
func (bp *breakpoints) parseBreakpoint(tokens *commandline.Tokens) error {
andBreaks := false
// default target of CPU PC. meaning that "BREAK n" will cause a breakpoint

View file

@ -2,7 +2,7 @@ package colorterm
import (
"gopher2600/debugger/colorterm/easyterm"
"gopher2600/debugger/input"
"gopher2600/debugger/console"
"os"
)
@ -12,7 +12,7 @@ type ColorTerminal struct {
reader runeReader
commandHistory []command
tabCompleter *input.TabCompletion
tabCompleter console.TabCompleter
}
type command struct {
@ -41,7 +41,7 @@ func (ct *ColorTerminal) CleanUp() {
// RegisterTabCompleter adds an implementation of TabCompleter to the
// ColorTerminal
func (ct *ColorTerminal) RegisterTabCompleter(tc *input.TabCompletion) {
func (ct *ColorTerminal) RegisterTabCompleter(tc console.TabCompleter) {
ct.tabCompleter = tc
}

View file

@ -65,10 +65,18 @@ func (ct *ColorTerminal) UserRead(input []byte, prompt string, events chan gui.E
return inputLen, readRune.err
}
// reset tabcompletion state if anything other than the tab key has
// been pressed
if readRune.r != easyterm.KeyTab {
if ct.tabCompleter != nil {
ct.tabCompleter.Reset()
}
}
switch readRune.r {
case easyterm.KeyTab:
if ct.tabCompleter != nil {
s := ct.tabCompleter.GuessWord(string(input[:cursorPos]))
s := ct.tabCompleter.Complete(string(input[:cursorPos]))
// the difference in the length of the new input and the old
// input

View file

@ -0,0 +1,35 @@
package commandline
import (
"fmt"
"strings"
)
// ParseError is the error type for the ParseCommandTemplate function
type ParseError struct {
definition string
position int
underlyingError error
}
func (er ParseError) Error() string {
return fmt.Sprintf("parser error: %s", er.underlyingError)
}
// Location returns detailed information about the Error
func (er ParseError) Location() string {
s := strings.Builder{}
s.WriteString(er.definition)
s.WriteString("\n")
s.WriteString(fmt.Sprintf("%s^", strings.Repeat(" ", er.position)))
return s.String()
}
// NewParseError is used to create a new instance of a Error
func NewParseError(defn string, position int, underlyingError error) *ParseError {
er := new(ParseError)
er.definition = defn
er.position = position
er.underlyingError = underlyingError
return er
}

View file

@ -0,0 +1,98 @@
package commandline
import (
"fmt"
"strings"
)
// Commands is the root of the command tree
//
// currently, the top-level of the Commands tree is an array of nodes. each
// entry in this array is effectively a branch off a conceptual root-node. with
// a bit of work, we could alter the command tree such that the array is a
// sequence of branches off an otherwise unused root-node. this would simplify
// validation and tab-completion a little bit. as it is though, this is fine
// for now.
type Commands []*node
// Len implements Sort package interface
func (cmds Commands) Len() int {
return len(cmds)
}
// Less implements Sort package interface
func (cmds Commands) Less(i int, j int) bool {
return cmds[i].tag < cmds[j].tag
}
// Swap implements Sort package interface
func (cmds Commands) Swap(i int, j int) {
swp := cmds[i]
cmds[i] = cmds[j]
cmds[j] = swp
}
func (cmds Commands) String() string {
s := strings.Builder{}
for c := range cmds {
s.WriteString(fmt.Sprintf("%v", cmds[c]))
s.WriteString("\n")
}
return strings.TrimRight(s.String(), "\n")
}
type groupType int
const (
groupUndefined groupType = iota
groupRoot
groupRequired
groupOptional
)
type node struct {
// tag should always be non-empty
tag string
// group will have the following values:
// groupRoot: nodes that are not in an explicit grouping
// groupRequired
// groupOptional
group groupType
next []*node
branch []*node
}
func (n node) String() string {
s := strings.Builder{}
s.WriteString(n.tag)
if n.next != nil {
for i := range n.next {
if n.next[i].group == groupRequired {
s.WriteString(" [")
} else if n.next[i].group == groupOptional {
s.WriteString(" (")
} else {
s.WriteString(" ")
}
s.WriteString(fmt.Sprintf("%s", n.next[i]))
if n.next[i].group == groupRequired {
s.WriteString("]")
} else if n.next[i].group == groupOptional {
s.WriteString(")")
}
}
}
if n.branch != nil {
for i := range n.branch {
s.WriteString(fmt.Sprintf("|%s", n.branch[i]))
}
}
return s.String()
}

View file

@ -0,0 +1,287 @@
package commandline
import (
"fmt"
"strings"
)
// ParseCommandTemplate turns a string representation of a command template
// into a machine friendly representation
//
// Syntax
// [ a ] required keyword
// ( a ) optional keyword
// [ a | b | ... ] required selection
// ( a | b | ... ) optional selection
//
// groups can be embedded in one another
//
// Placeholders
// %V numeric value
// %I irrational number value
// %S string
// %F file name
// %* allow anything to follow this point
//
// note that a placeholder will implicitly be treated as a separate token
//
func ParseCommandTemplate(template []string) (*Commands, error) {
cmds := make(Commands, 0, 10)
for t := range template {
defn := template[t]
// tidy up spaces in definition string - we don't want more than one
// consecutive space
defn = strings.Join(strings.Fields(defn), " ")
// normalise to upper case
defn = strings.ToUpper(defn)
// parse the definition for this command
p, d, err := parseDefinition(defn, "")
if err != nil {
return nil, NewParseError(defn, d, fmt.Errorf("%s: %s", err, defn))
}
// add to list of commands (order doesn't matter at this stage)
cmds = append(cmds, p)
// check that parsing was complete
if d < len(defn)-1 {
return nil, NewParseError(defn, len(defn), fmt.Errorf("outstanding characters in command definition"))
}
}
return &cmds, nil
}
func parseDefinition(defn string, trigger string) (*node, int, error) {
// handle special conditions before parsing loop
if defn[0] == '(' || defn[0] == '[' {
return nil, 0, fmt.Errorf("first argument of a group should not be itself be the start of a group")
}
// working nodes should be initialised with this function
newWorkingNode := func() *node {
if trigger == "(" {
return &node{group: groupOptional}
} else if trigger == "[" {
return &node{group: groupRequired}
} else if trigger == "|" {
// group is left unset for the branch trigger. value will be set
// once parseDefinition() has returned
return &node{}
} else if trigger == "" {
return &node{group: groupRoot}
}
panic("unknown trigger")
}
wn := newWorkingNode() // working node (attached to the end of the sequence when required)
sn := wn // start node (of the sequence)
addNext := func(nx *node) error {
// new node is already in the correct place
if sn == nx {
wn = newWorkingNode()
return nil
}
// do not add nodes that have no content
if nx.tag == "" {
return nil
}
// sanity check to make sure we're not clobbering an active working
// node
if wn != nx && wn.tag != "" {
return fmt.Errorf("orphaned working node: %s", wn.tag)
}
// create a new next array if necessary, and add new node to the end of
// it
if sn.next == nil {
sn.next = make([]*node, 0)
}
sn.next = append(sn.next, nx)
// create new working node
wn = newWorkingNode()
return nil
}
addBranch := func(bx *node) error {
// do not add nodes that have no content
if bx.tag == "" {
return nil
}
// sanity check to make sure we're not clobbering an active working
// node
if wn != bx && wn.tag != "" {
return fmt.Errorf("orphaned working node: %s", wn.tag)
}
// create a new next array if necessary, and add new node to the end of
// it
if sn.branch == nil {
sn.branch = make([]*node, 0)
}
sn.branch = append(sn.branch, bx)
// create new working node
wn = newWorkingNode()
return nil
}
for i := 0; i < len(defn); i++ {
switch defn[i] {
case '[':
err := addNext(wn)
if err != nil {
return nil, i, err
}
i++
ns, e, err := parseDefinition(defn[i:], "[")
if err != nil {
return nil, i + e, err
}
ns.group = groupRequired
err = addNext(ns)
if err != nil {
return nil, i, err
}
i += e
case '(':
err := addNext(wn)
if err != nil {
return nil, i, err
}
i++
ns, e, err := parseDefinition(defn[i:], "(")
if err != nil {
return nil, i + e, err
}
ns.group = groupOptional
err = addNext(ns)
if err != nil {
return nil, i, err
}
i += e
case ']':
err := addNext(wn)
if err != nil {
return nil, i, err
}
if trigger == "[" {
return sn, i, nil
}
if trigger == "|" {
return sn, i - 1, nil
}
return nil, i, fmt.Errorf("unexpected ]")
case ')':
err := addNext(wn)
if err != nil {
return nil, i, err
}
if trigger == "(" {
return sn, i, nil
}
if trigger == "|" {
return sn, i - 1, nil
}
return nil, i, fmt.Errorf("unexpected )")
case '|':
err := addNext(wn)
if err != nil {
return nil, i, err
}
if trigger == "|" {
return sn, i - 1, nil
}
i++
nb, e, err := parseDefinition(defn[i:], "|")
if err != nil {
return nil, i + e, err
}
// change group to current group - we don't want any unresolved
// instances of groupUndefined
nb.group = sn.group
err = addBranch(nb)
if err != nil {
return nil, i, err
}
i += e
case '%':
if wn.tag != "" {
return nil, i, fmt.Errorf("placeholders cannot be part of a wider string")
}
if i == len(defn)-1 {
return nil, i, fmt.Errorf("orphaned placeholder directives not allowed")
}
// add placeholder to working node if it is recognised
p := string(defn[i+1])
if p != "V" && p != "I" && p != "S" && p != "F" && p != "*" && p != "%" {
return nil, i, fmt.Errorf("unknown placeholder directive (%s)", wn.tag)
}
wn.tag = fmt.Sprintf("%%%s", p)
// we've consumed an additional character when retreiving a value
// for p
i++
case ' ':
// tokens are separated by spaces as well group markers
err := addNext(wn)
if err != nil {
return nil, i, err
}
default:
wn.tag += string(defn[i])
}
}
// make sure we've added working node to the sequence
err := addNext(wn)
if err != nil {
return nil, len(defn), err
}
// if we reach this point and trigger is non-empty then that implies that
// the opening trigger has not been closed correctly
if trigger == "[" || trigger == "(" {
return nil, len(defn), fmt.Errorf(fmt.Sprintf("unclosed %s group", trigger))
}
return sn, len(defn), nil
}

View file

@ -0,0 +1,164 @@
package commandline_test
import (
"gopher2600/debugger/commandline"
"os"
"sort"
"strings"
"testing"
"github.com/bradleyjkemp/memviz"
)
func expectFailure(t *testing.T, err error) {
t.Helper()
if err == nil {
t.Errorf("expected failure")
}
}
func expectSuccess(t *testing.T, err error) bool {
t.Helper()
if err != nil {
t.Errorf("%s", err)
return false
}
return true
}
// expectEquality compares a template, as passed to ParseCommandTemplate(),
// with the String() output of the resulting Commands object. both outputs
// should be the same.
//
// the template is transformed slightly. each entry in the array is joined with
// a newline character and also converted to uppercase. this is okay because
// we're only really interested in how the groupings and branching is
// represented.
func expectEquality(t *testing.T, template []string, cmds *commandline.Commands) bool {
t.Helper()
if strings.ToUpper(strings.Join(template, "\n")) != cmds.String() {
t.Errorf("parsed commands do not match template")
return false
}
return true
}
// memvizOutput produces a dot file that can be used to identify how elements
// in the Commands object are connected
func memvizOutput(t *testing.T, filename string, cmds *commandline.Commands) {
if len(filename) < 4 || strings.ToLower(filename[len(filename)-4:]) != ".dot" {
filename += ".dot"
}
f, err := os.Create(filename)
defer f.Close()
if err != nil {
t.Errorf("%s", err)
} else {
memviz.Map(f, cmds)
}
}
func TestParser_badGroupings(t *testing.T) {
var err error
// optional groups must be closed
_, err = commandline.ParseCommandTemplate([]string{"TEST (arg"})
expectFailure(t, err)
// required groups must be closed
_, err = commandline.ParseCommandTemplate([]string{"TEST (arg]"})
expectFailure(t, err)
}
func TestParser_goodGroupings(t *testing.T) {
var template []string
var cmds *commandline.Commands
var err error
template = []string{"TEST (1 [2] [3] [4] [5])"}
cmds, err = commandline.ParseCommandTemplate(template)
if expectSuccess(t, err) {
expectEquality(t, template, cmds)
}
template = []string{"TEST [1 [2] [3] [4] [5]]"}
cmds, err = commandline.ParseCommandTemplate(template)
if expectSuccess(t, err) {
expectEquality(t, template, cmds)
}
}
func TestParser_rootGroupings(t *testing.T) {
var template []string
var cmds *commandline.Commands
var err error
template = []string{"TEST (arg) %*"}
cmds, err = commandline.ParseCommandTemplate(template)
if expectSuccess(t, err) {
expectEquality(t, template, cmds)
}
}
func TestParser_placeholders(t *testing.T) {
var template []string
var cmds *commandline.Commands
var err error
// placeholder directives must be complete
_, err = commandline.ParseCommandTemplate([]string{"TEST foo %"})
expectFailure(t, err)
// placeholder directives must be recognised
_, err = commandline.ParseCommandTemplate([]string{"TEST foo %q"})
expectFailure(t, err)
// double %% is a valid placeholder directive
template = []string{"TEST foo %%"}
cmds, err = commandline.ParseCommandTemplate(template)
if expectSuccess(t, err) {
expectEquality(t, template, cmds)
}
// placeholder directives must be separated from surrounding text
cmds, err = commandline.ParseCommandTemplate([]string{"TEST foo%%"})
expectFailure(t, err)
}
func TestParser_doubleArgs(t *testing.T) {
var template []string
var cmds *commandline.Commands
var err error
template = []string{"TEST (egg|fog|nug nog|big) (tug)"}
cmds, err = commandline.ParseCommandTemplate(template)
if expectSuccess(t, err) {
sort.Stable(cmds)
expectEquality(t, template, cmds)
}
}
func TestParser(t *testing.T) {
var template []string
var cmds *commandline.Commands
var err error
template = []string{
"DISPLAY (OFF|DEBUG|SCALE [%V]|DEBUGCOLORS)",
"DROP [BREAK|TRAP|WATCH] [%S]",
"GREP %V",
"TEST [FOO [%S]|BAR] (EGG [%S]|FOG|NOG NUG) (TUG)",
}
cmds, err = commandline.ParseCommandTemplate(template)
if expectSuccess(t, err) {
expectEquality(t, template, cmds)
//memvizOutput(t, "1", cmds)
}
}

View file

@ -0,0 +1,210 @@
package commandline
import (
"fmt"
"strconv"
"strings"
"time"
)
const cycleDuration = 500 * time.Millisecond
// TabCompletion keeps track of the most recent tab completion attempt
type TabCompletion struct {
commands *Commands
matches []string
match int
lastCompletion string
}
// NewTabCompletion initialises a new TabCompletion instance
//
// completion works best if commands has been sorted
func NewTabCompletion(commands *Commands) *TabCompletion {
tc := new(TabCompletion)
tc.commands = commands
tc.Reset()
return tc
}
// Complete 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) Complete(input string) string {
// split input tokens -- it's easier to work with tokens
tokens := TokeniseInput(input)
// common function that polishes off a successful Complete()
endGuess := func() string {
if tc.match >= 0 {
tokens.ReplaceEnd(tc.matches[tc.match])
tc.lastCompletion = fmt.Sprintf("%s ", tokens.String())
} else {
// no matches found so completion string is by definition, the same
// as the input
tc.lastCompletion = input
}
return tc.lastCompletion
}
// if the input argument is the same as what we returned last time, then
// cycle through the options that were compiled last time
if tc.lastCompletion == input && tc.match >= 0 {
tc.match++
if tc.match >= len(tc.matches) {
tc.match = 0
}
return endGuess()
}
// new tabcompletion session
// reinitialise matches array
tc.Reset()
// no need to to anything if input ends with a space
if strings.HasSuffix(input, " ") {
return input
}
// get first token
tok, ok := tokens.Get()
if !ok {
return input
}
tok = strings.ToUpper(tok)
// look for match
for i := range *tc.commands {
n := (*tc.commands)[i]
// if there is an exact match then recurse into the node looking for
// where the last token coincides with the node tree
if tok == n.tag {
// we may have encountered partial matches earlier in the loop. now
// that we have found an exact match however, we need to make sure
// the match list is empty so that we don't erroneously trigger the
// match-cycling branch above.
tc.Reset()
// recurse
tokens.Unget()
tc.buildMatches(n, tokens)
return endGuess()
}
// if there is a partial match, then add the current node to the list
// of matches
if tokens.IsEnd() && len(tok) < len(n.tag) && tok == n.tag[:len(tok)] {
tc.matches = append(tc.matches, n.tag)
tc.match = 0
}
}
return endGuess()
}
// Reset is used to clear an outstanding completion session. note that this
// only really needs to be called if the input argument to Complete() is not
// different to the previous return value from that function, and you want to
// start a new completion session.
func (tc *TabCompletion) Reset() {
tc.matches = make([]string, 0)
tc.match = -1
}
func (tc *TabCompletion) buildMatches(n *node, tokens *Tokens) {
// if there is no more input then return true (validation has passed) if
// the node is optional, false if it is required
tok, ok := tokens.Get()
if !ok {
return
}
match := true
switch n.tag {
case "%V":
_, err := strconv.ParseInt(tok, 0, 32)
match = err == nil
case "%I":
_, err := strconv.ParseFloat(tok, 32)
match = err == nil
case "%S":
// accept anything
case "%F":
// TODO: filename completion
case "%*":
// this placeholder indicates that the rest of the tokens can be
// ignored.
return
default:
// case sensitive matching
tok = strings.ToUpper(tok)
match = tok == n.tag
}
// if token doesn't match this node, check branches. if there are no
// branches, return false (validation has failed)
if !match {
// if there is a partial match, then add the current node to the list
// of matches
if tokens.IsEnd() && len(tok) < len(n.tag) && tok == n.tag[:len(tok)] {
tc.matches = append(tc.matches, n.tag)
tc.match = 0
}
if n.branch == nil {
return
}
// take a note of current token position. if the token wanders past
// this point as a result of a branch then we can see that the branch
// was deeper then just one token. if this is the case then we can see
// that the branch was *partially* accepted and that we should not
// proceed onto next-nodes from here.
tokenAt := tokens.curr
for bi := range n.branch {
// we want to use the current token again so we unget() the last token
// so that it is available at the beginning of the recursed validate()
// function
tokens.Unget()
tc.buildMatches(n.branch[bi], tokens)
}
// the key to this condition is the tokenAt variable. see note above.
if n.group == groupOptional && len(tc.matches) == 0 && tokenAt == tokens.curr {
tokens.Unget()
} else {
return
}
}
// token does match and there are no more tokens to consume so we can add
// this successful token to the list of matches
//
// note that this is specific to tab-completion, validation has no
// equivalent. the purpose of this is to cause the Complete() function
// above to replace the last token with a normalised version of that token
// and to suffix it with a space.
if tokens.IsEnd() {
tc.matches = append(tc.matches, tok)
tc.match = 0
return
}
// token does match this node. check nodes that follow on.
for nx := range n.next {
tc.buildMatches(n.next[nx], tokens)
}
return
}

View file

@ -0,0 +1,192 @@
package commandline_test
import (
"gopher2600/debugger/commandline"
"sort"
"testing"
)
func TestTabCompletion(t *testing.T) {
var cmds *commandline.Commands
var tc *commandline.TabCompletion
var completion, expected string
var err error
cmds, err = commandline.ParseCommandTemplate([]string{
"TEST [arg]",
"TEST1 [arg]",
"FOO [bar|baz] wibble",
})
if err != nil {
t.Fatalf("%s", err)
}
sort.Stable(cmds)
tc = commandline.NewTabCompletion(cmds)
completion = "TE"
expected = "TEST "
completion = tc.Complete(completion)
if completion != expected {
t.Errorf("expecting '%s' got '%s'", expected, completion)
}
// next completion option
expected = "TEST1 "
completion = tc.Complete(completion)
if completion != expected {
t.Errorf("expecting '%s' got '%s'", expected, completion)
}
// cycle back to the first completion option
expected = "TEST "
completion = tc.Complete(completion)
if completion != expected {
t.Errorf("expecting '%s' got '%s'", expected, completion)
}
tc.Reset()
completion = "TEST a"
expected = "TEST ARG "
completion = tc.Complete(completion)
if completion != expected {
t.Errorf("expecting '%s' got '%s'", expected, completion)
}
tc.Reset()
completion = "FOO ba"
expected = "FOO BAR "
completion = tc.Complete(completion)
if completion != expected {
t.Errorf("expecting '%s' got '%s'", expected, completion)
}
expected = "FOO BAZ "
completion = tc.Complete(completion)
if completion != expected {
t.Errorf("expecting '%s' got '%s'", expected, completion)
}
// the completer will preserve whitespace
tc.Reset()
completion = "FOO bar wib"
expected = "FOO bar WIBBLE "
completion = tc.Complete(completion)
if completion != expected {
t.Errorf("expecting '%s' got '%s'", expected, completion)
}
}
func TestTabCompletion_placeholders(t *testing.T) {
var cmds *commandline.Commands
var tc *commandline.TabCompletion
var completion, expected string
var err error
cmds, err = commandline.ParseCommandTemplate([]string{
"TEST %V (foo|bar)",
})
if err != nil {
t.Fatalf("%s", err)
}
sort.Stable(cmds)
tc = commandline.NewTabCompletion(cmds)
completion = "TEST 100 f"
expected = "TEST 100 FOO "
completion = tc.Complete(completion)
if completion != expected {
t.Errorf("expecting '%s' got '%s'", expected, completion)
}
}
func TestTabCompletion_doubleArgs(t *testing.T) {
var cmds *commandline.Commands
var tc *commandline.TabCompletion
var completion, expected string
var err error
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (egg|fog|nug nog|big) (tug)"})
if err != nil {
t.Fatalf("%s", err)
}
tc = commandline.NewTabCompletion(cmds)
completion = "TEST eg"
expected = "TEST EGG "
completion = tc.Complete(completion)
if completion != expected {
t.Errorf("expecting '%s' got '%s'", expected, completion)
}
completion = "TEST egg T"
expected = "TEST egg TUG "
completion = tc.Complete(completion)
if completion != expected {
t.Errorf("expecting '%s' got '%s'", expected, completion)
}
completion = "TEST n"
expected = "TEST NUG "
completion = tc.Complete(completion)
if completion != expected {
t.Errorf("expecting '%s' got '%s'", expected, completion)
}
completion = "TEST nug N"
expected = "TEST nug NOG "
completion = tc.Complete(completion)
if completion != expected {
t.Errorf("expecting '%s' got '%s'", expected, completion)
}
completion = "TEST T"
expected = "TEST TUG "
completion = tc.Complete(completion)
if completion != expected {
t.Errorf("expecting '%s' got '%s'", expected, completion)
}
completion = "TEST nug nog T"
expected = "TEST nug nog TUG "
completion = tc.Complete(completion)
if completion != expected {
t.Errorf("expecting '%s' got '%s'", expected, completion)
}
}
func TestTabCompletion_complex(t *testing.T) {
var cmds *commandline.Commands
var tc *commandline.TabCompletion
var completion, expected string
var err error
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (arg [%V|bar]|foo) %*"})
if err != nil {
t.Fatalf("%s", err)
}
tc = commandline.NewTabCompletion(cmds)
completion = "TEST ar"
expected = "TEST ARG "
completion = tc.Complete(completion)
if completion != expected {
t.Errorf("expecting '%s' got '%s'", expected, completion)
}
completion = "TEST arg b"
expected = "TEST arg BAR "
completion = tc.Complete(completion)
if completion != expected {
t.Errorf("expecting '%s' got '%s'", expected, completion)
}
completion = "TEST arg 10 wib"
expected = "TEST arg 10 wib"
completion = tc.Complete(completion)
if completion != expected {
t.Errorf("expecting '%s' got '%s'", expected, completion)
}
}

View file

@ -1,4 +1,4 @@
package input
package commandline
import (
"fmt"
@ -8,17 +8,20 @@ import (
// Tokens represents tokenised input. This can be used to walk through the
// input string (using get()) for eas(ier) parsing
type Tokens struct {
input string
tokens []string
curr int
}
func (tk *Tokens) String() string {
s := strings.Builder{}
for i := range tk.tokens {
s.WriteString(tk.tokens[i])
s.WriteString(" ")
}
return s.String()
return tk.input
// s := strings.Builder{}
// for i := range tk.tokens {
// s.WriteString(tk.tokens[i])
// s.WriteString(" ")
// }
// return s.String()
}
// Reset begins the token traversal process from the beginning
@ -26,6 +29,11 @@ func (tk *Tokens) Reset() {
tk.curr = 0
}
// IsEnd returns true if we're at the end of the token list
func (tk Tokens) IsEnd() bool {
return tk.curr >= len(tk.tokens)
}
// Remainder returns the remaining tokens as a string
func (tk Tokens) Remainder() string {
return strings.Join(tk.tokens[tk.curr:], " ")
@ -36,9 +44,14 @@ 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)
// ReplaceEnd changes the last entry of the token list
func (tk *Tokens) ReplaceEnd(newEnd string) {
// change end of original string
t := strings.LastIndex(tk.input, tk.tokens[len(tk.tokens)-1])
tk.input = tk.input[:t] + newEnd
// change tokens
tk.tokens[len(tk.tokens)-1] = newEnd
}
// Get returns the next token in the list, and a success boolean - if the end
@ -76,9 +89,12 @@ func TokeniseInput(input string) *Tokens {
// remove leading/trailing space
input = strings.TrimSpace(input)
// divide user input into tokens
// divide user input into tokens. removes excess white space
tk.tokens = tokeniseInput(input)
// take a note of the raw input
tk.input = input
// normalise variations in syntax
for i := 0; i < len(tk.tokens); i++ {
// normalise hex notation
@ -93,7 +109,7 @@ func TokeniseInput(input string) *Tokens {
// 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())
// (eg. TabCompletion.Complete())
func tokeniseInput(input string) []string {
return strings.Fields(input)
}

View file

@ -0,0 +1,173 @@
package commandline
import (
"fmt"
"strconv"
"strings"
)
// Validate input string against command defintions
func (cmds Commands) Validate(input string) error {
return cmds.ValidateTokens(TokeniseInput(input))
}
// ValidateTokens like Validate, but works on tokens rather than an input
// string
func (cmds Commands) ValidateTokens(tokens *Tokens) error {
cmd, ok := tokens.Peek()
if !ok {
return nil
}
cmd = strings.ToUpper(cmd)
for n := range cmds {
if cmd == cmds[n].tag {
err := cmds[n].validate(tokens)
if err != nil {
return fmt.Errorf("%s for %s", err, cmd)
}
if tokens.Remaining() > 0 {
return fmt.Errorf("too many arguments for %s", cmd)
}
return nil
}
}
return fmt.Errorf("unrecognised command (%s)", cmd)
}
// branches creates a readable string, listing all the branches of the node
func branches(n *node) string {
s := strings.Builder{}
s.WriteString(n.tag)
for bi := range n.branch {
s.WriteString(", ")
s.WriteString(n.branch[bi].tag)
}
return s.String()
}
func (n *node) validate(tokens *Tokens) error {
// if there is no more input then return true (validation has passed) if
// the node is optional, false if it is required
tok, ok := tokens.Get()
if !ok {
// we treat arguments in the root-group as though they are required,
// with the exception of the %* placeholder
if n.group == groupRequired || (n.group == groupRoot && n.tag != "%*") {
// replace placeholder arguments with something a little less cryptic
switch n.tag {
case "%*":
return fmt.Errorf("missing required arguments")
case "%S":
return fmt.Errorf("missing string argument")
case "%V":
return fmt.Errorf("missing numeric argument")
case "%I":
return fmt.Errorf("missing floating-point argument")
case "%F":
return fmt.Errorf("missing filename argument")
}
return fmt.Errorf("missing a required argument (%s)", branches(n))
}
return nil
}
// check to see if input matches this node. using placeholder matching if
// appropriate
match := true
// default error in case nothing matches - replaced as necessary
err := fmt.Errorf("unrecognised argument (%s)", tok)
switch n.tag {
case "%V":
_, err := strconv.ParseInt(tok, 0, 32)
if err != nil {
err = fmt.Errorf("numeric argument required (%s is not numeric)", tok)
match = false
}
case "%I":
_, err := strconv.ParseFloat(tok, 32)
if err != nil {
err = fmt.Errorf("float argument required (%s is not numeric)", tok)
match = false
}
case "%S":
// accept anything
case "%F":
// accept anything (note: filename is distinct from %S when we use it
// for tab-completion)
case "%*":
// this placeholder indicates that the rest of the tokens can be
// ignored.
// consume the rest of the tokens without a care
for ok {
_, ok = tokens.Get()
}
return nil
default:
// case sensitive matching
tok = strings.ToUpper(tok)
match = tok == n.tag
}
// if input doesn't match this node, check branches
if !match {
if n.branch != nil {
for bi := range n.branch {
// recursing into the validate function means we need to use the
// same token as above. Unget() prepares the tokens object for
// that.
tokens.Unget()
if n.branch[bi].validate(tokens) == nil {
// break loop on first successful branch
match = true
break
}
}
// tricky condition: if we've not found anything in any of the
// branches and this is an optional group, then claim that we have
// matched this group and prepare tokens object for additional
// nodes. if group is not optional then return error.
if !match {
if n.group == groupOptional {
tokens.Unget()
} else {
return err
}
}
return nil
}
if !match {
return err
}
}
// input does match this node. check nodes that follow on.
for ni := range n.next {
err := n.next[ni].validate(tokens)
if err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,230 @@
package commandline_test
import (
"fmt"
"gopher2600/debugger/commandline"
"testing"
)
func TestValidation_required(t *testing.T) {
var cmds *commandline.Commands
var err error
cmds, err = commandline.ParseCommandTemplate([]string{"TEST [arg]"})
if err != nil {
t.Fatalf("%s", err)
}
err = cmds.Validate("TEST arg foo")
if err == nil {
t.Errorf("matches but shouldn't")
} else {
fmt.Println(err)
}
err = cmds.Validate("TEST arg")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
err = cmds.Validate("TEST")
if err == nil {
t.Errorf("matches but shouldn't")
} else {
fmt.Println(err)
}
}
func TestValidation_optional(t *testing.T) {
var cmds *commandline.Commands
var err error
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (arg)"})
if err != nil {
t.Fatalf("%s", err)
}
err = cmds.Validate("TEST")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
err = cmds.Validate("TEST arg")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
err = cmds.Validate("TEST arg foo")
if err == nil {
t.Errorf("matches but shouldn't")
} else {
fmt.Println(err)
}
err = cmds.Validate("TEST foo")
if err == nil {
t.Errorf("matches but shouldn't")
} else {
fmt.Println(err)
}
}
func TestValidation_branchesAndNumeric(t *testing.T) {
var cmds *commandline.Commands
var err error
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (arg [%V]|foo) %*"})
if err != nil {
t.Fatalf("%s", err)
}
err = cmds.Validate("TEST")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
err = cmds.Validate("TEST foo wibble")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
err = cmds.Validate("TEST arg")
if err == nil {
t.Errorf("matches but shouldn't")
} else {
fmt.Println(err)
}
// numeric argument matching
err = cmds.Validate("TEST arg 10")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
// failing a numeric argument match
err = cmds.Validate("TEST arg bar")
if err == nil {
t.Errorf("matches but shouldn't")
} else {
fmt.Println(err)
}
// ---------------
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (arg|foo) %V"})
if err != nil {
t.Fatalf("%s", err)
}
err = cmds.Validate("TEST arg")
if err == nil {
t.Errorf("matches but shouldn't")
} else {
fmt.Println(err)
}
err = cmds.Validate("TEST arg 10")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
err = cmds.Validate("TEST 10")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
}
func TestValidation_deepBranches(t *testing.T) {
var cmds *commandline.Commands
var err error
// retry numeric argument matching but with an option for a specific string
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (arg [%V|bar]|foo) %*"})
if err != nil {
t.Fatalf("%s", err)
}
err = cmds.Validate("TEST arg bar")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
err = cmds.Validate("TEST arg foo")
if err == nil {
t.Errorf("matches but shouldn't")
} else {
fmt.Println(err)
}
}
func TestValidation_tripleBranches(t *testing.T) {
var cmds *commandline.Commands
var err error
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (arg|foo|bar) wibble"})
if err != nil {
t.Fatalf("%s", err)
}
err = cmds.Validate("TEST foo wibble")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
err = cmds.Validate("TEST bar wibble")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
err = cmds.Validate("TEST wibble")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
}
func TestValidation_doubleArgs(t *testing.T) {
var cmds *commandline.Commands
var err error
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (nug nog|egg|cream) (tug)"})
if err != nil {
t.Fatalf("%s", err)
}
err = cmds.Validate("TEST nug nog")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
err = cmds.Validate("TEST egg tug")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
err = cmds.Validate("TEST nug nog tug")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
// ---------------
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (egg|fog|nug nog|big) (tug)"})
if err != nil {
t.Fatalf("%s", err)
}
err = cmds.Validate("TEST nug nog")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
err = cmds.Validate("TEST fog tug")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
err = cmds.Validate("TEST nug nog tug")
if err != nil {
t.Errorf("doesn't match but should: %s", err)
}
}

View file

@ -2,14 +2,15 @@ package debugger
import (
"fmt"
"gopher2600/debugger/commandline"
"gopher2600/debugger/console"
"gopher2600/debugger/input"
"gopher2600/errors"
"gopher2600/gui"
"gopher2600/hardware/cpu/result"
"gopher2600/symbols"
"gopher2600/television"
"os"
"sort"
"strconv"
"strings"
)
@ -49,8 +50,7 @@ const (
cmdScript = "SCRIPT"
cmdStep = "STEP"
cmdStepMode = "STEPMODE"
cmdStick0 = "STICK0"
cmdStick1 = "STICK1"
cmdStick = "STICK"
cmdSymbol = "SYMBOL"
cmdTIA = "TIA"
cmdTV = "TV"
@ -61,70 +61,62 @@ const (
cmdWatch = "WATCH"
)
// notes
// o KeywordStep can take a valid target
// o KeywordDisplay SCALE takes an additional argument but OFF and DEBUG do
// not. the %* is a compromise
// break/trap/watch values are parsed in parseTargets() function
// TODO: find some way to create valid templates using information from
// other sources
var commandTemplate = input.CommandTemplate{
cmdBall: "",
cmdBreak: "%*",
cmdCPU: "",
cmdCapture: "[END|%F]",
cmdCartridge: "",
cmdClear: "[BREAKS|TRAPS|WATCHES]",
cmdDebuggerState: "",
cmdDisassemble: "",
cmdDisplay: "[|OFF|DEBUG|SCALE|DEBUGCOLORS] %*", // see notes
cmdDrop: "[BREAK|TRAP|WATCH] %V",
cmdGrep: "%S %*",
cmdHexLoad: "%*",
cmdInsert: "%F",
cmdLast: "[|DEFN]",
cmdList: "[BREAKS|TRAPS|WATCHES]",
cmdMemMap: "",
cmdMissile: "",
cmdOnHalt: "[|OFF|RESTORE] %*",
cmdOnStep: "[|OFF|RESTORE] %*",
cmdPeek: "%*",
cmdPlayer: "",
cmdPlayfield: "",
cmdPoke: "%*",
cmdQuit: "",
cmdRAM: "",
cmdRIOT: "",
cmdReset: "",
cmdRun: "",
cmdScript: "%F",
cmdStep: "[|CPU|VIDEO|SCANLINE]", // see notes
cmdStepMode: "[|CPU|VIDEO]",
cmdStick0: "[LEFT|RIGHT|UP|DOWN|FIRE|CENTRE|NOFIRE]",
cmdStick1: "[LEFT|RIGHT|UP|DOWN|FIRE|CENTRE|NOFIRE]",
cmdSymbol: "%S [|ALL]",
cmdTIA: "[|FUTURE|HMOVE]",
cmdTV: "[|SPEC]",
cmdTerse: "",
cmdTrap: "%*",
cmdVerbose: "",
cmdVerbosity: "",
cmdWatch: "[READ|WRITE|] %V %*",
var expCommandTemplate = []string{
cmdBall,
cmdBreak + " [%*]",
cmdCPU,
cmdCapture + " [END|%F]",
cmdCartridge,
cmdClear + " [BREAKS|TRAPS|WATCHES]",
cmdDebuggerState,
cmdDisassemble,
cmdDisplay + " (OFF|DEBUG|SCALE [%I]|DEBUGCOLORS)", // see notes
cmdDrop + " [BREAK|TRAP|WATCH] %V",
cmdGrep + " %V",
cmdHelp + " %*",
cmdHexLoad + " %V %*",
cmdInsert + " %F",
cmdLast + " (DEFN)",
cmdList + " [BREAKS|TRAPS|WATCHES]",
cmdMemMap,
cmdMissile + "(0|1)",
cmdOnHalt + " (OFF|RESTORE|%*)",
cmdOnStep + " (OFF|RESTORE|%*)",
cmdPeek + " %V %*",
cmdPlayer + "(0|1)",
cmdPlayfield,
cmdPoke + " %V %*",
cmdQuit,
cmdRAM,
cmdRIOT,
cmdReset,
cmdRun,
cmdScript + " %F",
cmdStep + " (CPU|VIDEO|SCANLINE)", // see notes
cmdStepMode + " (CPU|VIDEO)",
cmdStick + "[0|1] [LEFT|RIGHT|UP|DOWN|FIRE|CENTRE|NOFIRE]",
cmdSymbol + " %V (ALL)",
cmdTIA + " (FUTURE|HMOVE)",
cmdTV + " (SPEC)",
cmdTerse,
cmdTrap + " [%*]",
cmdVerbose,
cmdVerbosity,
cmdWatch + " (READ|WRITE) [%V]",
}
// DebuggerCommands is the tree of valid commands
var DebuggerCommands input.Commands
var debuggerCommands *commandline.Commands
func init() {
var err error
// parse command template
DebuggerCommands, err = input.CompileCommandTemplate(commandTemplate, cmdHelp)
debuggerCommands, err = commandline.ParseCommandTemplate(expCommandTemplate)
if err != nil {
panic(fmt.Errorf("error compiling command template: %s", err))
fmt.Println(err)
os.Exit(100)
}
sort.Stable(debuggerCommands)
}
type parseCommandResult int
@ -146,25 +138,41 @@ const (
// TODO: categorise commands into script-safe and non-script-safe
func (dbg *Debugger) parseCommand(userInput *string) (parseCommandResult, error) {
// tokenise input
tokens := input.TokeniseInput(*userInput)
tokens := commandline.TokeniseInput(*userInput)
// check validity of input
err := DebuggerCommands.ValidateInput(tokens)
if err != nil {
return doNothing, err
}
// normalise user input -- we don't use the results in this
// function but we do use it futher-up. eg. when capturing user input to a
// script
*userInput = tokens.String()
// if there are no tokens in the input then return emptyInput directive
if tokens.Remaining() == 0 {
return emptyInput, nil
}
// normalise user input
*userInput = tokens.String()
// check validity of tokenised input
//
// the absolute best thing about this is that we don't need to worrying too
// much about the success of tokens.Get() in the command implementations
// below:
//
// tok, _ := tokens.Get()
//
// is an acceptable pattern
err := debuggerCommands.ValidateTokens(tokens)
if err != nil {
return doNothing, err
}
// check first token. if this token makes sense then we will consume the
// rest of the tokens appropriately
tokens.Reset()
command, _ := tokens.Get()
// take uppercase value of the first token. it's useful to take the
// uppercase value but we have to be careful when we do it because
command = strings.ToUpper(command)
switch command {
default:
return doNothing, fmt.Errorf("%s is not yet implemented", command)
@ -180,9 +188,7 @@ func (dbg *Debugger) parseCommand(userInput *string) (parseCommandResult, error)
dbg.print(console.Help, txt)
}
} else {
for k := range DebuggerCommands {
dbg.print(console.Help, k)
}
dbg.print(console.Help, debuggerCommands.String())
}
case cmdInsert:
@ -211,7 +217,7 @@ func (dbg *Debugger) parseCommand(userInput *string) (parseCommandResult, error)
dbg.disasm.Dump(os.Stdout)
case cmdGrep:
search := tokens.Remainder()
search, _ := tokens.Get()
output := strings.Builder{}
dbg.disasm.Grep(search, &output, false, 3)
if output.Len() == 0 {
@ -509,7 +515,7 @@ func (dbg *Debugger) parseCommand(userInput *string) (parseCommandResult, error)
}
case cmdDebuggerState:
_, err := dbg.parseInput("VERBOSITY; STEPMODE; ONHALT ECHO; ONSTEP ECHO", false)
_, err := dbg.parseInput("VERBOSITY; STEPMODE; ONHALT; ONSTEP", false)
if err != nil {
return doNothing, err
}
@ -670,61 +676,121 @@ func (dbg *Debugger) parseCommand(userInput *string) (parseCommandResult, error)
// information about the machine (sprites, playfield)
case cmdPlayer:
// TODO: argument to print either player 0 or player 1
plyr := -1
tok, _ := tokens.Get()
switch tok {
case "0":
plyr = 0
case "1":
plyr = 1
default:
tokens.Unget()
}
if dbg.machineInfoVerbose {
// arrange the two player's information side by side in order to
// save space and to allow for easy comparison
p0 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Player0), "\n")
p1 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Player1), "\n")
switch plyr {
case 0:
p0 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Player0), "\n")
dbg.print(console.MachineInfo, strings.Join(p0, "\n"))
ml := 0
for i := range p0 {
if len(p0[i]) > ml {
ml = len(p0[i])
}
}
case 1:
p1 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Player1), "\n")
dbg.print(console.MachineInfo, strings.Join(p1, "\n"))
s := strings.Builder{}
for i := range p0 {
if p0[i] != "" {
s.WriteString(fmt.Sprintf("%s %s | %s\n", p0[i], strings.Repeat(" ", ml-len(p0[i])), p1[i]))
default:
p0 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Player0), "\n")
p1 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Player1), "\n")
ml := 0
for i := range p0 {
if len(p0[i]) > ml {
ml = len(p0[i])
}
}
s := strings.Builder{}
for i := range p0 {
if p0[i] != "" {
s.WriteString(fmt.Sprintf("%s %s | %s\n", p0[i], strings.Repeat(" ", ml-len(p0[i])), p1[i]))
}
}
dbg.print(console.MachineInfo, s.String())
}
dbg.print(console.MachineInfo, s.String())
} else {
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player0)
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player1)
switch plyr {
case 0:
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player0)
case 1:
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player1)
default:
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player0)
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player1)
}
}
case cmdMissile:
// TODO: argument to print either missile 0 or missile 1
mssl := -1
tok, _ := tokens.Get()
switch tok {
case "0":
mssl = 0
case "1":
mssl = 1
default:
tokens.Unget()
}
if dbg.machineInfoVerbose {
// arrange the two missile's information side by side in order to
// save space and to allow for easy comparison
switch mssl {
case 0:
m0 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Missile0), "\n")
dbg.print(console.MachineInfo, strings.Join(m0, "\n"))
p0 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Missile0), "\n")
p1 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Missile1), "\n")
case 1:
m1 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Missile0), "\n")
dbg.print(console.MachineInfo, strings.Join(m1, "\n"))
ml := 0
for i := range p0 {
if len(p0[i]) > ml {
ml = len(p0[i])
default:
// arrange the two missile's information side by side in order to
// save space and to allow for easy comparison
m0 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Missile0), "\n")
m1 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Missile1), "\n")
ml := 0
for i := range m0 {
if len(m0[i]) > ml {
ml = len(m0[i])
}
}
}
s := strings.Builder{}
for i := range p0 {
if p0[i] != "" {
s.WriteString(fmt.Sprintf("%s %s | %s\n", p0[i], strings.Repeat(" ", ml-len(p0[i])), p1[i]))
s := strings.Builder{}
for i := range m0 {
if m0[i] != "" {
s.WriteString(fmt.Sprintf("%s %s | %s\n", m0[i], strings.Repeat(" ", ml-len(m0[i])), m1[i]))
}
}
dbg.print(console.MachineInfo, s.String())
}
dbg.print(console.MachineInfo, s.String())
} else {
dbg.printMachineInfo(dbg.vcs.TIA.Video.Missile0)
dbg.printMachineInfo(dbg.vcs.TIA.Video.Missile1)
switch mssl {
case 0:
dbg.printMachineInfo(dbg.vcs.TIA.Video.Missile0)
case 1:
dbg.printMachineInfo(dbg.vcs.TIA.Video.Missile1)
default:
dbg.printMachineInfo(dbg.vcs.TIA.Video.Missile0)
dbg.printMachineInfo(dbg.vcs.TIA.Video.Missile1)
}
}
case cmdBall:
@ -783,22 +849,15 @@ func (dbg *Debugger) parseCommand(userInput *string) (parseCommandResult, error)
}
}
case cmdStick0:
action, present := tokens.Get()
if present {
err := dbg.vcs.Controller.HandleStick(0, action)
if err != nil {
return doNothing, err
}
}
case cmdStick:
stick, _ := tokens.Get()
action, _ := tokens.Get()
case cmdStick1:
action, present := tokens.Get()
if present {
err := dbg.vcs.Controller.HandleStick(1, action)
if err != nil {
return doNothing, err
}
stickN, _ := strconv.Atoi(stick)
err := dbg.vcs.Controller.HandleStick(stickN, action)
if err != nil {
return doNothing, err
}
case cmdCapture:

View file

@ -1,7 +1,6 @@
package console
import (
"gopher2600/debugger/input"
"gopher2600/gui"
)
@ -21,7 +20,13 @@ type UserOutput interface {
type UserInterface interface {
Initialise() error
CleanUp()
RegisterTabCompleter(*input.TabCompletion)
RegisterTabCompleter(TabCompleter)
UserInput
UserOutput
}
// TabCompleter defines the operations required for tab completion
type TabCompleter interface {
Complete(input string) string
Reset()
}

View file

@ -2,7 +2,6 @@ package console
import (
"fmt"
"gopher2600/debugger/input"
"gopher2600/gui"
"os"
)
@ -21,7 +20,7 @@ func (pt *PlainTerminal) CleanUp() {
}
// RegisterTabCompleter adds an implementation of TabCompleter to the terminal
func (pt *PlainTerminal) RegisterTabCompleter(tc *input.TabCompletion) {
func (pt *PlainTerminal) RegisterTabCompleter(TabCompleter) {
}
// UserPrint is the plain terminal print routine

View file

@ -2,8 +2,8 @@ package debugger
import (
"fmt"
"gopher2600/debugger/commandline"
"gopher2600/debugger/console"
"gopher2600/debugger/input"
"gopher2600/debugger/monitor"
"gopher2600/disassembly"
"gopher2600/errors"
@ -184,7 +184,7 @@ func (dbg *Debugger) Start(iface console.UserInterface, filename string, initScr
}
defer dbg.console.CleanUp()
dbg.console.RegisterTabCompleter(input.NewTabCompletion(DebuggerCommands))
dbg.console.RegisterTabCompleter(commandline.NewTabCompletion(debuggerCommands))
err = dbg.loadCartridge(filename)
if err != nil {

View file

@ -2,7 +2,7 @@ package debugger
// Help contains the help text for the debugger's top level commands
var Help = map[string]string{
cmdBall: "Display the current state of the Ball sprite",
cmdBall: "Display the current state of the ball sprite",
cmdBreak: "Cause emulator to halt when conditions are met",
cmdCPU: "Display the current state of the CPU",
cmdCapture: "Start capturing entered commands to an extermnal script",
@ -19,11 +19,11 @@ var Help = map[string]string{
cmdLast: "Prints the result of the last cpu/video cycle",
cmdList: "List current entries for BREAKS and TRAPS",
cmdMemMap: "Display high-level VCS memory map",
cmdMissile: "Display the current state of the Missile 0/1 sprite",
cmdMissile: "Display the current state of the missile 0/1 sprite",
cmdOnHalt: "Commands to run whenever emulation is halted (separate commands with comma)",
cmdOnStep: "Commands to run whenever emulation steps forward an cpu/video cycle (separate commands with comma)",
cmdPeek: "Inspect an individual memory address",
cmdPlayer: "Display the current state of the Player 0/1 sprite",
cmdPlayer: "Display the current state of the player 0/1 sprite",
cmdPlayfield: "Display the current playfield data",
cmdPoke: "Modify an individual memory address",
cmdQuit: "Exits the emulator",
@ -34,8 +34,7 @@ var Help = map[string]string{
cmdScript: "Run commands from specified file",
cmdStep: "Step forward emulator one step (see STEPMODE command)",
cmdStepMode: "Change method of stepping: CPU or VIDEO",
cmdStick0: "Emulate a joystick input for Player 0",
cmdStick1: "Emulate a joystick input for Player 1",
cmdStick: "Emulate a joystick input for Player 0 or Player 1",
cmdSymbol: "Search for the address label symbol in disassembly. returns address",
cmdTIA: "Display current state of the TIA",
cmdTV: "Display the current TV state",

View file

@ -1,53 +0,0 @@
package input
// Commands is the root of the argument "tree"
type Commands map[string]commandArgList
// 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 {
// to indicate indeterminancy, 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
}
// argType defines the expected argument type
type argType int
// the possible values for argType
const (
argKeyword argType = iota
argFile
argValue
argString
argIndeterminate
argNode
)
// commandArg specifies the type and properties of an individual argument
type commandArg struct {
typ argType
required bool
values interface{}
}

View file

@ -1,61 +0,0 @@
package input
import (
"fmt"
"strings"
)
// string functions for the three types:
//
// o Commands
// o commandArgList
// o commandArg
//
// calling String() on Commands should reproduce the template from which the
// commands were compiled
func (cmd Commands) String() string {
s := strings.Builder{}
for k, v := range cmd {
s.WriteString(k)
s.WriteString(fmt.Sprintf("%s", v))
s.WriteString("\n")
}
return s.String()
}
func (a commandArgList) String() string {
s := strings.Builder{}
for i := range a {
s.WriteString(fmt.Sprintf(" %s", a[i]))
}
return s.String()
}
func (c commandArg) String() string {
switch c.typ {
case argKeyword:
s := "["
switch values := c.values.(type) {
case []string:
for i := range values {
s = fmt.Sprintf("%s%s|", s, values[i])
}
s = strings.TrimSuffix(s, "|")
case *Commands:
s = fmt.Sprintf("%s<commands>", s)
default:
s = fmt.Sprintf("%s%T", s, values)
}
return fmt.Sprintf("%s]", s)
case argFile:
return "%F"
case argValue:
return "%V"
case argString:
return "%S"
case argIndeterminate:
return "%*"
}
return "!!"
}

View file

@ -1,124 +0,0 @@
package input
import (
"strings"
"time"
)
const cycleDuration = 500 * time.Millisecond
// TabCompletion keeps track of the most recent tab completion attempt
type TabCompletion struct {
commands Commands
options []string
lastOption int
// lastGuess is the last string generated and returned by the GuessWord
// function. we use it to help decide whether to start a new completion
// session
lastGuess string
lastCompletionTime time.Time
}
// NewTabCompletion initialises a new TabCompletion instance
func NewTabCompletion(commands Commands) *TabCompletion {
tc := new(TabCompletion)
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 {
p := tokeniseInput(input)
if len(p) == 0 {
return input
}
// if input string is the same as the string last returned by this function
// AND it is within a time duration of 'cycleDuration' then return the next
// option
if input == tc.lastGuess { //&& time.Since(tc.lastCompletionTime) < cycleDuration {
// if there was only one option in the option list then return immediatly
if len(tc.options) <= 1 {
return input
}
// there is more than one completion option, so shorten the input by
// one word (getting rid of the last completion effort) and step to
// next option
p = p[:len(p)-1]
tc.lastOption++
if tc.lastOption >= len(tc.options) {
tc.lastOption = 0
}
} else {
if strings.HasSuffix(input, " ") {
return input
}
// this is a new tabcompletion session
tc.options = tc.options[:0]
tc.lastOption = 0
// get args for command
var arg commandArg
argList, ok := tc.commands[strings.ToUpper(p[0])]
if ok && len(input) > len(p[0]) && len(argList) != 0 && len(argList) > len(p)-2 {
arg = argList[len(p)-2]
} else {
arg.typ = argKeyword
arg.values = &tc.commands
}
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.values.(type) {
case *Commands:
for k := range *kw {
if len(trigger) <= len(k) && trigger == k[:len(trigger)] {
tc.options = append(tc.options, k)
}
}
case []string:
for _, k := range kw {
if len(trigger) <= len(k) && trigger == k[:len(trigger)] {
tc.options = append(tc.options, k)
}
}
default:
tc.options = append(tc.options, "unhandled argument type")
}
case argFile:
// TODO: filename completion
tc.options = append(tc.options, "<TODO: file-completion>")
}
// no completion options - return input unchanged
if len(tc.options) == 0 {
return input
}
}
// add guessed word to end of input-list and rejoin to form the output
p = append(p, tc.options[tc.lastOption])
tc.lastGuess = strings.Join(p, " ") + " "
// note current time. we'll use this to help decide whether to cycle
// through a list of options or to begin a new completion session
tc.lastCompletionTime = time.Now()
return tc.lastGuess
}

View file

@ -1,108 +0,0 @@
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 command is required, call the function with
// helpKeyword == ""
func CompileCommandTemplate(template CommandTemplate, helpKeyword string) (Commands, error) {
var err error
commands := Commands{}
for k, v := range template {
commands[k], err = compileTemplateFragment(v)
if err != nil {
return nil, fmt.Errorf("error compiling %s: %s", k, err)
}
}
if helpKeyword != "" {
commands[helpKeyword] = commandArgList{commandArg{typ: argKeyword, required: false, values: &commands}}
}
return commands, nil
}
func compileTemplateFragment(fragment string) (commandArgList, error) {
argl := commandArgList{}
placeholder := false
// loop over template string
for i := 0; i < len(fragment); i++ {
switch fragment[i] {
case '%':
placeholder = true
case '[':
// find end of option list
j := strings.LastIndex(fragment[i:], "]") + i
if j == -1 {
return nil, fmt.Errorf("unterminated option list")
}
// check for empty list
if i+1 == j {
return nil, fmt.Errorf("empty option list")
}
// split options list into individual options
options := strings.Split(fragment[i+1:j], "|")
if len(options) == 1 {
options = make([]string, 1)
options[0] = fragment[i+1 : j]
}
// decide whether the option is a required option - if there is an
// empty option then the option isn't required
req := true
for o := 0; o < len(options); o++ {
if options[o] == "" {
if req == false {
return nil, fmt.Errorf("option list can contain only one empty option")
}
req = false
}
optionParts := strings.Split(options[o], " ")
if len(optionParts) > 1 {
return nil, fmt.Errorf("option list can only contain single keywords (%s)", options[o])
}
}
argl = append(argl, commandArg{typ: argKeyword, required: req, values: options})
i = j
case ' ':
// skip spaces
default:
if placeholder {
switch fragment[i] {
case 'F':
argl = append(argl, commandArg{typ: argFile, required: true})
case 'S':
argl = append(argl, commandArg{typ: argString, required: true})
case 'V':
argl = append(argl, commandArg{typ: argValue, required: true})
case '*':
argl = append(argl, commandArg{typ: argIndeterminate, required: false})
default:
return nil, fmt.Errorf("unknown placeholder directive (%c)", fragment[i])
}
placeholder = false
i++
} else {
return nil, fmt.Errorf("unparsable fragment (%s)", fragment)
}
}
}
return argl, nil
}

View file

@ -1,53 +0,0 @@
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 nil
}
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.NewFormattedError(errors.CommandError, fmt.Sprintf("%s is not a debugging command", tokens[0]))
}
// too *many* arguments have been supplied
if len(tokens)-1 > args.maximumLen() {
return errors.NewFormattedError(errors.CommandError, 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.NewFormattedError(errors.CommandError, fmt.Sprintf("keyword required for %s", tokens[0]))
case argFile:
return errors.NewFormattedError(errors.CommandError, fmt.Sprintf("filename required for %s", tokens[0]))
case argValue:
return errors.NewFormattedError(errors.CommandError, fmt.Sprintf("numeric argument required for %s", tokens[0]))
case argString:
return errors.NewFormattedError(errors.CommandError, 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.NewFormattedError(errors.CommandError, fmt.Sprintf("too few arguments for %s", tokens[0]))
}
}
return nil
}

View file

@ -2,7 +2,7 @@ package debugger
import (
"fmt"
"gopher2600/debugger/input"
"gopher2600/debugger/commandline"
"gopher2600/errors"
"gopher2600/television"
"strings"
@ -62,7 +62,7 @@ func (trg genericTarget) FormatValue(fv interface{}) string {
}
// parseTarget uses a keyword to decide which part of the vcs to target
func parseTarget(dbg *Debugger, tokens *input.Tokens) (target, error) {
func parseTarget(dbg *Debugger, tokens *commandline.Tokens) (target, error) {
var trg target
var err error

View file

@ -6,8 +6,8 @@ package debugger
import (
"fmt"
"gopher2600/debugger/commandline"
"gopher2600/debugger/console"
"gopher2600/debugger/input"
"gopher2600/errors"
"strings"
)
@ -74,7 +74,7 @@ func (tr traps) list() {
}
}
func (tr *traps) parseTrap(tokens *input.Tokens) error {
func (tr *traps) parseTrap(tokens *commandline.Tokens) error {
_, present := tokens.Peek()
for present {
tgt, err := parseTarget(tr.dbg, tokens)

View file

@ -2,8 +2,8 @@ package debugger
import (
"fmt"
"gopher2600/debugger/commandline"
"gopher2600/debugger/console"
"gopher2600/debugger/input"
"gopher2600/errors"
"gopher2600/hardware/memory"
"strconv"
@ -127,13 +127,13 @@ func (wtc *watches) list() {
}
}
func (wtc *watches) parseWatch(tokens *input.Tokens, dbgmem *memoryDebug) error {
func (wtc *watches) parseWatch(tokens *commandline.Tokens, dbgmem *memoryDebug) error {
var event watchEvent
// read mode
mode, present := tokens.Get()
if !present {
return nil
return fmt.Errorf("watch address required")
}
mode = strings.ToUpper(mode)
switch mode {

View file

@ -17,10 +17,10 @@ type FormattedError struct {
// NewFormattedError is used to create a new instance of a FormattedError
func NewFormattedError(errno Errno, values ...interface{}) FormattedError {
ge := new(FormattedError)
ge.Errno = errno
ge.Values = values
return *ge
er := new(FormattedError)
er.Errno = errno
er.Values = values
return *er
}
func (er FormattedError) Error() string {