go-vise

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

commit b78e28622ab7f15106078e1d035051e602c01ee1
parent 4181fe057697d421382cf956f78b150f86f4843b
Author: lash <dev@holbrook.no>
Date:   Sat,  1 Apr 2023 22:19:12 +0100

Implement INCMP and check in nav match flag

Diffstat:
MREADME.md | 5+++--
Mgo/engine/engine.go | 13++++++++-----
Mgo/engine/engine_test.go | 4+---
Ago/state/flag.go | 5+++++
Mgo/state/state.go | 70++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mgo/state/state_test.go | 8++++----
Ago/testdata/bar | 1+
Ago/testdata/bar.bin | 0
Ago/testdata/root.bin | 0
Mgo/vm/opcodes.go | 20++++++++++++--------
Mgo/vm/vm.go | 99+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mgo/vm/vm_test.go | 221+++++++++++++++++--------------------------------------------------------------
12 files changed, 177 insertions(+), 269 deletions(-)

diff --git a/README.md b/README.md @@ -1,6 +1,6 @@ # festive: A Constrained Size Output Virtual Machine -An attempt at defining a small VM to create a stack machine for size-constrained clients and servers. +An attempt at defining a small VM to handle menu interaction for size-constrained clients and servers. Original motivation was to create a simple templating renderer for USSD clients, combined with an agnostic data-retrieval reference that may conceal any level of complexity. @@ -16,7 +16,7 @@ The VM defines the following opcode symbols: * `RELOAD <symbol>` - Execute a code symbol already loaded by `LOAD` and cache the data, constrained to the previously given `size` for the same symbol. * `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 (typicaly, 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. ### External code @@ -48,6 +48,7 @@ Signal may be set when executing of external code symbols, and may be used as a The signal flag arguments should only set a single flag to be tested. If more than one flag is set, the first flag matched will be used as the trigger. +First 8 flags are reserved and used for internal VM operations. ## Rendering diff --git a/go/engine/engine.go b/go/engine/engine.go @@ -61,11 +61,10 @@ func(en *Engine) Init(ctx context.Context) error { // - no current bytecode is available // - input processing against bytcode failed func (en *Engine) Exec(input []byte, ctx context.Context) error { - l := uint8(len(input)) - if l > 255 { - return fmt.Errorf("input too long (%v)", l) + err := en.st.SetInput(input) + if err != nil { + return err } - input = append([]byte{l}, input...) code, err := en.st.GetCode() if err != nil { return err @@ -73,7 +72,11 @@ func (en *Engine) Exec(input []byte, ctx context.Context) error { if len(code) == 0 { return fmt.Errorf("no code to execute") } - code, err = vm.Apply(input, code, en.st, en.rs, ctx) + err = en.st.SetInput(input) + if err != nil { + return err + } + code, err = vm.Run(code, en.st, en.rs, ctx) en.st.SetCode(code) return err } diff --git a/go/engine/engine_test.go b/go/engine/engine_test.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "io/ioutil" - "log" "path" "text/template" "testing" @@ -63,7 +62,6 @@ func(fs FsWrapper) GetCode(sym string) ([]byte, error) { sym += ".bin" fp := path.Join(fs.Path, sym) r, err := ioutil.ReadFile(fp) - log.Printf("getcode for %v %v", fp, r) return r, err } @@ -87,7 +85,7 @@ func TestEngineInit(t *testing.T) { if !bytes.Equal(b, []byte("hello world")) { t.Fatalf("expected result 'hello world', got %v", b) } - input := []byte("foo") + input := []byte("bar") err = en.Exec(input, ctx) if err != nil { t.Fatal(err) diff --git a/go/state/flag.go b/go/state/flag.go @@ -0,0 +1,5 @@ +package state + +const ( + FLAG_INMATCH = 1 +) diff --git a/go/state/state.go b/go/state/state.go @@ -19,13 +19,16 @@ import ( // // Symbol keys do not count towards cache size limitations. // +// 8 first flags are reserved. +// // TODO factor out cache type State struct { Flags []byte // Error state - CacheSize uint32 // Total allowed cumulative size of values in cache - CacheUseSize uint32 // Currently used bytes by all values in cache + CacheSize uint32 // Total allowed cumulative size of values (not code) in cache + CacheUseSize uint32 // Currently used bytes by all values (not code) in cache Cache []map[string]string // All loaded cache items CacheMap map[string]string // Mapped + input []byte // Last input code []byte // Pending bytecode to execute execPath []string // Command symbols stack arg *string // Optional argument. Nil if not set. @@ -54,14 +57,14 @@ func getFlag(bitIndex uint32, bitField []byte) bool { return (b & (1 << localBitIndex)) > 0 } -// NewState creates a new State object with bitSize number of error condition states. +// NewState creates a new State object with bitSize number of error condition states in ADDITION to the 8 builtin flags. func NewState(bitSize uint32) State { st := State{ CacheSize: 0, CacheUseSize: 0, - bitSize: bitSize, + bitSize: bitSize + 8, } - byteSize := toByteSize(bitSize) + byteSize := toByteSize(bitSize + 8) if byteSize > 0 { st.Flags = make([]byte, byteSize) } else { @@ -181,26 +184,26 @@ func(st State) Where() string { return st.execPath[l-1] } -// PutArg adds the optional argument. -// -// Fails if arg already set. -func(st *State) PutArg(input string) error { - st.arg = &input - if st.arg != nil { - return fmt.Errorf("arg already set to %s", *st.arg) - } - return nil -} - -// PopArg retrieves the optional argument. Will be freed upon retrieval. +//// PutArg adds the optional argument. +//// +//// Fails if arg already set. +//func(st *State) PutArg(input string) error { +// st.arg = &input +// if st.arg != nil { +// return fmt.Errorf("arg already set to %s", *st.arg) +// } +// return nil +//} // -// Fails if arg not set (or already freed). -func(st *State) PopArg() (string, error) { - if st.arg == nil { - return "", fmt.Errorf("arg is not set") - } - return *st.arg, nil -} +//// PopArg retrieves the optional argument. Will be freed upon retrieval. +//// +//// Fails if arg not set (or already freed). +//func(st *State) PopArg() (string, error) { +// if st.arg == nil { +// return "", fmt.Errorf("arg is not set") +// } +// return *st.arg, nil +//} // Down adds the given symbol to the command stack. // @@ -391,12 +394,31 @@ func(st *State) SetCode(b []byte) { st.code = b } +// Get the remaning cached bytecode func(st *State) GetCode() ([]byte, error) { b := st.code st.code = []byte{} return b, nil } +// GetInput gets the most recent client input. +func(st *State) GetInput() ([]byte, error) { + if st.input == nil { + return nil, fmt.Errorf("no input has been set") + } + return st.input, nil +} + +// SetInput is used to record the latest client input. +func(st *State) SetInput(input []byte) error { + l := len(input) + if l > 255 { + return fmt.Errorf("input size %v too large (limit %v)", l, 255) + } + st.input = input + return nil +} + // return 0-indexed frame number where key is defined. -1 if not defined func(st *State) frameOf(key string) int { for i, m := range st.Cache { diff --git a/go/state/state_test.go b/go/state/state_test.go @@ -8,21 +8,21 @@ import ( // Check creation func TestNewState(t *testing.T) { st := NewState(5) - if len(st.Flags) != 1 { + if len(st.Flags) != 2 { t.Errorf("invalid state flag length: %v", len(st.Flags)) } st = NewState(8) - if len(st.Flags) != 1 { + if len(st.Flags) != 2 { t.Errorf("invalid state flag length: %v", len(st.Flags)) } st = NewState(17) - if len(st.Flags) != 3 { + if len(st.Flags) != 4 { t.Errorf("invalid state flag length: %v", len(st.Flags)) } } func TestStateFlags(t *testing.T) { - st := NewState(17) + st := NewState(9) v, err := st.GetFlag(2) if err != nil { t.Error(err) diff --git a/go/testdata/bar b/go/testdata/bar @@ -0,0 +1 @@ +i am in bar diff --git a/go/testdata/bar.bin b/go/testdata/bar.bin Binary files differ. diff --git a/go/testdata/root.bin b/go/testdata/root.bin Binary files differ. diff --git a/go/vm/opcodes.go b/go/vm/opcodes.go @@ -5,6 +5,7 @@ import ( ) const VERSION = 0 +// Opcodes const ( BACK = 0 CATCH = 1 @@ -14,22 +15,25 @@ const ( MAP = 5 MOVE = 6 HALT = 7 - _MAX = 7 + INCMP = 8 + //IN = 9 + _MAX = 8 ) -func NewLine(instructionList []byte, instruction uint16, args []string, post []byte, szPost []uint8) []byte { +// NewLine creates a new instruction line for the VM. +func NewLine(instructionList []byte, instruction uint16, strargs []string, byteargs []byte, numargs []uint8) []byte { b := []byte{0x00, 0x00} binary.BigEndian.PutUint16(b, instruction) - for _, arg := range args { + for _, arg := range strargs { b = append(b, uint8(len(arg))) b = append(b, []byte(arg)...) } - if post != nil { - b = append(b, uint8(len(post))) - b = append(b, post...) + if byteargs != nil { + b = append(b, uint8(len(byteargs))) + b = append(b, byteargs...) } - if szPost != nil { - b = append(b, szPost...) + if numargs != nil { + b = append(b, numargs...) } return append(instructionList, b...) } diff --git a/go/vm/vm.go b/go/vm/vm.go @@ -7,7 +7,6 @@ import ( "log" "git.defalsify.org/festive/resource" - "git.defalsify.org/festive/router" "git.defalsify.org/festive/state" ) @@ -22,52 +21,6 @@ func argFromBytes(input []byte) (string, []byte, error) { return string(out), input[1+sz:], nil } -// Apply applies input to router bytecode to resolve the node symbol to execute. -// -// The execution byte code is initialized with the appropriate MOVE -// -// If the router indicates an argument input, the optional argument is set on the state. -// -// TODO: the bytecode load is a separate step so Run should be run separately. -func Apply(input []byte, instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { - var err error - - log.Printf("running input %v against instruction %v", input, instruction) - arg, input, err := argFromBytes(input) - if err != nil { - return input, err - } - - rt := router.FromBytes(instruction) - sym := rt.Get(arg) - if sym == "" { - sym = rt.Default() - st.PutArg(arg) - } - - if sym == "" { - instruction = NewLine([]byte{}, MOVE, []string{"_catch"}, nil, nil) - } else { - instruction, err = rs.GetCode(sym) - if err != nil { - return instruction, err - } - - if sym == "_" { - instruction = NewLine([]byte{}, BACK, nil, nil, nil) - } else { - new_instruction := NewLine([]byte{}, MOVE, []string{sym}, nil, nil) - instruction = append(new_instruction, instruction...) - } - } - - instruction, err = Run(instruction, st, rs, ctx) - if err != nil { - return instruction, err - } - return instruction, nil -} - // Run extracts individual op codes and arguments and executes them. // // Each step may update the state. @@ -96,9 +49,10 @@ func Run(instruction []byte, st *state.State, rs resource.Resource, ctx context. instruction, err = RunMove(instruction[2:], st, rs, ctx) case BACK: instruction, err = RunBack(instruction[2:], st, rs, ctx) + case INCMP: + instruction, err = RunIncmp(instruction[2:], st, rs, ctx) case HALT: - log.Printf("found HALT, stopping") - return instruction[2:], err + return RunHalt(instruction[2:], st, rs, ctx) default: err = fmt.Errorf("Unhandled state: %v", op) } @@ -128,7 +82,18 @@ func RunCatch(instruction []byte, st *state.State, rs resource.Resource, ctx con bitFieldSize := tail[0] bitField := tail[1:1+bitFieldSize] tail = tail[1+bitFieldSize:] - if st.GetIndex(bitField) { + matchMode := tail[0] // matchmode 1 is match NOT set bit + tail = tail[1:] + match := false + if matchMode > 0 { + if !st.GetIndex(bitField) { + match = true + } + } else if st.GetIndex(bitField) { + match = true + } + + if match { log.Printf("catch at flag %v, moving to %v", bitField, head) st.Down(head) tail = []byte{} @@ -198,6 +163,40 @@ func RunBack(instruction []byte, st *state.State, rs resource.Resource, ctx cont return instruction, nil } +// RunIncmp executes the INCMP opcode +func RunIncmp(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { + head, tail, err := instructionSplit(instruction) + if err != nil { + return instruction, err + } + v, err := st.GetFlag(state.FLAG_INMATCH) + if err != nil { + return tail, err + } + if v { + return tail, nil + } + input, err := st.GetInput() + if err != nil { + return tail, err + } + log.Printf("checking input %v %v", input, head) + if head == string(input) { + log.Printf("input match for '%s'", input) + _, err = st.SetFlag(state.FLAG_INMATCH) + st.Down(head) + } + return tail, err +} + +// RunHalt executes the HALT opcode +func RunHalt(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { + log.Printf("found HALT, stopping") + _, err := st.ResetFlag(state.FLAG_INMATCH) + return instruction, err +} + + // retrieve data for key func refresh(key string, rs resource.Resource, ctx context.Context) (string, error) { fn, err := rs.FuncFor(key) diff --git a/go/vm/vm_test.go b/go/vm/vm_test.go @@ -9,7 +9,7 @@ import ( "text/template" "git.defalsify.org/festive/resource" - "git.defalsify.org/festive/router" +// "git.defalsify.org/festive/router" "git.defalsify.org/festive/state" ) @@ -35,11 +35,6 @@ type TestStatefulResolver struct { state *state.State } - -func (r *TestResource) getEachArg(ctx context.Context) (string, error) { - return r.state.PopArg() -} - func (r *TestResource) GetTemplate(sym string) (string, error) { switch sym { case "foo": @@ -84,12 +79,17 @@ func (r *TestResource) FuncFor(sym string) (resource.EntryFunc, error) { case "dyn": return getDyn, nil case "arg": - return r.getEachArg, nil + return r.getInput, nil } return nil, fmt.Errorf("invalid function: '%s'", sym) } -func (r *TestResource) GetCode(sym string) ([]byte, error) { +func(r *TestResource) getInput(ctx context.Context) (string, error) { + v, err := r.state.GetInput() + return string(v), err +} + +func(r *TestResource) GetCode(sym string) ([]byte, error) { return []byte{}, nil } @@ -208,40 +208,35 @@ func TestRunReload(t *testing.T) { } -func TestRunArg(t *testing.T) { +func TestHalt(t *testing.T) { st := state.NewState(5) rs := TestResource{} - rt := router.NewRouter() - rt.Add("foo", "bar") - rt.Add("baz", "xyzzy") - b := []byte{0x03} - b = append(b, []byte("baz")...) - //b = append(b, rt.ToBytes()...) + b := NewLine([]byte{}, LOAD, []string{"one"}, nil, []uint8{0}) + b = NewLine(b, HALT, nil, nil, nil) + b = NewLine(b, MOVE, []string{"foo"}, nil, nil) var err error - b, err = Apply(b, rt.ToBytes(), &st, &rs, context.TODO()) + 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) + t.Error(err) } r := st.Where() - if r != "xyzzy" { - t.Errorf("expected where-state baz, got %v", r) + if r == "foo" { + t.Fatalf("Expected where-symbol not to be 'foo'") + } + if !bytes.Equal(b[:2], []byte{0x00, MOVE}) { + t.Fatalf("Expected MOVE instruction, found '%v'", b) } } -func TestRunArgInvalid(t *testing.T) { +func TestRunArg(t *testing.T) { st := state.NewState(5) - rt := router.NewRouter() - rt.Add("foo", "bar") - rt.Add("baz", "xyzzy") - b := []byte{0x03} - b = append(b, []byte("bar")...) - //b = append(b, rt.ToBytes()...) - var err error - b, err = Apply(b, rt.ToBytes(), &st, nil, context.TODO()) + rs := TestResource{} + + input := []byte("baz") + _ = st.SetInput(input) + + bi := NewLine([]byte{}, INCMP, []string{"baz"}, nil, nil) + b, err := Run(bi, &st, &rs, context.TODO()) if err != nil { t.Error(err) } @@ -250,176 +245,56 @@ func TestRunArgInvalid(t *testing.T) { t.Errorf("expected empty remainder, got length %v: %v", l, b) } r := st.Where() - if r != "_catch" { - t.Errorf("expected where-state _catch, got %v", r) + if r != "baz" { + t.Errorf("expected where-state baz, got %v", r) } } -func TestRunArgInstructions(t *testing.T) { - t.Skip("pending fix for separating router code from executing code") +func TestRunInputHandler(t *testing.T) { st := state.NewState(5) rs := TestResource{} - rt := router.NewRouter() - rt.Add("foo", "bar") - b := []byte{0x03} - b = append(b, []byte("foo")...) + _ = st.SetInput([]byte("foo")) - bi := NewLine(rt.ToBytes(), LOAD, []string{"one"}, nil, []uint8{0}) + bi := NewLine([]byte{}, INCMP, []string{"bar"}, nil, nil) + bi = NewLine(bi, INCMP, []string{"foo"}, nil, nil) + bi = NewLine(bi, LOAD, []string{"one"}, nil, []uint8{0}) bi = NewLine(bi, LOAD, []string{"two"}, nil, []uint8{3}) bi = NewLine(bi, MAP, []string{"one"}, nil, nil) bi = NewLine(bi, MAP, []string{"two"}, nil, nil) + var err error - b, err = Apply(b, bi, &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) - } - loc := st.Where() - if loc != "bar" { - t.Errorf("expected where-state bar, got %v", loc) - } - m, err := st.Get() - if err != nil { - t.Fatal(err) - } - r, err := rs.RenderTemplate(loc, m) - if err != nil { - t.Fatal(err) //f("expected error to generate template") - } - if r != "aiee" { - t.Fatalf("expected result 'aiee', got '%v'", r) - } _, err = Run(bi, &st, &rs, context.TODO()) if err != nil { - t.Error(err) - } - m, err = st.Get() - if err != nil { t.Fatal(err) } - _, err = rs.RenderTemplate(loc, m) - if err != nil { - t.Fatal(err) + r := st.Where() + if r != "foo" { + t.Fatalf("expected where-sym 'foo', got '%v'", r) } } -func TestRunMoveAndBack(t *testing.T) { - t.Skip("pending fix for separating router code from executing code") +func TestRunArgInvalid(t *testing.T) { st := state.NewState(5) rs := TestResource{} - rt := router.NewRouter() - rt.Add("foo", "bar") - b := []byte{0x03} - b = append(b, []byte("foo")...) - //b = append(b, rt.ToBytes()...) - bi := NewLine([]byte{}, LOAD, []string{"one"}, nil, []uint8{0}) + + _ = st.SetInput([]byte("foo")) var err error - b, err = Apply(b, bi, &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) - } - rt = router.NewRouter() - rt.Add("foo", "baz") - b = []byte{0x03} - b = append(b, []byte("foo")...) - b = append(b, rt.ToBytes()...) - bi = NewLine([]byte{}, LOAD, []string{"two"}, nil, []uint8{0}) - b, err = Apply(b, bi, &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) - } + b := NewLine([]byte{}, INCMP, []string{"bar"}, nil, nil) + b = NewLine(b, CATCH, []string{"_catch"}, []byte{state.FLAG_INMATCH}, []uint8{1}) - rt = router.NewRouter() - rt.Add("foo", "_") - b = []byte{0x03} - b = append(b, []byte("foo")...) - //b = append(b, rt.ToBytes()...) - b, err = Apply(b, rt.ToBytes(), &st, &rs, context.TODO()) + b, err = Run(b, &st, &rs, context.TODO()) if err != nil { - t.Error(err) + t.Error(err) } - l = len(b) + l := len(b) if l != 0 { t.Errorf("expected empty remainder, got length %v: %v", l, b) } - loc := st.Where() - if loc != "bar" { - t.Errorf("expected where-string 'bar', got %v", loc) - } -} - -func TestCatchAndBack(t *testing.T) { - st := state.NewState(5) - rs := TestResource{} - rt := router.NewRouter() - rt.Add("foo", "bar") - b := NewLine([]byte{}, LOAD, []string{"one"}, nil, []uint8{0}) - b = NewLine(b, CATCH, []string{"bar"}, []byte{0x04}, nil) - b = NewLine(b, MOVE, []string{"foo"}, nil, nil) - _, err := Run(b, &st, &rs, context.TODO()) - if err != nil { - t.Error(err) - } - r := st.Where() - if r != "foo" { - t.Errorf("expected where-symbol 'foo', got %v", r) - } - - st.SetFlag(2) - b = NewLine([]byte{}, LOAD, []string{"two"}, nil, []uint8{0}) - b = NewLine(b, CATCH, []string{"bar"}, []byte{0x04}, nil) - b = NewLine(b, MOVE, []string{"foo"}, nil, nil) - _, err = Run(b, &st, &rs, context.TODO()) - if err != nil { - t.Error(err) - } - r = st.Where() - if r != "bar" { - t.Errorf("expected where-symbol 'bar', got %v", r) - } - - st.Up() - r = st.Where() - if r != "foo" { - t.Errorf("expected where-symbol 'foo', got %v", r) - } - err = st.Map("one") - if err != nil { - t.Error(err) - } -} - - -func TestHalt(t *testing.T) { - st := state.NewState(5) - rs := TestResource{} - b := NewLine([]byte{}, LOAD, []string{"one"}, nil, []uint8{0}) - b = NewLine(b, HALT, nil, nil, nil) - b = NewLine(b, MOVE, []string{"foo"}, nil, nil) - var err error - b, err = Run(b, &st, &rs, context.TODO()) - if err != nil { - t.Error(err) - } r := st.Where() - if r == "foo" { - t.Fatalf("Expected where-symbol not to be 'foo'") - } - if !bytes.Equal(b[:2], []byte{0x00, MOVE}) { - t.Fatalf("Expected MOVE instruction, found '%v'", b) + if r != "_catch" { + t.Errorf("expected where-state _catch, got %v", r) } }