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:
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
}