From 9e4205e6e8e994c762042bcfe42826e0b4bd7a48 Mon Sep 17 00:00:00 2001 From: lash Date: Sat, 8 Apr 2023 09:31:32 +0100 Subject: [PATCH] Reinstatate MNEXT, MPREV --- go/asm/menu.go | 7 +++-- go/asm/menu_test.go | 4 +-- go/engine/engine.go | 2 +- go/state/state.go | 23 ++++++++++++-- go/vm/debug.go | 16 ++++++++++ go/vm/debug_test.go | 20 ++++++++++++ go/vm/input.go | 74 ++++++++++++++++++++++++++++++++------------- go/vm/opcodes.go | 8 ++++- go/vm/runner.go | 24 +++++++++++++++ 9 files changed, 148 insertions(+), 30 deletions(-) diff --git a/go/asm/menu.go b/go/asm/menu.go index cf34dc4..2bdd795 100644 --- a/go/asm/menu.go +++ b/go/asm/menu.go @@ -14,7 +14,6 @@ const ( MENU_UP = _MENU_OFFSET + 1 MENU_NEXT = _MENU_OFFSET + 2 MENU_PREVIOUS = _MENU_OFFSET + 3 - //MENU_BROWSE = _MENU_OFFSET + 4 ) var ( @@ -23,7 +22,6 @@ var ( "UP": MENU_UP, "NEXT": MENU_NEXT, "PREVIOUS": MENU_PREVIOUS, - //"BROWSE": MENU_BROWSE, } ) @@ -66,15 +64,18 @@ func (mp *MenuProcessor) ToLines() []byte { postLines := []byte{} for _, v := range mp.items { - preLines = vm.NewLine(preLines, vm.MOUT, []string{v.choice, v.display}, nil, nil) switch v.code { case MENU_UP: + preLines = vm.NewLine(preLines, vm.MOUT, []string{v.choice, v.display}, nil, nil) postLines = vm.NewLine(postLines, vm.INCMP, []string{v.choice, "_"}, nil, nil) case MENU_NEXT: + preLines = vm.NewLine(preLines, vm.MNEXT, []string{v.choice, v.display}, nil, nil) postLines = vm.NewLine(postLines, vm.INCMP, []string{v.choice, ">"}, nil, nil) case MENU_PREVIOUS: + preLines = vm.NewLine(preLines, vm.MPREV, []string{v.choice, v.display}, nil, nil) postLines = vm.NewLine(postLines, vm.INCMP, []string{v.choice, "<"}, nil, nil) default: + preLines = vm.NewLine(preLines, vm.MOUT, []string{v.choice, v.display}, nil, nil) postLines = vm.NewLine(postLines, vm.INCMP, []string{v.choice, v.target}, nil, nil) } } diff --git a/go/asm/menu_test.go b/go/asm/menu_test.go index 46e070a..b4f41dc 100644 --- a/go/asm/menu_test.go +++ b/go/asm/menu_test.go @@ -35,8 +35,8 @@ func TestMenuInterpreter(t *testing.T) { t.Fatal(err) } expect := `MOUT 0 "inky" -MOUT 1 "pinky" -MOUT 2 "blinky clyde" +MNEXT 1 "pinky" +MPREV 2 "blinky clyde" MOUT 99 "tinky-winky" HALT INCMP 0 foo diff --git a/go/engine/engine.go b/go/engine/engine.go index 5e0aa98..e956cab 100644 --- a/go/engine/engine.go +++ b/go/engine/engine.go @@ -56,7 +56,7 @@ func(en *Engine) Init(sym string, ctx context.Context) error { // - no current bytecode is available // - input processing against bytcode failed func (en *Engine) Exec(input []byte, ctx context.Context) (bool, error) { - err := vm.CheckInput(input) + err := vm.ValidInput(input) if err != nil { return true, err } diff --git a/go/state/state.go b/go/state/state.go index 62f3f6a..1f73f44 100644 --- a/go/state/state.go +++ b/go/state/state.go @@ -214,6 +214,9 @@ func(st State) Where() (string, uint16) { // Next moves to the next sink page index. func(st State) Next() (uint16, error) { + if len(st.execPath) == 0 { + return 0, fmt.Errorf("state root node not yet defined") + } st.sizeIdx += 1 return st.sizeIdx, nil } @@ -222,6 +225,9 @@ func(st State) Next() (uint16, error) { // // Fails if try to move beyond index 0. func(st *State) Previous() (uint16, error) { + if len(st.execPath) == 0 { + return 0, fmt.Errorf("state root node not yet defined") + } if st.sizeIdx == 0 { return 0, fmt.Errorf("already at first index") } @@ -233,6 +239,9 @@ func(st *State) Previous() (uint16, error) { // // Two values are returned, for the "next" and "previous" options in that order. A false value means the option is not available in the current state. func(st *State) Sides() (bool, bool) { + if len(st.execPath) == 0 { + return false, false + } next := true if st.sizeIdx == 0 { return next, false @@ -240,6 +249,16 @@ func(st *State) Sides() (bool, bool) { return next, true } +// Top returns true if currently at topmode node. +// +// Fails if first Down() was never called. +func(st *State) Top() (bool, error) { + if len(st.execPath) == 0 { + return false, fmt.Errorf("state root node not yet defined") + } + return len(st.execPath) == 1, nil +} + // Down adds the given symbol to the command stack. // // Clears mapping and sink. @@ -259,10 +278,10 @@ func(st *State) Down(input string) { // // Fails if called at top frame. func(st *State) Up() (string, error) { - l := len(st.Cache) - if l == 0 { + if len(st.execPath) == 0 { return "", fmt.Errorf("exit called beyond top frame") } + l := len(st.Cache) l -= 1 m := st.Cache[l] for k, v := range m { diff --git a/go/vm/debug.go b/go/vm/debug.go index 2e885bd..6b912a8 100644 --- a/go/vm/debug.go +++ b/go/vm/debug.go @@ -129,6 +129,22 @@ func ParseAll(b []byte, w io.Writer) (int, error) { rs = fmt.Sprintf("%s %s \"%s\"\n", s, r, v) } } + case MNEXT: + r, v, bb, err := ParseMNext(b) + b = bb + if err == nil { + if w != nil { + rs = fmt.Sprintf("%s %s \"%s\"\n", s, r, v) + } + } + case MPREV: + r, v, bb, err := ParseMPrev(b) + b = bb + if err == nil { + if w != nil { + rs = fmt.Sprintf("%s %s \"%s\"\n", s, r, v) + } + } } if err != nil { return rn, err diff --git a/go/vm/debug_test.go b/go/vm/debug_test.go index 3605b10..7afd303 100644 --- a/go/vm/debug_test.go +++ b/go/vm/debug_test.go @@ -91,6 +91,26 @@ func TestToString(t *testing.T) { t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r) } + b = NewLine(nil, MNEXT, []string{"11", "nextmenu"}, nil, nil) + r, err = ToString(b) + if err != nil { + t.Fatal(err) + } + expect = "MNEXT 11 \"nextmenu\"\n" + if r != expect { + t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r) + } + + b = NewLine(nil, MPREV, []string{"222", "previous menu item"}, nil, nil) + r, err = ToString(b) + if err != nil { + t.Fatal(err) + } + expect = "MPREV 222 \"previous menu item\"\n" + if r != expect { + t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r) + } + b = NewLine(nil, MOUT, []string{"1", "foo"}, nil, nil) r, err = ToString(b) if err != nil { diff --git a/go/vm/input.go b/go/vm/input.go index 1fc0054..cfa0c47 100644 --- a/go/vm/input.go +++ b/go/vm/input.go @@ -19,7 +19,7 @@ var ( ) // CheckInput validates the given byte string as client input. -func CheckInput(input []byte) error { +func ValidInput(input []byte) error { if !inputRegex.Match(input) { return fmt.Errorf("Input '%s' does not match input format /%s/", input, inputRegexStr) } @@ -27,7 +27,7 @@ func CheckInput(input []byte) error { } // control characters for relative navigation. -func checkControl(input []byte) error { +func validControl(input []byte) error { if !ctrlRegex.Match(input) { return fmt.Errorf("Input '%s' does not match 'control' format /%s/", input, ctrlRegexStr) } @@ -35,36 +35,68 @@ func checkControl(input []byte) error { } // CheckSym validates the given byte string as a node symbol. -func CheckSym(input []byte) error { +func ValidSym(input []byte) error { if !symRegex.Match(input) { return fmt.Errorf("Input '%s' does not match 'sym' format /%s/", input, symRegexStr) } return nil } +// false if target is not valid +func valid(target []byte) bool { + var ok bool + if len(target) == 0 { + return false + } + + err := ValidSym(target) + if err == nil { + ok = true + } + + if !ok { + err = validControl(target) + if err == nil { + ok = true + } + } + return ok +} + +// CheckTarget tests whether the navigation state transition is available in the current state. +// +// Fails if target is formally invalid, or if navigation is unavailable. +func CheckTarget(target []byte, st *state.State) (bool, error) { + ok := valid(target) + if !ok { + return false, fmt.Errorf("invalid target: %x", target) + } + + switch target[0] { + case '_': + topOk, err := st.Top() + if err!= nil { + return false, err + } + return topOk, nil + case '<': + _, prevOk := st.Sides() + return prevOk, nil + case '>': + nextOk, _ := st.Sides() + return nextOk, nil + } + return true, nil +} + // route parsed target symbol to navigation state change method, func applyTarget(target []byte, st *state.State, ctx context.Context) (string, uint16, error) { var err error - var valid bool sym, idx := st.Where() - err = CheckInput(target) - if err == nil { - valid = true - } - - if !valid { - err = CheckSym(target) - if err == nil { - valid = true - } - } - - if !valid { - err = checkControl(target) - if err == nil { - valid = true - } + ok := valid(target) + if !ok { + return sym, idx, fmt.Errorf("invalid input: %x", target) } switch target[0] { diff --git a/go/vm/opcodes.go b/go/vm/opcodes.go index 9072b1a..77855b8 100644 --- a/go/vm/opcodes.go +++ b/go/vm/opcodes.go @@ -17,7 +17,9 @@ const ( INCMP = 8 MSIZE = 9 MOUT = 10 - _MAX = 10 + MNEXT = 11 + MPREV = 12 + _MAX = 12 ) var ( @@ -33,6 +35,8 @@ var ( INCMP: "INCMP", MSIZE: "MSIZE", MOUT: "MOUT", + MNEXT: "MNEXT", + MPREV: "MPREV", } OpcodeIndex = map[string]Opcode { @@ -47,6 +51,8 @@ var ( "INCMP": INCMP, "MSIZE": MSIZE, "MOUT": MOUT, + "MNEXT": MNEXT, + "MPREV": MPREV, } ) diff --git a/go/vm/runner.go b/go/vm/runner.go index 9db606a..eb2e7cb 100644 --- a/go/vm/runner.go +++ b/go/vm/runner.go @@ -44,6 +44,10 @@ func Run(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ( b, err = RunMSize(b, st, rs, ctx) case MOUT: b, err = RunMOut(b, st, rs, ctx) + case MNEXT: + b, err = RunMNext(b, st, rs, ctx) + case MPREV: + b, err = RunMPrev(b, st, rs, ctx) case HALT: b, err = RunHalt(b, st, rs, ctx) return b, err @@ -253,6 +257,26 @@ func RunMOut(b []byte, st *state.State, rs resource.Resource, ctx context.Contex return b, err } +// RunMNext executes the MNEXT opcode +func RunMNext(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { + selector, display, b, err := ParseMNext(b) + if err != nil { + return b, err + } + err = rs.SetMenuBrowse(selector, display, false) + return b, err +} + +// RunMPrev executes the MPREV opcode +func RunMPrev(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { + selector, display, b, err := ParseMPrev(b) + if err != nil { + return b, err + } + err = rs.SetMenuBrowse(selector, display, false) + return b, err +} + // retrieve data for key func refresh(key string, rs resource.Resource, ctx context.Context) (string, error) { fn, err := rs.FuncFor(key)