commit 9e4205e6e8e994c762042bcfe42826e0b4bd7a48
parent 8b1f91e8599c08b83911adcc278d0d19a8f42fba
Author: lash <dev@holbrook.no>
Date: Sat, 8 Apr 2023 09:31:32 +0100
Reinstatate MNEXT, MPREV
Diffstat:
9 files changed, 144 insertions(+), 26 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
}
-// 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()
+// false if target is not valid
+func valid(target []byte) bool {
+ var ok bool
+ if len(target) == 0 {
+ return false
+ }
- err = CheckInput(target)
+ err := ValidSym(target)
if err == nil {
- valid = true
+ ok = true
}
- if !valid {
- err = CheckSym(target)
+ if !ok {
+ err = validControl(target)
if err == nil {
- valid = true
+ ok = true
}
}
+ return ok
+}
- if !valid {
- err = checkControl(target)
- if err == nil {
- valid = true
+// 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
+ sym, idx := st.Where()
+
+ 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
@@ -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
@@ -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)