Implement INCMP and check in nav match flag

This commit is contained in:
lash 2023-04-01 22:19:12 +01:00
parent 4181fe0576
commit b78e28622a
Signed by untrusted user who does not match committer: lash
GPG Key ID: 21D2E7BB88C2A746
12 changed files with 208 additions and 300 deletions

View File

@ -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

View File

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

View File

@ -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)

5
go/state/flag.go Normal file
View File

@ -0,0 +1,5 @@
package state
const (
FLAG_INMATCH = 1
)

View File

@ -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.
//// 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 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.
//
// 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 {

View File

@ -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)

1
go/testdata/bar vendored Normal file
View File

@ -0,0 +1 @@
i am in bar

BIN
go/testdata/bar.bin vendored Normal file

Binary file not shown.

BIN
go/testdata/root.bin vendored Normal file

Binary file not shown.

View File

@ -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...)
}

View File

@ -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)

View File

@ -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,202 +208,6 @@ func TestRunReload(t *testing.T) {
}
func TestRunArg(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()...)
var err error
b, err = Apply(b, rt.ToBytes(), &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 := st.Where()
if r != "xyzzy" {
t.Errorf("expected where-state baz, got %v", r)
}
}
func TestRunArgInvalid(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())
if err != nil {
t.Error(err)
}
l := len(b)
if l != 0 {
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)
}
}
func TestRunArgInstructions(t *testing.T) {
t.Skip("pending fix for separating router code from executing code")
st := state.NewState(5)
rs := TestResource{}
rt := router.NewRouter()
rt.Add("foo", "bar")
b := []byte{0x03}
b = append(b, []byte("foo")...)
bi := NewLine(rt.ToBytes(), 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)
}
}
func TestRunMoveAndBack(t *testing.T) {
t.Skip("pending fix for separating router code from executing code")
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})
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)
}
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())
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-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{}
@ -423,3 +227,74 @@ func TestHalt(t *testing.T) {
t.Fatalf("Expected MOVE instruction, found '%v'", b)
}
}
func TestRunArg(t *testing.T) {
st := state.NewState(5)
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)
}
l := len(b)
if l != 0 {
t.Errorf("expected empty remainder, got length %v: %v", l, b)
}
r := st.Where()
if r != "baz" {
t.Errorf("expected where-state baz, got %v", r)
}
}
func TestRunInputHandler(t *testing.T) {
st := state.NewState(5)
rs := TestResource{}
_ = st.SetInput([]byte("foo"))
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
_, err = Run(bi, &st, &rs, context.TODO())
if err != nil {
t.Fatal(err)
}
r := st.Where()
if r != "foo" {
t.Fatalf("expected where-sym 'foo', got '%v'", r)
}
}
func TestRunArgInvalid(t *testing.T) {
st := state.NewState(5)
rs := TestResource{}
_ = st.SetInput([]byte("foo"))
var err error
b := NewLine([]byte{}, INCMP, []string{"bar"}, nil, nil)
b = NewLine(b, CATCH, []string{"_catch"}, []byte{state.FLAG_INMATCH}, []uint8{1})
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 := st.Where()
if r != "_catch" {
t.Errorf("expected where-state _catch, got %v", r)
}
}