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