Instructions and render for menu display

This commit is contained in:
lash 2023-04-02 23:53:21 +01:00
parent ac4a2bac00
commit a0f7ad5c80
Signed by untrusted user who does not match committer: lash
GPG Key ID: 21D2E7BB88C2A746
12 changed files with 196 additions and 25 deletions

View File

@ -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. * `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. * `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. * `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 ### External code
@ -128,6 +129,7 @@ It expects all replacement symbols to be available at time of rendering, and has
(Minimal, WIP) (Minimal, WIP)
``` ```
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" 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) 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 04616263 020104 # LOAD "abc" 260 - execute code symbol "abc" with a result size limit of 260 (2 byte BE integer, 0x0104)

View File

@ -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. // 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 { 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 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 { if err != nil {
return err return err
} }
location := en.st.Where() // location := en.st.Where()
code, err := en.rs.GetCode(location) // code, err := en.rs.GetCode(location)
if err != nil { // if err != nil {
return err // return err
} // }
if len(code) == 0 { // if len(code) == 0 {
return fmt.Errorf("no code found at resource %s", en.rs) // return fmt.Errorf("no code found at resource %s", en.rs)
} // }
return en.st.AppendCode(code) //
// 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. // 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 { if err != nil {
return err 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) c, err := io.WriteString(w, r)
log.Printf("%v bytes written as result for %v", c, location) log.Printf("%v bytes written as result for %v", c, location)
return err return err

View File

@ -39,10 +39,16 @@ func(fs FsWrapper) one(ctx context.Context) (string, error) {
return "one", nil return "one", nil
} }
func(fs FsWrapper) inky(ctx context.Context) (string, error) {
return "tinkywinky", nil
}
func(fs FsWrapper) FuncFor(sym string) (resource.EntryFunc, error) { func(fs FsWrapper) FuncFor(sym string) (resource.EntryFunc, error) {
switch sym { switch sym {
case "one": case "one":
return fs.one, nil return fs.one, nil
case "inky":
return fs.inky, nil
} }
return nil, fmt.Errorf("function for %v not found", sym) return nil, fmt.Errorf("function for %v not found", sym)
} }
@ -93,6 +99,20 @@ func TestEngineInit(t *testing.T) {
} }
r := st.Where() r := st.Where()
if r != "foo" { 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)
} }
} }

View File

@ -9,6 +9,7 @@ import (
) )
type FsResource struct { type FsResource struct {
MenuResource
Path string Path string
} }

View File

@ -22,3 +22,4 @@ func DefaultRenderTemplate(r Resource, sym string, values map[string]string) (st
} }
return b.String(), err return b.String(), err
} }

View File

@ -2,6 +2,8 @@ package resource
import ( import (
"context" "context"
"fmt"
"log"
) )
// EntryFunc is a function signature for retrieving value for a key // 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 { type Resource interface {
GetTemplate(sym string) (string, error) // Get the template for a given symbol. GetTemplate(sym string) (string, error) // Get the template for a given symbol.
GetCode(sym string) ([]byte, error) // Get the bytecode for the 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. 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. 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
}

View File

@ -37,6 +37,7 @@ func out(sym string, b []byte, tpl string) error {
func root() error { func root() error {
b := []byte{} 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{"1", "foo"}, nil, nil)
b = vm.NewLine(b, vm.INCMP, []string{"2", "bar"}, nil, nil) b = vm.NewLine(b, vm.INCMP, []string{"2", "bar"}, nil, nil)
@ -47,9 +48,11 @@ func root() error {
func foo() error { func foo() error {
b := []byte{} 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.LOAD, []string{"inky"}, []byte{20}, nil)
b = vm.NewLine(b, vm.HALT, nil, nil, 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.INCMP, []string{"1", "baz"}, nil, nil)
b = vm.NewLine(b, vm.CATCH, []string{"_catch"}, []byte{1}, []uint8{1}) b = vm.NewLine(b, vm.CATCH, []string{"_catch"}, []byte{1}, []uint8{1})

View File

@ -83,6 +83,20 @@ func ToString(b []byte) (string, error) {
if err != nil { if err != nil {
return "", err 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" s += "\n"
if len(b) == 0 { if len(b) == 0 {

View File

@ -15,7 +15,9 @@ const (
MOVE = 6 MOVE = 6
HALT = 7 HALT = 7
INCMP = 8 INCMP = 8
_MAX = 8 MSIZE = 9
MOUT = 10
_MAX = 10
) )
var ( var (
@ -29,5 +31,7 @@ var (
MOVE: "MOVE", MOVE: "MOVE",
HALT: "HALT", HALT: "HALT",
INCMP: "INCMP", INCMP: "INCMP",
MSIZE: "MSIZE",
MOUT: "MOUT",
} }
) )

View File

@ -19,6 +19,7 @@ import (
func Run(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { func Run(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) {
running := true running := true
for running { for running {
log.Printf("execute code %x", b)
op, bb, err := opSplit(b) op, bb, err := opSplit(b)
if err != nil { if err != nil {
return b, err 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) b, err = RunMove(b, st, rs, ctx)
case INCMP: case INCMP:
b, err = RunInCmp(b, st, rs, ctx) 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: case HALT:
b, err = RunHalt(b, st, rs, ctx) b, err = RunHalt(b, st, rs, ctx)
return b, err return b, err
@ -149,6 +154,12 @@ func RunMove(b []byte, st *state.State, rs resource.Resource, ctx context.Contex
return b, err return b, err
} }
st.Down(sym) 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 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) _, err = st.SetFlag(state.FLAG_INMATCH)
st.Down(target) 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 return b, err
} }
@ -191,6 +207,19 @@ func RunHalt(b []byte, st *state.State, rs resource.Resource, ctx context.Contex
return b, err 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 // retrieve data for key
func refresh(key string, rs resource.Resource, ctx context.Context) (string, error) { func refresh(key string, rs resource.Resource, ctx context.Context) (string, error) {

View File

@ -14,6 +14,7 @@ import (
var dynVal = "three" var dynVal = "three"
type TestResource struct { type TestResource struct {
resource.MenuResource
state *state.State state *state.State
} }
@ -287,3 +288,32 @@ func TestRunArgInvalid(t *testing.T) {
t.Errorf("expected where-state _catch, got %v", r) 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)
}
}

View File

@ -47,6 +47,19 @@ func ParseInCmp(b []byte) (string, string, []byte, error) {
return parseTwoSym(b) 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) { func parseNoArg(b []byte) ([]byte, error) {
return b, nil return b, nil
} }