go-vise

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

commit a0f7ad5c808ef678e081d75934994e43a16bd8e0
parent ac4a2bac00e4d794349e6af922fa0f7bcf4c47f2
Author: lash <dev@holbrook.no>
Date:   Sun,  2 Apr 2023 23:53:21 +0100

Instructions and render for menu display

Diffstat:
MREADME.md | 22++++++++++++----------
Mgo/engine/engine.go | 33++++++++++++++++++++++-----------
Mgo/engine/engine_test.go | 22+++++++++++++++++++++-
Mgo/resource/fs.go | 1+
Mgo/resource/render.go | 1+
Mgo/resource/resource.go | 43+++++++++++++++++++++++++++++++++++++++++++
Mgo/testdata/testdata.go | 5++++-
Mgo/vm/debug.go | 14++++++++++++++
Mgo/vm/opcodes.go | 6+++++-
Mgo/vm/runner.go | 31++++++++++++++++++++++++++++++-
Mgo/vm/runner_test.go | 30++++++++++++++++++++++++++++++
Mgo/vm/vm.go | 13+++++++++++++
12 files changed, 196 insertions(+), 25 deletions(-)

diff --git a/README.md b/README.md @@ -17,8 +17,9 @@ The VM defines the following opcode symbols: * `MAP <symbol>` - Expose a code symbol previously loaded by `LOAD` to the rendering client. Roughly corresponds to the `global` directive in Python. * `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` and and move to `symbol` node on match. Any consecutive matches will be ignored until `HALT` is called. - +* `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. +* `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. ### External code @@ -128,14 +129,15 @@ It expects all replacement symbols to be available at time of rendering, and has (Minimal, WIP) ``` -0008 03666f6f 03626172 # INCMP "foo" "bar" - move to node "bar" if input is "FOO" -0001 0461696565 01 01 # CATCH "aiee" 1 1 - move to node "aiee" (and immediately halt) if input match flag (1) is not set (1) -0003 04616263 020104 # LOAD "abc" 260 - execute code symbol "abc" with a result size limit of 260 (2 byte BE integer, 0x0104) -0003 04646566 00 # LOAD "def" 0 - execute code symbol "abc" with no size limit (sink) -0005 04616263 # MAP "abc" - make "abc" available for renderer -0007 # HALT - stop execution (require new input to continue) -0006 03313233 # MOVE "123" - move to node "123" (regardless of input) -0007 # HALT - stop execution +000a 03666f6f 06746f20666f6f # MOUT "foo" "to foo" - display a menu entry for choice "foo", described by "to foo" +0008 03666f6f 03626172 # INCMP "foo" "bar" - move to node "bar" if input is "FOO" +0001 0461696565 01 01 # CATCH "aiee" 1 1 - move to node "aiee" (and immediately halt) if input match flag (1) is not set (1) +0003 04616263 020104 # LOAD "abc" 260 - execute code symbol "abc" with a result size limit of 260 (2 byte BE integer, 0x0104) +0003 04646566 00 # LOAD "def" 0 - execute code symbol "abc" with no size limit (sink) +0005 04616263 # MAP "abc" - make "abc" available for renderer +0007 # HALT - stop execution (require new input to continue) +0006 03313233 # MOVE "123" - move to node "123" (regardless of input) +0007 # HALT - stop execution ``` ## Development tools diff --git a/go/engine/engine.go b/go/engine/engine.go @@ -32,21 +32,25 @@ func NewEngine(st *state.State, rs resource.Resource) Engine { // // It makes sure bootstrapping code has been executed, and that the exposed bytecode is ready for user input. func(en *Engine) Init(sym string, ctx context.Context) error { - b := vm.NewLine([]byte{}, vm.MOVE, []string{sym}, nil, nil) + b := vm.NewLine(nil, vm.MOVE, []string{sym}, nil, nil) var err error - _, err = vm.Run(b, en.st, en.rs, ctx) + b, err = vm.Run(b, en.st, en.rs, ctx) if err != nil { return err } - location := en.st.Where() - code, err := en.rs.GetCode(location) - if err != nil { - return err - } - if len(code) == 0 { - return fmt.Errorf("no code found at resource %s", en.rs) - } - return en.st.AppendCode(code) +// location := en.st.Where() +// code, err := en.rs.GetCode(location) +// if err != nil { +// return err +// } +// if len(code) == 0 { +// return fmt.Errorf("no code found at resource %s", en.rs) +// } +// +// code, err = vm.Run(code, en.st, en.rs, ctx) +// + en.st.SetCode(b) + return nil } // Exec processes user input against the current state of the virtual machine environment. @@ -97,6 +101,13 @@ func(en *Engine) WriteResult(w io.Writer) error { if err != nil { return err } + m, err := en.rs.RenderMenu() + if err != nil { + return err + } + if len(m) > 0 { + r += "\n" + m + } c, err := io.WriteString(w, r) log.Printf("%v bytes written as result for %v", c, location) return err diff --git a/go/engine/engine_test.go b/go/engine/engine_test.go @@ -39,10 +39,16 @@ func(fs FsWrapper) one(ctx context.Context) (string, error) { return "one", nil } +func(fs FsWrapper) inky(ctx context.Context) (string, error) { + return "tinkywinky", nil +} + func(fs FsWrapper) FuncFor(sym string) (resource.EntryFunc, error) { switch sym { case "one": return fs.one, nil + case "inky": + return fs.inky, nil } return nil, fmt.Errorf("function for %v not found", sym) } @@ -93,6 +99,20 @@ func TestEngineInit(t *testing.T) { } r := st.Where() if r != "foo" { - t.Fatalf("expected where-string 'foo', got %v", r) + t.Fatalf("expected where-string 'foo', got %s", r) + } + w = bytes.NewBuffer(nil) + err = en.WriteResult(w) + if err != nil { + t.Fatal(err) + } + b = w.Bytes() + expect := `this is in foo + +it has more lines +0:to foo +1:go bar` + if !bytes.Equal(b, []byte(expect)) { + t.Fatalf("expected\n\t%s\ngot:\n\t%s\n", expect, b) } } diff --git a/go/resource/fs.go b/go/resource/fs.go @@ -9,6 +9,7 @@ import ( ) type FsResource struct { + MenuResource Path string } diff --git a/go/resource/render.go b/go/resource/render.go @@ -22,3 +22,4 @@ func DefaultRenderTemplate(r Resource, sym string, values map[string]string) (st } return b.String(), err } + diff --git a/go/resource/resource.go b/go/resource/resource.go @@ -2,6 +2,8 @@ package resource import ( "context" + "fmt" + "log" ) // EntryFunc is a function signature for retrieving value for a key @@ -11,6 +13,47 @@ 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) 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. } + +type MenuResource struct { + menu [][2]string +} + +func(m *MenuResource) PutMenu(selector string, title string) error { + m.menu = append(m.menu, [2]string{selector, title}) + log.Printf("menu %v", m.menu) + return nil +} + +func(m *MenuResource) ShiftMenu() (string, string, error) { + if len(m.menu) == 0 { + return "", "", fmt.Errorf("menu is empty") + } + r := m.menu[0] + m.menu = m.menu[1:] + return r[0], r[1], nil +} + +func(m *MenuResource) RenderMenu() (string, error) { + r := "" + for true { + l := len(r) + choice, title, err := m.ShiftMenu() + if err != nil { + //if l == 0 { // TODO: replace with EOF + // return "", err + //} + break + } + if l > 0 { + r += "\n" + } + r += fmt.Sprintf("%s:%s", choice, title) + } + return r, nil +} diff --git a/go/testdata/testdata.go b/go/testdata/testdata.go @@ -37,6 +37,7 @@ func out(sym string, b []byte, tpl string) error { func root() error { b := []byte{} + b = vm.NewLine(b, vm.HALT, nil, nil, nil) b = vm.NewLine(b, vm.INCMP, []string{"1", "foo"}, nil, nil) b = vm.NewLine(b, vm.INCMP, []string{"2", "bar"}, nil, nil) @@ -47,9 +48,11 @@ func root() error { func foo() error { b := []byte{} + b = vm.NewLine(b, vm.MOUT, []string{"0", "to foo"}, nil, nil) + b = vm.NewLine(b, vm.MOUT, []string{"1", "go bar"}, nil, nil) b = vm.NewLine(b, vm.LOAD, []string{"inky"}, []byte{20}, nil) b = vm.NewLine(b, vm.HALT, nil, nil, nil) - b = vm.NewLine(b, vm.INCMP, []string{"0", "_back"}, nil, nil) + b = vm.NewLine(b, vm.INCMP, []string{"0", "_"}, nil, nil) b = vm.NewLine(b, vm.INCMP, []string{"1", "baz"}, nil, nil) b = vm.NewLine(b, vm.CATCH, []string{"_catch"}, []byte{1}, []uint8{1}) diff --git a/go/vm/debug.go b/go/vm/debug.go @@ -83,6 +83,20 @@ func ToString(b []byte) (string, error) { if err != nil { return "", err } + case MSIZE: + r, bb, err := ParseMSize(b) + b = bb + if err != nil { + return "", err + } + s = fmt.Sprintf("%s %v", s, r) + case MOUT: + r, v, bb, err := ParseMOut(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/opcodes.go b/go/vm/opcodes.go @@ -15,7 +15,9 @@ const ( MOVE = 6 HALT = 7 INCMP = 8 - _MAX = 8 + MSIZE = 9 + MOUT = 10 + _MAX = 10 ) var ( @@ -29,5 +31,7 @@ var ( MOVE: "MOVE", HALT: "HALT", INCMP: "INCMP", + MSIZE: "MSIZE", + MOUT: "MOUT", } ) diff --git a/go/vm/runner.go b/go/vm/runner.go @@ -19,6 +19,7 @@ import ( func Run(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { running := true for running { + log.Printf("execute code %x", b) op, bb, err := opSplit(b) if err != nil { return b, err @@ -39,6 +40,10 @@ func Run(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ( b, err = RunMove(b, st, rs, ctx) case INCMP: b, err = RunInCmp(b, st, rs, ctx) + case MSIZE: + b, err = RunMSize(b, st, rs, ctx) + case MOUT: + b, err = RunMOut(b, st, rs, ctx) case HALT: b, err = RunHalt(b, st, rs, ctx) return b, err @@ -149,6 +154,12 @@ func RunMove(b []byte, st *state.State, rs resource.Resource, ctx context.Contex return b, err } st.Down(sym) + code, err := rs.GetCode(sym) + if err != nil { + return b, err + } + log.Printf("loaded additional code: %x", code) + b = append(b, code...) return b, nil } @@ -175,7 +186,12 @@ func RunInCmp(b []byte, st *state.State, rs resource.Resource, ctx context.Conte _, err = st.SetFlag(state.FLAG_INMATCH) st.Down(target) } - log.Printf("b last %v", b) + code, err := rs.GetCode(target) + if err != nil { + return b, err + } + log.Printf("loaded additional code: %x", code) + b = append(b, code...) return b, err } @@ -191,6 +207,19 @@ func RunHalt(b []byte, st *state.State, rs resource.Resource, ctx context.Contex return b, err } +// RunMSize +func RunMSize(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { + return b, nil +} + +func RunMOut(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { + choice, title, b, err := ParseMOut(b) + if err != nil { + return b, err + } + err = rs.PutMenu(choice, title) + return b, err +} // retrieve data for key func refresh(key string, rs resource.Resource, ctx context.Context) (string, error) { diff --git a/go/vm/runner_test.go b/go/vm/runner_test.go @@ -14,6 +14,7 @@ import ( var dynVal = "three" type TestResource struct { + resource.MenuResource state *state.State } @@ -287,3 +288,32 @@ func TestRunArgInvalid(t *testing.T) { t.Errorf("expected where-state _catch, got %v", r) } } + +func TestRunMenu(t *testing.T) { + st := state.NewState(5) + rs := TestResource{} + + var err error + + b := NewLine(nil, MOVE, []string{"foo"}, 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,19 @@ func ParseInCmp(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") + } + r := uint32(b[0]) + b = b[1:] + return r, b, nil +} + +func ParseMOut(b []byte) (string, string, []byte, error) { + return parseTwoSym(b) +} + func parseNoArg(b []byte) ([]byte, error) { return b, nil }