Instructions and render for menu display
This commit is contained in:
parent
ac4a2bac00
commit
a0f7ad5c80
22
README.md
22
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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
)
|
||||
|
||||
type FsResource struct {
|
||||
MenuResource
|
||||
Path string
|
||||
}
|
||||
|
||||
|
@ -22,3 +22,4 @@ func DefaultRenderTemplate(r Resource, sym string, values map[string]string) (st
|
||||
}
|
||||
return b.String(), err
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
5
go/testdata/testdata.go
vendored
5
go/testdata/testdata.go
vendored
@ -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})
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
}
|
||||
)
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
13
go/vm/vm.go
13
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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user