From 8c287b909bb4d6ed2254717b51fbb1ca24177b04 Mon Sep 17 00:00:00 2001 From: lash Date: Sun, 2 Apr 2023 12:12:06 +0100 Subject: [PATCH] WIP Factor out instruction parsing --- go/vm/opcodes.go | 23 +-- go/vm/runner.go | 205 ++++++++++++++++++++++++++ go/vm/runner_test.go | 284 ++++++++++++++++++++++++++++++++++++ go/vm/vm.go | 339 +++++++++++++++++++------------------------ go/vm/vm_test.go | 320 +++++++++------------------------------- 5 files changed, 710 insertions(+), 461 deletions(-) create mode 100644 go/vm/runner.go create mode 100644 go/vm/runner_test.go diff --git a/go/vm/opcodes.go b/go/vm/opcodes.go index 5c17818..14e1e77 100644 --- a/go/vm/opcodes.go +++ b/go/vm/opcodes.go @@ -1,10 +1,9 @@ package vm -import ( - "encoding/binary" -) const VERSION = 0 +type Opcode uint16 + // VM Opcodes const ( BACK = 0 @@ -18,21 +17,3 @@ const ( INCMP = 8 _MAX = 8 ) - -// 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 strargs { - b = append(b, uint8(len(arg))) - b = append(b, []byte(arg)...) - } - if byteargs != nil { - b = append(b, uint8(len(byteargs))) - b = append(b, byteargs...) - } - if numargs != nil { - b = append(b, numargs...) - } - return append(instructionList, b...) -} diff --git a/go/vm/runner.go b/go/vm/runner.go new file mode 100644 index 0000000..730af93 --- /dev/null +++ b/go/vm/runner.go @@ -0,0 +1,205 @@ +package vm + +import ( + "encoding/binary" + "context" + "fmt" + "log" + + "git.defalsify.org/festive/resource" + "git.defalsify.org/festive/state" +) + +//type Runner func(instruction []byte, st state.State, rs resource.Resource, ctx context.Context) (state.State, []byte, error) + +// Run extracts individual op codes and arguments and executes them. +// +// Each step may update the state. +// +// On error, the remaining instructions will be returned. State will not be rolled back. +func Run(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { + var err error + for len(instruction) > 0 { + log.Printf("instruction is now 0x%x", instruction) + op := binary.BigEndian.Uint16(instruction[:2]) + if op > _MAX { + return instruction, fmt.Errorf("opcode value %v out of range (%v)", op, _MAX) + } + switch op { + case CATCH: + instruction, err = RunCatch(instruction[2:], st, rs, ctx) + case CROAK: + instruction, err = RunCroak(instruction[2:], st, rs, ctx) + case LOAD: + instruction, err = RunLoad(instruction[2:], st, rs, ctx) + case RELOAD: + instruction, err = RunReload(instruction[2:], st, rs, ctx) + case MAP: + instruction, err = RunMap(instruction[2:], st, rs, ctx) + case MOVE: + 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: + return RunHalt(instruction[2:], st, rs, ctx) + default: + err = fmt.Errorf("Unhandled state: %v", op) + } + if err != nil { + return instruction, err + } + } + return instruction, nil +} + +// RunMap executes the MAP opcode +func RunMap(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { + head, tail, err := instructionSplit(instruction) + if err != nil { + return instruction, err + } + err = st.Map(head) + return tail, err +} + +// RunMap executes the CATCH opcode +func RunCatch(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { + head, tail, err := instructionSplit(instruction) + if err != nil { + return instruction, err + } + bitFieldSize := tail[0] + bitField := tail[1:1+bitFieldSize] + tail = tail[1+bitFieldSize:] + 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{} + } + return tail, nil +} + +// RunMap executes the CROAK opcode +func RunCroak(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { + head, tail, err := instructionSplit(instruction) + if err != nil { + return instruction, err + } + _ = head + _ = tail + st.Reset() + return []byte{}, nil +} + +// RunLoad executes the LOAD opcode +func RunLoad(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { + head, tail, err := instructionSplit(instruction) + if err != nil { + return instruction, err + } + if !st.Check(head) { + return instruction, fmt.Errorf("key %v already loaded", head) + } + sz := uint16(tail[0]) + tail = tail[1:] + + r, err := refresh(head, rs, ctx) + if err != nil { + return tail, err + } + err = st.Add(head, r, sz) + return tail, err +} + +// RunLoad executes the RELOAD opcode +func RunReload(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { + head, tail, err := instructionSplit(instruction) + if err != nil { + return instruction, err + } + r, err := refresh(head, rs, ctx) + if err != nil { + return tail, err + } + st.Update(head, r) + return tail, nil +} + +// RunLoad executes the MOVE opcode +func RunMove(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { + head, tail, err := instructionSplit(instruction) + if err != nil { + return instruction, err + } + st.Down(head) + return tail, nil +} + +// RunLoad executes the BACK opcode +func RunBack(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { + st.Up() + 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 + } + sym, tail, err := instructionSplit(tail) + 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(sym) + } + 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) + if err != nil { + return "", err + } + if fn == nil { + return "", fmt.Errorf("no retrieve function for external symbol %v", key) + } + return fn(ctx) +} diff --git a/go/vm/runner_test.go b/go/vm/runner_test.go new file mode 100644 index 0000000..4753716 --- /dev/null +++ b/go/vm/runner_test.go @@ -0,0 +1,284 @@ +package vm + +import ( + "bytes" + "context" + "fmt" + "log" + "testing" + + "git.defalsify.org/festive/resource" + "git.defalsify.org/festive/state" +) + +var dynVal = "three" + +type TestResource struct { + state *state.State +} + +func getOne(ctx context.Context) (string, error) { + return "one", nil +} + +func getTwo(ctx context.Context) (string, error) { + return "two", nil +} + +func getDyn(ctx context.Context) (string, error) { + return dynVal, nil +} + +type TestStatefulResolver struct { + state *state.State +} + +func (r *TestResource) GetTemplate(sym string) (string, error) { + switch sym { + case "foo": + return "inky pinky blinky clyde", nil + case "bar": + return "inky pinky {{.one}} blinky {{.two}} clyde", nil + case "baz": + return "inky pinky {{.baz}} blinky clyde", nil + case "three": + return "{{.one}} inky pinky {{.three}} blinky clyde {{.two}}", nil + case "_catch": + return "aiee", nil + } + panic(fmt.Sprintf("unknown symbol %s", sym)) + return "", fmt.Errorf("unknown symbol %s", sym) +} + +func (r *TestResource) RenderTemplate(sym string, values map[string]string) (string, error) { + return resource.DefaultRenderTemplate(r, sym, values) +} + +func (r *TestResource) FuncFor(sym string) (resource.EntryFunc, error) { + switch sym { + case "one": + return getOne, nil + case "two": + return getTwo, nil + case "dyn": + return getDyn, nil + case "arg": + return r.getInput, nil + } + return nil, fmt.Errorf("invalid function: '%s'", sym) +} + +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 +} + +func TestRun(t *testing.T) { + st := state.NewState(5) + rs := TestResource{} + b := []byte{0x00, MOVE, 0x03} + b = append(b, []byte("foo")...) + _, err := Run(b, &st, &rs, context.TODO()) + if err != nil { + t.Errorf("error on valid opcode: %v", err) + } + + b = []byte{0x01, 0x02} + _, err = Run(b, &st, &rs, context.TODO()) + if err == nil { + t.Errorf("no error on invalid opcode") + } +} + +func TestRunLoadRender(t *testing.T) { + st := state.NewState(5) + st.Down("barbarbar") + rs := TestResource{} + sym := "one" + ins := append([]byte{uint8(len(sym))}, []byte(sym)...) + ins = append(ins, 0x0a) + var err error + _, err = RunLoad(ins, &st, &rs, context.TODO()) + if err != nil { + t.Error(err) + } + m, err := st.Get() + if err != nil { + t.Error(err) + } + r, err := rs.RenderTemplate("foo", m) + if err != nil { + t.Error(err) + } + expect := "inky pinky blinky clyde" + if r != expect { + t.Errorf("Expected %v, got %v", []byte(expect), []byte(r)) + } + + r, err = rs.RenderTemplate("bar", m) + if err == nil { + t.Errorf("expected error for render of bar: %v" ,err) + } + + sym = "two" + ins = append([]byte{uint8(len(sym))}, []byte(sym)...) + ins = append(ins, 0) + _, err = RunLoad(ins, &st, &rs, context.TODO()) + if err != nil { + t.Error(err) + } + m, err = st.Get() + if err != nil { + t.Error(err) + } + r, err = rs.RenderTemplate("bar", m) + if err != nil { + t.Error(err) + } + expect = "inky pinky one blinky two clyde" + if r != expect { + t.Errorf("Expected %v, got %v", expect, r) + } +} + +func TestRunMultiple(t *testing.T) { + st := state.NewState(5) + rs := TestResource{} + b := []byte{} + b = NewLine(b, LOAD, []string{"one"}, nil, []uint8{0}) + b = NewLine(b, LOAD, []string{"two"}, nil, []uint8{42}) + _, err := Run(b, &st, &rs, context.TODO()) + if err != nil { + t.Error(err) + } +} + +func TestRunReload(t *testing.T) { + st := state.NewState(5) + rs := TestResource{} + b := []byte{} + b = NewLine(b, LOAD, []string{"dyn"}, nil, []uint8{0}) + b = NewLine(b, MAP, []string{"dyn"}, nil, nil) + _, err := Run(b, &st, &rs, context.TODO()) + if err != nil { + t.Error(err) + } + r, err := st.Val("dyn") + if err != nil { + t.Error(err) + } + if r != "three" { + t.Errorf("expected result 'three', got %v", r) + } + dynVal = "baz" + b = []byte{} + b = NewLine(b, RELOAD, []string{"dyn"}, nil, nil) + _, err = Run(b, &st, &rs, context.TODO()) + if err != nil { + t.Error(err) + } + r, err = st.Val("dyn") + if err != nil { + t.Error(err) + } + log.Printf("dun now %s", r) + if r != "baz" { + t.Errorf("expected result 'baz', got %v", r) + } + +} + +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) + } +} + +func TestRunArg(t *testing.T) { + st := state.NewState(5) + rs := TestResource{} + + input := []byte("bar") + _ = st.SetInput(input) + + bi := NewLine([]byte{}, INCMP, []string{"bar", "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("baz")) + + bi := NewLine([]byte{}, INCMP, []string{"bar", "aiee"}, nil, nil) + bi = NewLine(bi, INCMP, []string{"baz", "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", "baz"}, 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) + } +} diff --git a/go/vm/vm.go b/go/vm/vm.go index 2ec9b86..8df1cb3 100644 --- a/go/vm/vm.go +++ b/go/vm/vm.go @@ -2,215 +2,147 @@ package vm import ( "encoding/binary" - "context" "fmt" - "log" - - "git.defalsify.org/festive/resource" - "git.defalsify.org/festive/state" ) -//type Runner func(instruction []byte, st state.State, rs resource.Resource, ctx context.Context) (state.State, []byte, error) -func argFromBytes(input []byte) (string, []byte, error) { - if len(input) == 0 { - return "", input, fmt.Errorf("zero length input") - } - sz := input[0] - out := input[1:1+sz] - return string(out), input[1+sz:], nil +func ParseLoad(b []byte) (string, uint32, []byte, error) { + return parseSymLen(b, LOAD) } -// Run extracts individual op codes and arguments and executes them. -// -// Each step may update the state. -// -// On error, the remaining instructions will be returned. State will not be rolled back. -func Run(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { - var err error - for len(instruction) > 0 { - log.Printf("instruction is now 0x%x", instruction) - op := binary.BigEndian.Uint16(instruction[:2]) - if op > _MAX { - return instruction, fmt.Errorf("opcode value %v out of range (%v)", op, _MAX) - } - switch op { - case CATCH: - instruction, err = RunCatch(instruction[2:], st, rs, ctx) - case CROAK: - instruction, err = RunCroak(instruction[2:], st, rs, ctx) - case LOAD: - instruction, err = RunLoad(instruction[2:], st, rs, ctx) - case RELOAD: - instruction, err = RunReload(instruction[2:], st, rs, ctx) - case MAP: - instruction, err = RunMap(instruction[2:], st, rs, ctx) - case MOVE: - 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: - return RunHalt(instruction[2:], st, rs, ctx) - default: - err = fmt.Errorf("Unhandled state: %v", op) - } - if err != nil { - return instruction, err - } - } - return instruction, nil +func ParseReload(b []byte) (string, []byte, error) { + return parseSym(b, RELOAD) } -// RunMap executes the MAP opcode -func RunMap(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { - head, tail, err := instructionSplit(instruction) +func ParseMap(b []byte) (string, []byte, error) { + return parseSym(b, MAP) +} + +func ParseMove(b []byte) (string, []byte, error) { + return parseSym(b, MOVE) +} + +func ParseHalt(b []byte) ([]byte, error) { + return parseNoArg(b, HALT) +} + +func ParseCatch(b []byte) (string, uint8, []byte, error) { + return parseSymSig(b, CATCH) +} + +func ParseCroak(b []byte) (string, uint8, []byte, error) { + return parseSymSig(b, CROAK) +} + +func ParseInCmp(b []byte) (string, string, []byte, error) { + return parseTwoSym(b, INCMP) +} + +func parseNoArg(b []byte, op Opcode) ([]byte, error) { + return opCheck(b, op) +} + +func parseSym(b []byte, op Opcode) (string, []byte, error) { + b, err := opCheck(b, op) if err != nil { - return instruction, err + return "", b, err } - err = st.Map(head) - return tail, err + sym, tail, err := instructionSplit(b) + if err != nil { + return "", b, err + } + return sym, tail, nil } -// RunMap executes the CATCH opcode -func RunCatch(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { - head, tail, err := instructionSplit(instruction) +func parseTwoSym(b []byte, op Opcode) (string, string, []byte, error) { + b, err := opCheck(b, op) if err != nil { - return instruction, err + return "", "", b, err } - bitFieldSize := tail[0] - bitField := tail[1:1+bitFieldSize] - tail = tail[1+bitFieldSize:] - matchMode := tail[0] // matchmode 1 is match NOT set bit + symOne, tail, err := instructionSplit(b) + if err != nil { + return "", "", b, err + } + symTwo, tail, err := instructionSplit(tail) + if err != nil { + return "", "", tail, err + } + return symOne, symTwo, tail, nil +} + +func parseSymLen(b []byte, op Opcode) (string, uint32, []byte, error) { + b, err := opCheck(b, op) + if err != nil { + return "", 0, b, err + } + sym, tail, err := instructionSplit(b) + if err != nil { + return "", 0, b, err + } + sz, tail, err := intSplit(tail) + if err != nil { + return "", 0, b, err + } + return sym, sz, tail, nil +} + +func parseSymSig(b []byte, op Opcode) (string, uint8, []byte, error) { + b, err := opCheck(b, op) + if err != nil { + return "", 0, b, err + } + sym, tail, err := instructionSplit(b) + if err != nil { + return "", 0, b, err + } + if len(tail) == 0 { + return "", 0, b, fmt.Errorf("instruction too short") + } + n := tail[0] tail = tail[1:] - match := false - if matchMode > 0 { - if !st.GetIndex(bitField) { - match = true + return sym, n, tail, nil +} + +// NewLine creates a new instruction line for the VM. +func NewLine(instructionList []byte, instruction uint16, strargs []string, byteargs []byte, numargs []uint8) []byte { + if instructionList == nil { + instructionList = []byte{} + } + b := []byte{0x00, 0x00} + binary.BigEndian.PutUint16(b, instruction) + for _, arg := range strargs { + b = append(b, uint8(len(arg))) + b = append(b, []byte(arg)...) + } + if byteargs != nil { + b = append(b, uint8(len(byteargs))) + b = append(b, byteargs...) + } + if numargs != nil { + b = append(b, numargs...) + } + return append(instructionList, b...) +} + +func intSplit(b []byte) (uint32, []byte, error) { + l := uint8(b[0]) + sz := uint32(l) + b = b[1:] + if l > 0 { + r := []byte{0, 0, 0, 0} + c := 0 + ll := 4 - l + var i uint8 + for i = 0; i < 4; i++ { + if i >= ll { + r[i] = b[c] + c += 1 + } } - } else if st.GetIndex(bitField) { - match = true + sz = binary.BigEndian.Uint32(r) + b = b[l:] } - - if match { - log.Printf("catch at flag %v, moving to %v", bitField, head) - st.Down(head) - tail = []byte{} - } - return tail, nil -} - -// RunMap executes the CROAK opcode -func RunCroak(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { - head, tail, err := instructionSplit(instruction) - if err != nil { - return instruction, err - } - _ = head - _ = tail - st.Reset() - return []byte{}, nil -} - -// RunLoad executes the LOAD opcode -func RunLoad(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { - head, tail, err := instructionSplit(instruction) - if err != nil { - return instruction, err - } - if !st.Check(head) { - return instruction, fmt.Errorf("key %v already loaded", head) - } - sz := uint16(tail[0]) - tail = tail[1:] - - r, err := refresh(head, rs, ctx) - if err != nil { - return tail, err - } - err = st.Add(head, r, sz) - return tail, err -} - -// RunLoad executes the RELOAD opcode -func RunReload(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { - head, tail, err := instructionSplit(instruction) - if err != nil { - return instruction, err - } - r, err := refresh(head, rs, ctx) - if err != nil { - return tail, err - } - st.Update(head, r) - return tail, nil -} - -// RunLoad executes the MOVE opcode -func RunMove(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { - head, tail, err := instructionSplit(instruction) - if err != nil { - return instruction, err - } - st.Down(head) - return tail, nil -} - -// RunLoad executes the BACK opcode -func RunBack(instruction []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) { - st.Up() - 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 - } - sym, tail, err := instructionSplit(tail) - 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(sym) - } - 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) - if err != nil { - return "", err - } - if fn == nil { - return "", fmt.Errorf("no retrieve function for external symbol %v", key) - } - return fn(ctx) + return sz, b, nil } // split instruction into symbol and arguments @@ -229,3 +161,26 @@ func instructionSplit(b []byte) (string, []byte, error) { r := string(b[1:1+sz]) return r, b[1+sz:], nil } + +func opCheck(b []byte, opIn Opcode) ([]byte, error) { + op, b, err := opSplit(b) + if err != nil { + return b, err + } + if op != opIn { + return b, fmt.Errorf("not a %v instruction", op) + } + return b, nil +} + +func opSplit(b []byte) (Opcode, []byte, error) { + l := len(b) + if l < 2 { + return 0, b, fmt.Errorf("input size %v too short for opcode", l) + } + op := binary.BigEndian.Uint16(b) + if op > _MAX { + return 0, b, fmt.Errorf("invalid opcode %v", op) + } + return Opcode(op), b[2:], nil +} diff --git a/go/vm/vm_test.go b/go/vm/vm_test.go index 4753716..5b164bb 100644 --- a/go/vm/vm_test.go +++ b/go/vm/vm_test.go @@ -1,284 +1,108 @@ package vm import ( - "bytes" - "context" - "fmt" - "log" "testing" - - "git.defalsify.org/festive/resource" - "git.defalsify.org/festive/state" ) -var dynVal = "three" - -type TestResource struct { - state *state.State -} - -func getOne(ctx context.Context) (string, error) { - return "one", nil -} - -func getTwo(ctx context.Context) (string, error) { - return "two", nil -} - -func getDyn(ctx context.Context) (string, error) { - return dynVal, nil -} - -type TestStatefulResolver struct { - state *state.State -} - -func (r *TestResource) GetTemplate(sym string) (string, error) { - switch sym { - case "foo": - return "inky pinky blinky clyde", nil - case "bar": - return "inky pinky {{.one}} blinky {{.two}} clyde", nil - case "baz": - return "inky pinky {{.baz}} blinky clyde", nil - case "three": - return "{{.one}} inky pinky {{.three}} blinky clyde {{.two}}", nil - case "_catch": - return "aiee", nil - } - panic(fmt.Sprintf("unknown symbol %s", sym)) - return "", fmt.Errorf("unknown symbol %s", sym) -} - -func (r *TestResource) RenderTemplate(sym string, values map[string]string) (string, error) { - return resource.DefaultRenderTemplate(r, sym, values) -} - -func (r *TestResource) FuncFor(sym string) (resource.EntryFunc, error) { - switch sym { - case "one": - return getOne, nil - case "two": - return getTwo, nil - case "dyn": - return getDyn, nil - case "arg": - return r.getInput, nil - } - return nil, fmt.Errorf("invalid function: '%s'", sym) -} - -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 -} - -func TestRun(t *testing.T) { - st := state.NewState(5) - rs := TestResource{} - b := []byte{0x00, MOVE, 0x03} - b = append(b, []byte("foo")...) - _, err := Run(b, &st, &rs, context.TODO()) +func TestParseNoArg(t *testing.T) { + b := NewLine(nil, HALT, nil, nil, nil) + b, err := ParseHalt(b) if err != nil { - t.Errorf("error on valid opcode: %v", err) - } - - b = []byte{0x01, 0x02} - _, err = Run(b, &st, &rs, context.TODO()) - if err == nil { - t.Errorf("no error on invalid opcode") + t.Fatal(err) } } -func TestRunLoadRender(t *testing.T) { - st := state.NewState(5) - st.Down("barbarbar") - rs := TestResource{} - sym := "one" - ins := append([]byte{uint8(len(sym))}, []byte(sym)...) - ins = append(ins, 0x0a) - var err error - _, err = RunLoad(ins, &st, &rs, context.TODO()) +func TestParseSym(t *testing.T) { + b := NewLine(nil, MAP, []string{"baz"}, nil, nil) + sym, b, err := ParseMap(b) if err != nil { - t.Error(err) + t.Fatal(err) } - m, err := st.Get() - if err != nil { - t.Error(err) - } - r, err := rs.RenderTemplate("foo", m) - if err != nil { - t.Error(err) - } - expect := "inky pinky blinky clyde" - if r != expect { - t.Errorf("Expected %v, got %v", []byte(expect), []byte(r)) + if sym != "baz" { + t.Fatalf("expected sym baz, got %v", sym) } - r, err = rs.RenderTemplate("bar", m) - if err == nil { - t.Errorf("expected error for render of bar: %v" ,err) + b = NewLine(nil, RELOAD, []string{"xyzzy"}, nil, nil) + sym, b, err = ParseReload(b) + if err != nil { + t.Fatal(err) + } + if sym != "xyzzy" { + t.Fatalf("expected sym xyzzy, got %v", sym) } - sym = "two" - ins = append([]byte{uint8(len(sym))}, []byte(sym)...) - ins = append(ins, 0) - _, err = RunLoad(ins, &st, &rs, context.TODO()) + b = NewLine(nil, MOVE, []string{"plugh"}, nil, nil) + sym, b, err = ParseMove(b) if err != nil { - t.Error(err) + t.Fatal(err) } - m, err = st.Get() - if err != nil { - t.Error(err) - } - r, err = rs.RenderTemplate("bar", m) - if err != nil { - t.Error(err) - } - expect = "inky pinky one blinky two clyde" - if r != expect { - t.Errorf("Expected %v, got %v", expect, r) + if sym != "plugh" { + t.Fatalf("expected sym plugh, got %v", sym) } } -func TestRunMultiple(t *testing.T) { - st := state.NewState(5) - rs := TestResource{} - b := []byte{} - b = NewLine(b, LOAD, []string{"one"}, nil, []uint8{0}) - b = NewLine(b, LOAD, []string{"two"}, nil, []uint8{42}) - _, err := Run(b, &st, &rs, context.TODO()) +func TestParseTwoSym(t *testing.T) { + b := NewLine(nil, INCMP, []string{"foo", "bar"}, nil, nil) + one, two, b, err := ParseInCmp(b) if err != nil { - t.Error(err) + t.Fatal(err) + } + if one != "foo" { + t.Fatalf("expected symone foo, got %v", one) + } + if two != "bar" { + t.Fatalf("expected symtwo bar, got %v", two) } } -func TestRunReload(t *testing.T) { - st := state.NewState(5) - rs := TestResource{} - b := []byte{} - b = NewLine(b, LOAD, []string{"dyn"}, nil, []uint8{0}) - b = NewLine(b, MAP, []string{"dyn"}, nil, nil) - _, err := Run(b, &st, &rs, context.TODO()) +func TestParseSymSig(t *testing.T) { + b := NewLine(nil, CATCH, []string{"baz"}, nil, []uint8{0x0d}) + sym, n, b, err := ParseCatch(b) if err != nil { - t.Error(err) + t.Fatal(err) } - r, err := st.Val("dyn") - if err != nil { - t.Error(err) + if sym != "baz" { + t.Fatalf("expected sym baz, got %v", sym) } - if r != "three" { - t.Errorf("expected result 'three', got %v", r) - } - dynVal = "baz" - b = []byte{} - b = NewLine(b, RELOAD, []string{"dyn"}, nil, nil) - _, err = Run(b, &st, &rs, context.TODO()) - if err != nil { - t.Error(err) - } - r, err = st.Val("dyn") - if err != nil { - t.Error(err) - } - log.Printf("dun now %s", r) - if r != "baz" { - t.Errorf("expected result 'baz', got %v", r) - } - -} - -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 n != 13 { + t.Fatalf("expected n 13, got %v", n) } } -func TestRunArg(t *testing.T) { - st := state.NewState(5) - rs := TestResource{} - - input := []byte("bar") - _ = st.SetInput(input) - - bi := NewLine([]byte{}, INCMP, []string{"bar", "baz"}, nil, nil) - b, err := Run(bi, &st, &rs, context.TODO()) +func TestParseSymAndLen(t *testing.T) { + b := NewLine(nil, LOAD, []string{"foo"}, []byte{0x2a}, nil) + sym, n, b, err := ParseLoad(b) if err != nil { - t.Error(err) + t.Fatal(err) } - l := len(b) - if l != 0 { - t.Errorf("expected empty remainder, got length %v: %v", l, b) + if sym != "foo" { + t.Fatalf("expected sym foo, got %v", sym) } - 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("baz")) - - bi := NewLine([]byte{}, INCMP, []string{"bar", "aiee"}, nil, nil) - bi = NewLine(bi, INCMP, []string{"baz", "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", "baz"}, 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) + if n != 42 { + t.Fatalf("expected n 42, got %v", n) + } + + b = NewLine(nil, LOAD, []string{"bar"}, []byte{0x02, 0x9a}, nil) + sym, n, b, err = ParseLoad(b) + if err != nil { + t.Fatal(err) + } + if sym != "bar" { + t.Fatalf("expected sym foo, got %v", sym) + } + if n != 666 { + t.Fatalf("expected n 666, got %v", n) + } + + b = NewLine(nil, LOAD, []string{"baz"}, []byte{0x0}, nil) + sym, n, b, err = ParseLoad(b) + if err != nil { + t.Fatal(err) + } + if sym != "baz" { + t.Fatalf("expected sym foo, got %v", sym) + } + if n != 0 { + t.Fatalf("expected n 666, got %v", n) } }