go-vise

Constrained Size Output Virtual Machine
Info | Log | Files | Refs | README | LICENSE

commit 06938a96282296d02ed1c6d38b4c863d4a89ff06
parent 856bbdeb6317e8c28b46b828f2357c50f5d0f785
Author: lash <dev@holbrook.no>
Date:   Mon,  3 Apr 2023 09:11:44 +0100

Add menu browser choices handling

Diffstat:
MREADME.md | 34+++++++++++++++++++++++++++++++++-
Mgo/resource/resource.go | 20+++++++++++++++-----
Mgo/vm/debug.go | 14++++++++++++++
Mgo/vm/debug_test.go | 40++++++++++++++++++++++++++++++++++++++++
Mgo/vm/opcodes.go | 6+++++-
Mgo/vm/runner.go | 40+++++++++++++++++++++++++---------------
Mgo/vm/runner_test.go | 33+++++++++++++++++++++++++++++++++
Mgo/vm/vm.go | 8++++++++
8 files changed, 173 insertions(+), 22 deletions(-)

diff --git a/README.md b/README.md @@ -18,8 +18,10 @@ The VM defines the following opcode symbols: * `MOVE <symbol>` - Create a new execution frame, invalidating all previous `MAP` calls. More detailed: After a `MOVE` call, a `BACK` call will return to the same execution frame, with the same symbols available, but all `MAP` calls will have to be repeated. * `HALT` - Stop execution. The remaining bytecode (typically, the routing code for the node) is returned to the invoking function. * `INCMP <arg> <symbol>` - Compare registered input to `arg`. If match, it has the same side-effects as `MOVE`. In addition, any consecutive `INCMP` matches will be ignored until `HALT` is called. -* `MSIZE <num>` - Set max display size of menu part to `num` bytes. +* `MSIZE <max> <min>` - Set min and max display size of menu part to `num` bytes. * `MOUT <choice> <display>` - Add menu display entry. Each entry should have a matching `INCMP` whose `arg` matches `choice`. `display` is a descriptive text of the menu item. +* `MSEP <choice> <display> <direction>` - Define how to display a menu separator, when browsing menus that are too long. Direction `0` is forward, `>0` is backward. If a `>0` value is not defined, no previous step will be available. + ### External code @@ -53,6 +55,25 @@ The signal flag arguments should only set a single flag to be tested. If more th First 8 flags are reserved and used for internal VM operations. +### Avoid duplicate menu items + +The vm execution should overwrite duplicate `MOUT` directives with the last definition between `HALT` instructions. + +The assembler should detect duplicate `INCMP` and `MOUT` (or menu batch code) selectors, and fail to compile. `MSEP` should be included in duplication detection. + + +## Menus + +A menu has both a display and a input processing part. They are on either side of a `HALT` instruction. + +To assist with menu creation, a few batch operation symbols have been made available for use with the assembly language. + +* `DOWN <choice> <display> <symbol>` descend to next frame +* `UP <choice> <display>` return to the previous frame +* `NEXT <choice> <display>` include pagination advance +* `PREVIOUS <choice> <display>` include pagination return. If `NEXT` has not been defined this will not be rendered. + + ## Rendering The fixed-size output is generated using a templating language, and a combination of one or more _max size_ properties, and an optional _sink_ property that will attempt to consume all remaining capacity of the rendered template. @@ -68,6 +89,17 @@ For example, in this example The renderer may use up to `256 - 120 - 5 - 12 = 119` bytes from the _sink_ when rendering the output. +### Menu rendering + +The menu is appended to the template output. + +A max size can be set for the menu, which will count towards the space available for the _template sink_. + +Menus too long for a single screen should be browseable through separate screens. How the browse choice is displayed is defined using the `MSEP` definition. The browse choice counts towards the menu size capacity. + +When browsing additional menu pages, the template output should not be included. + + ### Multipage support Multipage outputs, like listings, are handled using the _sink_ output constraints: diff --git a/go/resource/resource.go b/go/resource/resource.go @@ -13,8 +13,9 @@ type EntryFunc func(ctx context.Context) (string, error) type Resource interface { GetTemplate(sym string) (string, error) // Get the template for a given symbol. GetCode(sym string) ([]byte, error) // Get the bytecode for the given symbol. - PutMenu(string, string) error // Add a menu item - ShiftMenu() (string, string, error) + PutMenu(string, string) error // Add a menu item. + ShiftMenu() (string, string, error) // Remove and return the first menu item in list. + SetMenuBrowse(string, string, bool) error // Set menu browser display details. RenderTemplate(sym string, values map[string]string) (string, error) // Render the given data map using the template of the symbol. RenderMenu() (string, error) FuncFor(sym string) (EntryFunc, error) // Resolve symbol code point for. @@ -22,6 +23,18 @@ type Resource interface { type MenuResource struct { menu [][2]string + next [2]string + prev [2]string +} + +func(m *MenuResource) SetMenuBrowse(selector string, title string, back bool) error { + entry := [2]string{selector, title} + if back { + m.prev = entry + } else { + m.next = entry + } + return nil } func(m *MenuResource) PutMenu(selector string, title string) error { @@ -45,9 +58,6 @@ func(m *MenuResource) RenderMenu() (string, error) { l := len(r) choice, title, err := m.ShiftMenu() if err != nil { - //if l == 0 { // TODO: replace with EOF - // return "", err - //} break } if l > 0 { diff --git a/go/vm/debug.go b/go/vm/debug.go @@ -97,6 +97,20 @@ func ToString(b []byte) (string, error) { return "", err } s = fmt.Sprintf("%s %s \"%s\"", s, r, v) + case MNEXT: + r, v, bb, err := ParseMNext(b) + b = bb + if err != nil { + return "", err + } + s = fmt.Sprintf("%s %s \"%s\"", s, r, v) + case MPREV: + r, v, bb, err := ParseMPrev(b) + b = bb + if err != nil { + return "", err + } + s = fmt.Sprintf("%s %s \"%s\"", s, r, v) } s += "\n" if len(b) == 0 { diff --git a/go/vm/debug_test.go b/go/vm/debug_test.go @@ -90,6 +90,46 @@ func TestToString(t *testing.T) { if r != expect { 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 { + t.Fatal(err) + } + expect = "MOUT 1 \"foo\"\n" + if r != expect { + t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r) + } + + b = NewLine(nil, MSIZE, nil, nil, []uint8{0x42}) + r, err = ToString(b) + if err != nil { + t.Fatal(err) + } + expect = "MSIZE 66\n" + if r != expect { + t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r) + } } func TestToStringMultiple(t *testing.T) { 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,5 +35,7 @@ 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 @@ -105,15 +109,6 @@ func RunCroak(b []byte, st *state.State, rs resource.Resource, ctx context.Conte // RunLoad executes the LOAD opcode func RunLoad(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { -// head, tail, err := instructionSplit(b) -// if err != nil { -// return b, err -// } -// if !st.Check(head) { -// return b, fmt.Errorf("key %v already loaded", head) -// } -// sz := uint16(tail[0]) -// tail = tail[1:] sym, sz, b, err := ParseLoad(b) if err != nil { return b, err @@ -129,10 +124,6 @@ func RunLoad(b []byte, st *state.State, rs resource.Resource, ctx context.Contex // RunLoad executes the RELOAD opcode func RunReload(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { -// head, tail, err := instructionSplit(b) -// if err != nil { -// return b, err -// } sym, b, err := ParseReload(b) if err != nil { return b, err @@ -149,7 +140,6 @@ func RunReload(b []byte, st *state.State, rs resource.Resource, ctx context.Cont // RunLoad executes the MOVE opcode func RunMove(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { sym, b, err := ParseMove(b) -// head, tail, err := instructionSplit(b) if err != nil { return b, err } @@ -165,7 +155,6 @@ func RunMove(b []byte, st *state.State, rs resource.Resource, ctx context.Contex // RunIncmp executes the INCMP opcode func RunInCmp(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { - //head, tail, err := instructionSplit(b) sym, target, b, err := ParseInCmp(b) if err != nil { return b, err @@ -209,9 +198,30 @@ func RunHalt(b []byte, st *state.State, rs resource.Resource, ctx context.Contex // RunMSize func RunMSize(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { + log.Printf("WARNING MSIZE not yet implemented") return b, nil } +// RunMSize +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 +} + +// RunMSize +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 +} + func RunMOut(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { choice, title, b, err := ParseMOut(b) if err != nil { diff --git a/go/vm/runner_test.go b/go/vm/runner_test.go @@ -317,3 +317,36 @@ func TestRunMenu(t *testing.T) { t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) } } + + +func TestRunMenuBrowse(t *testing.T) { + log.Printf("This test is incomplete, it must check the output of a menu browser once one is implemented. For now it only checks whether it can execute the runner endpoints for the instrucitons.") + st := state.NewState(5) + rs := TestResource{} + + var err error + + b := NewLine(nil, MOVE, []string{"foo"}, nil, nil) + b = NewLine(b, MNEXT, []string{"11", "two"}, nil, nil) + b = NewLine(b, MPREV, []string{"22", "two"}, nil, nil) + b = NewLine(b, MOUT, []string{"0", "one"}, nil, nil) + b = NewLine(b, MOUT, []string{"1", "two"}, nil, nil) + + b, err = Run(b, &st, &rs, context.TODO()) + if err != nil { + t.Error(err) + } + l := len(b) + if l != 0 { + t.Errorf("expected empty remainder, got length %v: %v", l, b) + } + + r, err := rs.RenderMenu() + if err != nil { + t.Fatal(err) + } + expect := "0:one\n1:two" + if r != expect { + t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) + } +} diff --git a/go/vm/vm.go b/go/vm/vm.go @@ -47,6 +47,14 @@ func ParseInCmp(b []byte) (string, string, []byte, error) { return parseTwoSym(b) } +func ParseMPrev(b []byte) (string, string, []byte, error) { + return parseTwoSym(b) +} + +func ParseMNext(b []byte) (string, string, []byte, error) { + return parseTwoSym(b) +} + func ParseMSize(b []byte) (uint32, []byte, error) { if len(b) < 1 { return 0, b, fmt.Errorf("zero-length argument")