Move source files to root dir
This commit is contained in:
167
vm/debug.go
Normal file
167
vm/debug.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
)
|
||||
|
||||
// ToString verifies all instructions in bytecode and returns an assmebly code instruction for it.
|
||||
func ToString(b []byte) (string, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
n, err := ParseAll(b, buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
log.Printf("Total %v bytes written to string buffer", n)
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// ParseAll parses and verifies all instructions from bytecode.
|
||||
//
|
||||
// If writer is not nil, the parsed instruction as assembly code line string is written to it.
|
||||
//
|
||||
// Bytecode is consumed (and written) one instruction at a time.
|
||||
//
|
||||
// It fails on any parse error encountered before the bytecode EOF is reached.
|
||||
func ParseAll(b []byte, w io.Writer) (int, error) {
|
||||
var s string
|
||||
var rs string
|
||||
var rn int
|
||||
running := true
|
||||
for running {
|
||||
op, bb, err := opSplit(b)
|
||||
b = bb
|
||||
if err != nil {
|
||||
return rn, err
|
||||
}
|
||||
s = OpcodeString[op]
|
||||
if s == "" {
|
||||
return rn, fmt.Errorf("unknown opcode: %v", op)
|
||||
}
|
||||
|
||||
switch op {
|
||||
case CATCH:
|
||||
r, n, m, bb, err := ParseCatch(b)
|
||||
b = bb
|
||||
if err == nil {
|
||||
if w != nil {
|
||||
vv := 0
|
||||
if m {
|
||||
vv = 1
|
||||
}
|
||||
if w != nil {
|
||||
//rs = fmt.Sprintf("%s %s %v %v # invertmatch=%v\n", s, r, n, vv, m)
|
||||
rs = fmt.Sprintf("%s %s %v %v\n", s, r, n, vv)
|
||||
}
|
||||
}
|
||||
}
|
||||
case CROAK:
|
||||
n, m, bb, err := ParseCroak(b)
|
||||
b = bb
|
||||
if err == nil {
|
||||
if w != nil {
|
||||
vv := 0
|
||||
if m {
|
||||
vv = 1
|
||||
}
|
||||
//rs = fmt.Sprintf("%s %v %v # invertmatch=%v\n", s, n, vv, m)
|
||||
rs = fmt.Sprintf("%s %v %v\n", s, n, vv)
|
||||
}
|
||||
}
|
||||
case LOAD:
|
||||
r, n, bb, err := ParseLoad(b)
|
||||
b = bb
|
||||
if err == nil {
|
||||
if w != nil {
|
||||
rs = fmt.Sprintf("%s %s %v\n", s, r, n)
|
||||
}
|
||||
}
|
||||
case RELOAD:
|
||||
r, bb, err := ParseReload(b)
|
||||
b = bb
|
||||
if err == nil {
|
||||
if w != nil {
|
||||
rs = fmt.Sprintf("%s %s\n", s, r)
|
||||
}
|
||||
}
|
||||
case MAP:
|
||||
r, bb, err := ParseMap(b)
|
||||
b = bb
|
||||
if err == nil {
|
||||
if w != nil {
|
||||
rs = fmt.Sprintf("%s %s\n", s, r)
|
||||
}
|
||||
}
|
||||
case MOVE:
|
||||
r, bb, err := ParseMove(b)
|
||||
b = bb
|
||||
if err == nil {
|
||||
if w != nil {
|
||||
rs = fmt.Sprintf("%s %s\n", s, r)
|
||||
}
|
||||
}
|
||||
case INCMP:
|
||||
r, v, bb, err := ParseInCmp(b)
|
||||
b = bb
|
||||
if err == nil {
|
||||
if w != nil {
|
||||
rs = fmt.Sprintf("%s %s %s\n", s, r, v)
|
||||
}
|
||||
}
|
||||
case HALT:
|
||||
b, err = ParseHalt(b)
|
||||
rs = "HALT\n"
|
||||
case MSIZE:
|
||||
r, v, bb, err := ParseMSize(b)
|
||||
b = bb
|
||||
if err == nil {
|
||||
if w != nil {
|
||||
rs = fmt.Sprintf("%s %v %v\n", s, r, v)
|
||||
}
|
||||
}
|
||||
case MOUT:
|
||||
r, v, bb, err := ParseMOut(b)
|
||||
b = bb
|
||||
if err == nil {
|
||||
if w != nil {
|
||||
rs = fmt.Sprintf("%s %s \"%s\"\n", s, r, v)
|
||||
}
|
||||
}
|
||||
case MNEXT:
|
||||
r, v, bb, err := ParseMNext(b)
|
||||
b = bb
|
||||
if err == nil {
|
||||
if w != nil {
|
||||
rs = fmt.Sprintf("%s %s \"%s\"\n", s, r, v)
|
||||
}
|
||||
}
|
||||
case MPREV:
|
||||
r, v, bb, err := ParseMPrev(b)
|
||||
b = bb
|
||||
if err == nil {
|
||||
if w != nil {
|
||||
rs = fmt.Sprintf("%s %s \"%s\"\n", s, r, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return rn, err
|
||||
}
|
||||
if w != nil {
|
||||
n, err := io.WriteString(w, rs)
|
||||
if err != nil {
|
||||
return rn, err
|
||||
}
|
||||
rn += n
|
||||
log.Printf("wrote %v bytes for instruction %v", n, s)
|
||||
}
|
||||
|
||||
//rs += "\n"
|
||||
if len(b) == 0 {
|
||||
running = false
|
||||
}
|
||||
}
|
||||
return rn, nil
|
||||
}
|
||||
169
vm/debug_test.go
Normal file
169
vm/debug_test.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
||||
func TestToString(t *testing.T) {
|
||||
var b []byte
|
||||
var r string
|
||||
var expect string
|
||||
var err error
|
||||
|
||||
b = NewLine(nil, CATCH, []string{"xyzzy"}, []byte{0x0d}, []uint8{1})
|
||||
r, err = ToString(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect = "CATCH xyzzy 13 1\n"
|
||||
if r != expect {
|
||||
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
|
||||
}
|
||||
|
||||
b = NewLine(nil, CROAK, nil, []byte{0x0d}, []uint8{1})
|
||||
r, err = ToString(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect = "CROAK 13 1\n"
|
||||
if r != expect {
|
||||
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
|
||||
}
|
||||
|
||||
b = NewLine(nil, LOAD, []string{"foo"}, []byte{0x0a}, nil)
|
||||
r, err = ToString(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect = "LOAD foo 10\n"
|
||||
if r != expect {
|
||||
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
|
||||
}
|
||||
|
||||
b = NewLine(nil, RELOAD, []string{"bar"}, nil, nil)
|
||||
r, err = ToString(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect = "RELOAD bar\n"
|
||||
if r != expect {
|
||||
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
|
||||
}
|
||||
|
||||
b = NewLine(nil, MAP, []string{"inky_pinky"}, nil, nil)
|
||||
r, err = ToString(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect = "MAP inky_pinky\n"
|
||||
if r != expect {
|
||||
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
|
||||
}
|
||||
|
||||
b = NewLine(nil, MOVE, []string{"blinky_clyde"}, nil, nil)
|
||||
r, err = ToString(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect = "MOVE blinky_clyde\n"
|
||||
if r != expect {
|
||||
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
|
||||
}
|
||||
|
||||
b = NewLine(nil, HALT, nil, nil, nil)
|
||||
r, err = ToString(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect = "HALT\n"
|
||||
if r != expect {
|
||||
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
|
||||
}
|
||||
|
||||
b = NewLine(nil, INCMP, []string{"13", "baz"}, nil, nil)
|
||||
r, err = ToString(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect = "INCMP 13 baz\n"
|
||||
if r != expect {
|
||||
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
|
||||
}
|
||||
|
||||
b = NewLine(nil, MNEXT, []string{"11", "nextmenu"}, nil, nil)
|
||||
r, err = ToString(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect = "MNEXT 11 \"nextmenu\"\n"
|
||||
if r != expect {
|
||||
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
|
||||
}
|
||||
|
||||
b = NewLine(nil, MPREV, []string{"222", "previous menu item"}, nil, nil)
|
||||
r, err = ToString(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect = "MPREV 222 \"previous menu item\"\n"
|
||||
if r != expect {
|
||||
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
|
||||
}
|
||||
|
||||
b = NewLine(nil, MOUT, []string{"1", "foo"}, nil, nil)
|
||||
r, err = ToString(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect = "MOUT 1 \"foo\"\n"
|
||||
if r != expect {
|
||||
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
|
||||
}
|
||||
|
||||
b = NewLine(nil, MSIZE, nil, nil, []uint8{0x42, 0x2a})
|
||||
r, err = ToString(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect = "MSIZE 66 42\n"
|
||||
if r != expect {
|
||||
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToStringMultiple(t *testing.T) {
|
||||
b := NewLine(nil, INCMP, []string{"1", "foo"}, nil, nil)
|
||||
b = NewLine(b, INCMP, []string{"2", "bar"}, nil, nil)
|
||||
b = NewLine(b, CATCH, []string{"aiee"}, []byte{0x02, 0x9a}, []uint8{0})
|
||||
b = NewLine(b, LOAD, []string{"inky"}, []byte{0x2a}, nil)
|
||||
b = NewLine(b, HALT, nil, nil, nil)
|
||||
r, err := ToString(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect := `INCMP 1 foo
|
||||
INCMP 2 bar
|
||||
CATCH aiee 666 0
|
||||
LOAD inky 42
|
||||
HALT
|
||||
`
|
||||
if r != expect {
|
||||
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyMultiple(t *testing.T) {
|
||||
b := NewLine(nil, INCMP, []string{"1", "foo"}, nil, nil)
|
||||
b = NewLine(b, INCMP, []string{"2", "bar"}, nil, nil)
|
||||
b = NewLine(b, CATCH, []string{"aiee"}, []byte{0x02, 0x9a}, []uint8{0})
|
||||
b = NewLine(b, LOAD, []string{"inky"}, []byte{0x2a}, nil)
|
||||
b = NewLine(b, HALT, nil, nil, nil)
|
||||
n, err := ParseAll(b, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != 0 {
|
||||
t.Fatalf("expected write count to be 0, was %v (how is that possible)", n)
|
||||
}
|
||||
}
|
||||
153
vm/input.go
Normal file
153
vm/input.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"git.defalsify.org/festive/cache"
|
||||
"git.defalsify.org/festive/state"
|
||||
)
|
||||
|
||||
var (
|
||||
inputRegexStr = "^[a-zA-Z0-9].*$"
|
||||
inputRegex = regexp.MustCompile(inputRegexStr)
|
||||
ctrlRegexStr = "^[><_^]$"
|
||||
ctrlRegex = regexp.MustCompile(ctrlRegexStr)
|
||||
symRegexStr = "^[a-zA-Z0-9][a-zA-Z0-9_]+$"
|
||||
symRegex = regexp.MustCompile(symRegexStr)
|
||||
|
||||
)
|
||||
|
||||
// CheckInput validates the given byte string as client input.
|
||||
func ValidInput(input []byte) error {
|
||||
if !inputRegex.Match(input) {
|
||||
return fmt.Errorf("Input '%s' does not match input format /%s/", input, inputRegexStr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// control characters for relative navigation.
|
||||
func validControl(input []byte) error {
|
||||
if !ctrlRegex.Match(input) {
|
||||
return fmt.Errorf("Input '%s' does not match 'control' format /%s/", input, ctrlRegexStr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckSym validates the given byte string as a node symbol.
|
||||
func ValidSym(input []byte) error {
|
||||
if !symRegex.Match(input) {
|
||||
return fmt.Errorf("Input '%s' does not match 'sym' format /%s/", input, symRegexStr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// false if target is not valid
|
||||
func valid(target []byte) bool {
|
||||
var ok bool
|
||||
if len(target) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
err := ValidSym(target)
|
||||
if err == nil {
|
||||
ok = true
|
||||
}
|
||||
|
||||
if !ok {
|
||||
err = validControl(target)
|
||||
if err == nil {
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// CheckTarget tests whether the navigation state transition is available in the current state.
|
||||
//
|
||||
// Fails if target is formally invalid, or if navigation is unavailable.
|
||||
func CheckTarget(target []byte, st *state.State) (bool, error) {
|
||||
ok := valid(target)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("invalid target: %x", target)
|
||||
}
|
||||
|
||||
switch target[0] {
|
||||
case '_':
|
||||
topOk, err := st.Top()
|
||||
if err!= nil {
|
||||
return false, err
|
||||
}
|
||||
return topOk, nil
|
||||
case '<':
|
||||
_, prevOk := st.Sides()
|
||||
return prevOk, nil
|
||||
case '>':
|
||||
nextOk, _ := st.Sides()
|
||||
return nextOk, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// route parsed target symbol to navigation state change method,
|
||||
func applyTarget(target []byte, st *state.State, ca cache.Memory, ctx context.Context) (string, uint16, error) {
|
||||
var err error
|
||||
sym, idx := st.Where()
|
||||
|
||||
ok := valid(target)
|
||||
if !ok {
|
||||
return sym, idx, fmt.Errorf("invalid input: %x", target)
|
||||
}
|
||||
|
||||
switch target[0] {
|
||||
case '_':
|
||||
sym, err = st.Up()
|
||||
if err != nil {
|
||||
return sym, idx, err
|
||||
}
|
||||
err = ca.Pop()
|
||||
if err != nil {
|
||||
return sym, idx, err
|
||||
}
|
||||
|
||||
case '>':
|
||||
idx, err = st.Next()
|
||||
if err != nil {
|
||||
return sym, idx, err
|
||||
}
|
||||
case '<':
|
||||
idx, err = st.Previous()
|
||||
if err != nil {
|
||||
return sym, idx, err
|
||||
}
|
||||
case '^':
|
||||
notTop := true
|
||||
for notTop {
|
||||
notTop, err := st.Top()
|
||||
if notTop {
|
||||
break
|
||||
}
|
||||
sym, err = st.Up()
|
||||
if err != nil {
|
||||
return sym, idx, err
|
||||
}
|
||||
err = ca.Pop()
|
||||
if err != nil {
|
||||
return sym, idx, err
|
||||
}
|
||||
}
|
||||
default:
|
||||
sym = string(target)
|
||||
err := st.Down(sym)
|
||||
if err != nil {
|
||||
return sym, idx, err
|
||||
}
|
||||
err = ca.Push()
|
||||
if err != nil {
|
||||
return sym, idx, err
|
||||
}
|
||||
idx = 0
|
||||
}
|
||||
return sym, idx, nil
|
||||
}
|
||||
58
vm/opcodes.go
Normal file
58
vm/opcodes.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package vm
|
||||
|
||||
const VERSION = 0
|
||||
|
||||
type Opcode uint16
|
||||
|
||||
// VM Opcodes
|
||||
const (
|
||||
NOOP = 0
|
||||
CATCH = 1
|
||||
CROAK = 2
|
||||
LOAD = 3
|
||||
RELOAD = 4
|
||||
MAP = 5
|
||||
MOVE = 6
|
||||
HALT = 7
|
||||
INCMP = 8
|
||||
MSIZE = 9
|
||||
MOUT = 10
|
||||
MNEXT = 11
|
||||
MPREV = 12
|
||||
_MAX = 12
|
||||
)
|
||||
|
||||
var (
|
||||
OpcodeString = map[Opcode]string{
|
||||
NOOP: "NOOP",
|
||||
CATCH: "CATCH",
|
||||
CROAK: "CROAK",
|
||||
LOAD: "LOAD",
|
||||
RELOAD: "RELOAD",
|
||||
MAP: "MAP",
|
||||
MOVE: "MOVE",
|
||||
HALT: "HALT",
|
||||
INCMP: "INCMP",
|
||||
MSIZE: "MSIZE",
|
||||
MOUT: "MOUT",
|
||||
MNEXT: "MNEXT",
|
||||
MPREV: "MPREV",
|
||||
}
|
||||
|
||||
OpcodeIndex = map[string]Opcode {
|
||||
"NOOP": NOOP,
|
||||
"CATCH": CATCH,
|
||||
"CROAK": CROAK,
|
||||
"LOAD": LOAD,
|
||||
"RELOAD": RELOAD,
|
||||
"MAP": MAP,
|
||||
"MOVE": MOVE,
|
||||
"HALT": HALT,
|
||||
"INCMP": INCMP,
|
||||
"MSIZE": MSIZE,
|
||||
"MOUT": MOUT,
|
||||
"MNEXT": MNEXT,
|
||||
"MPREV": MPREV,
|
||||
}
|
||||
|
||||
)
|
||||
434
vm/runner.go
Normal file
434
vm/runner.go
Normal file
@@ -0,0 +1,434 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"git.defalsify.org/festive/cache"
|
||||
"git.defalsify.org/festive/render"
|
||||
"git.defalsify.org/festive/resource"
|
||||
"git.defalsify.org/festive/state"
|
||||
)
|
||||
|
||||
// Vm holds sub-components mutated by the vm execution.
|
||||
type Vm struct {
|
||||
st *state.State // Navigation and error states.
|
||||
rs resource.Resource // Retrieves content, code, and templates for symbols.
|
||||
ca cache.Memory // Loaded content.
|
||||
mn *render.Menu // Menu component of page.
|
||||
sizer *render.Sizer // Apply size constraints to output.
|
||||
pg *render.Page // Render outputs with menues to size constraints.
|
||||
}
|
||||
|
||||
// NewVm creates a new Vm.
|
||||
func NewVm(st *state.State, rs resource.Resource, ca cache.Memory, sizer *render.Sizer) *Vm {
|
||||
vmi := &Vm{
|
||||
st: st,
|
||||
rs: rs,
|
||||
ca: ca,
|
||||
pg: render.NewPage(ca, rs),
|
||||
sizer: sizer,
|
||||
}
|
||||
vmi.Reset()
|
||||
return vmi
|
||||
}
|
||||
|
||||
// Reset re-initializes sub-components for output rendering.
|
||||
func(vmi *Vm) Reset() {
|
||||
vmi.mn = render.NewMenu()
|
||||
vmi.pg.Reset()
|
||||
vmi.pg = vmi.pg.WithMenu(vmi.mn)
|
||||
if vmi.sizer != nil {
|
||||
vmi.pg = vmi.pg.WithSizer(vmi.sizer)
|
||||
}
|
||||
}
|
||||
|
||||
// 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(vm *Vm) Run(b []byte, ctx context.Context) ([]byte, error) {
|
||||
running := true
|
||||
for running {
|
||||
r, err := vm.st.MatchFlag(state.FLAG_TERMINATE, false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if r {
|
||||
log.Printf("terminate set! bailing!")
|
||||
return []byte{}, nil
|
||||
}
|
||||
_, err = vm.st.SetFlag(state.FLAG_DIRTY)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
op, bb, err := opSplit(b)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
b = bb
|
||||
log.Printf("execute code %x (%s) %x", op, OpcodeString[op], b)
|
||||
log.Printf("state: %v", vm.st)
|
||||
switch op {
|
||||
case CATCH:
|
||||
b, err = vm.RunCatch(b, ctx)
|
||||
case CROAK:
|
||||
b, err = vm.RunCroak(b, ctx)
|
||||
case LOAD:
|
||||
b, err = vm.RunLoad(b, ctx)
|
||||
case RELOAD:
|
||||
b, err = vm.RunReload(b, ctx)
|
||||
case MAP:
|
||||
b, err = vm.RunMap(b, ctx)
|
||||
case MOVE:
|
||||
b, err = vm.RunMove(b, ctx)
|
||||
case INCMP:
|
||||
b, err = vm.RunInCmp(b, ctx)
|
||||
case MSIZE:
|
||||
b, err = vm.RunMSize(b, ctx)
|
||||
case MOUT:
|
||||
b, err = vm.RunMOut(b, ctx)
|
||||
case MNEXT:
|
||||
b, err = vm.RunMNext(b, ctx)
|
||||
case MPREV:
|
||||
b, err = vm.RunMPrev(b, ctx)
|
||||
case HALT:
|
||||
b, err = vm.RunHalt(b, ctx)
|
||||
return b, err
|
||||
default:
|
||||
err = fmt.Errorf("Unhandled state: %v", op)
|
||||
}
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
if len(b) == 0 {
|
||||
b, err = vm.RunDeadCheck(b, ctx)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// RunDeadCheck determines whether a state of empty bytecode should result in termination.
|
||||
//
|
||||
// If there is remaining bytecode, this method is a noop.
|
||||
//
|
||||
// If input has not been matched, a default invalid input page should be generated aswell as a possiblity of return to last screen (or exit).
|
||||
//
|
||||
// If the termination flag has been set but not yet handled, execution is allowed to terminate.
|
||||
func(vm *Vm) RunDeadCheck(b []byte, ctx context.Context) ([]byte, error) {
|
||||
if len(b) > 0 {
|
||||
return b, nil
|
||||
}
|
||||
r, err := vm.st.MatchFlag(state.FLAG_READIN, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if r {
|
||||
log.Printf("Not processing input. Setting terminate")
|
||||
_, err := vm.st.SetFlag(state.FLAG_TERMINATE)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
r, err = vm.st.MatchFlag(state.FLAG_TERMINATE, false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if r {
|
||||
log.Printf("Terminate found!!")
|
||||
return b, nil
|
||||
}
|
||||
|
||||
|
||||
log.Printf("no code remaining but not terminating")
|
||||
location, _ := vm.st.Where()
|
||||
if location == "" {
|
||||
return b, fmt.Errorf("dead runner with no current location")
|
||||
}
|
||||
b = NewLine(nil, MOVE, []string{"_catch"}, nil, nil)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// RunMap executes the MAP opcode
|
||||
func(vm *Vm) RunMap(b []byte, ctx context.Context) ([]byte, error) {
|
||||
sym, b, err := ParseMap(b)
|
||||
err = vm.pg.Map(sym)
|
||||
return b, err
|
||||
}
|
||||
|
||||
// RunMap executes the CATCH opcode
|
||||
func(vm *Vm) RunCatch(b []byte, ctx context.Context) ([]byte, error) {
|
||||
sym, sig, mode, b, err := ParseCatch(b)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
r, err := vm.st.MatchFlag(sig, mode)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
if r {
|
||||
log.Printf("catch at flag %v, moving to %v", sig, sym) //bitField, d)
|
||||
vm.st.Down(sym)
|
||||
vm.Reset()
|
||||
b = []byte{}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// RunMap executes the CROAK opcode
|
||||
func(vm *Vm) RunCroak(b []byte, ctx context.Context) ([]byte, error) {
|
||||
sig, mode, b, err := ParseCroak(b)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
r, err := vm.st.MatchFlag(sig, mode)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
if r {
|
||||
log.Printf("croak at flag %v, purging and moving to top", sig)
|
||||
vm.Reset()
|
||||
vm.st.Reset()
|
||||
vm.pg.Reset()
|
||||
vm.ca.Reset()
|
||||
b = []byte{}
|
||||
}
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
// RunLoad executes the LOAD opcode
|
||||
func(vm *Vm) RunLoad(b []byte, ctx context.Context) ([]byte, error) {
|
||||
sym, sz, b, err := ParseLoad(b)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
r, err := refresh(sym, vm.rs, ctx)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
err = vm.ca.Add(sym, r, uint16(sz))
|
||||
return b, err
|
||||
}
|
||||
|
||||
// RunLoad executes the RELOAD opcode
|
||||
func(vm *Vm) RunReload(b []byte, ctx context.Context) ([]byte, error) {
|
||||
sym, b, err := ParseReload(b)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
|
||||
r, err := refresh(sym, vm.rs, ctx)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
vm.ca.Update(sym, r)
|
||||
if vm.pg != nil {
|
||||
err := vm.pg.Map(sym)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// RunLoad executes the MOVE opcode
|
||||
func(vm *Vm) RunMove(b []byte, ctx context.Context) ([]byte, error) {
|
||||
sym, b, err := ParseMove(b)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
if sym == "_" {
|
||||
vm.st.Up()
|
||||
vm.ca.Pop()
|
||||
sym, _ = vm.st.Where()
|
||||
} else {
|
||||
vm.st.Down(sym)
|
||||
vm.ca.Push()
|
||||
}
|
||||
code, err := vm.rs.GetCode(sym)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
log.Printf("loaded additional code: %x", code)
|
||||
b = append(b, code...)
|
||||
vm.Reset()
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// RunIncmp executes the INCMP opcode
|
||||
func(vm *Vm) RunInCmp(b []byte, ctx context.Context) ([]byte, error) {
|
||||
sym, target, b, err := ParseInCmp(b)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
|
||||
change, err := vm.st.SetFlag(state.FLAG_READIN)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
have, err := vm.st.GetFlag(state.FLAG_INMATCH)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if have {
|
||||
if change {
|
||||
_, err = vm.st.ResetFlag(state.FLAG_INMATCH)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
return b, nil
|
||||
}
|
||||
}
|
||||
input, err := vm.st.GetInput()
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
log.Printf("sym is %s", sym)
|
||||
if sym == "*" {
|
||||
log.Printf("input wildcard match ('%s'), target '%s'", input, target)
|
||||
} else {
|
||||
if sym != string(input) {
|
||||
return b, nil
|
||||
}
|
||||
log.Printf("input match for '%s', target '%s'", input, target)
|
||||
}
|
||||
|
||||
_, err = vm.st.SetFlag(state.FLAG_INMATCH)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, err = vm.st.ResetFlag(state.FLAG_READIN)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
target, _, err = applyTarget([]byte(target), vm.st, vm.ca, ctx)
|
||||
_, ok := err.(*state.IndexError)
|
||||
if ok {
|
||||
_, err = vm.st.ResetFlag(state.FLAG_INMATCH)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, err = vm.st.SetFlag(state.FLAG_READIN)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b, nil
|
||||
} else if err != nil {
|
||||
return b, err
|
||||
}
|
||||
vm.Reset()
|
||||
|
||||
code, err := vm.rs.GetCode(target)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
log.Printf("loaded additional code for target '%s': %x", target, code)
|
||||
b = append(b, code...)
|
||||
return b, err
|
||||
}
|
||||
|
||||
// RunHalt executes the HALT opcode
|
||||
func(vm *Vm) RunHalt(b []byte, ctx context.Context) ([]byte, error) {
|
||||
var err error
|
||||
b, err = ParseHalt(b)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
log.Printf("found HALT, stopping")
|
||||
return b, err
|
||||
}
|
||||
|
||||
// RunMSize executes the MSIZE opcode
|
||||
func(vm *Vm) RunMSize(b []byte, ctx context.Context) ([]byte, error) {
|
||||
log.Printf("WARNING MSIZE not yet implemented")
|
||||
_, _, b, err := ParseMSize(b)
|
||||
return b, err
|
||||
}
|
||||
|
||||
// RunMOut executes the MOUT opcode
|
||||
func(vm *Vm) RunMOut(b []byte, ctx context.Context) ([]byte, error) {
|
||||
choice, title, b, err := ParseMOut(b)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
err = vm.mn.Put(choice, title)
|
||||
return b, err
|
||||
}
|
||||
|
||||
// RunMNext executes the MNEXT opcode
|
||||
func(vm *Vm) RunMNext(b []byte, ctx context.Context) ([]byte, error) {
|
||||
selector, display, b, err := ParseMNext(b)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
cfg := vm.mn.GetBrowseConfig()
|
||||
cfg.NextSelector = selector
|
||||
cfg.NextTitle = display
|
||||
cfg.NextAvailable = true
|
||||
vm.mn = vm.mn.WithBrowseConfig(cfg)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// RunMPrev executes the MPREV opcode
|
||||
func(vm *Vm) RunMPrev(b []byte, ctx context.Context) ([]byte, error) {
|
||||
selector, display, b, err := ParseMPrev(b)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
cfg := vm.mn.GetBrowseConfig()
|
||||
cfg.PreviousSelector = selector
|
||||
cfg.PreviousTitle = display
|
||||
cfg.PreviousAvailable = true
|
||||
vm.mn = vm.mn.WithBrowseConfig(cfg)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// Render wraps output rendering, and handles error when attempting to browse beyond the rendered page count.
|
||||
func(vm *Vm) Render(ctx context.Context) (string, error) {
|
||||
changed, err := vm.st.ResetFlag(state.FLAG_DIRTY)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if !changed {
|
||||
log.Printf("Render called when not dirty, please investigate.")
|
||||
}
|
||||
sym, idx := vm.st.Where()
|
||||
r, err := vm.pg.Render(sym, idx)
|
||||
var ok bool
|
||||
_, ok = err.(*render.BrowseError)
|
||||
if ok {
|
||||
vm.Reset()
|
||||
b := NewLine(nil, MOVE, []string{"_catch"}, nil, nil)
|
||||
vm.Run(b, ctx)
|
||||
sym, idx := vm.st.Where()
|
||||
r, err = vm.pg.Render(sym, idx)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// 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(key, ctx)
|
||||
}
|
||||
|
||||
411
vm/runner_test.go
Normal file
411
vm/runner_test.go
Normal file
@@ -0,0 +1,411 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"git.defalsify.org/festive/cache"
|
||||
"git.defalsify.org/festive/render"
|
||||
"git.defalsify.org/festive/resource"
|
||||
"git.defalsify.org/festive/state"
|
||||
)
|
||||
|
||||
var dynVal = "three"
|
||||
|
||||
type TestResource struct {
|
||||
resource.MenuResource
|
||||
state *state.State
|
||||
}
|
||||
|
||||
func getOne(sym string, ctx context.Context) (string, error) {
|
||||
return "one", nil
|
||||
}
|
||||
|
||||
func getTwo(sym string, ctx context.Context) (string, error) {
|
||||
return "two", nil
|
||||
}
|
||||
|
||||
func getDyn(sym string, 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 "root":
|
||||
return "root", nil
|
||||
case "_catch":
|
||||
return "aiee", nil
|
||||
}
|
||||
panic(fmt.Sprintf("unknown symbol %s", sym))
|
||||
return "", fmt.Errorf("unknown symbol %s", sym)
|
||||
}
|
||||
|
||||
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(sym string, ctx context.Context) (string, error) {
|
||||
v, err := r.state.GetInput()
|
||||
return string(v), err
|
||||
}
|
||||
|
||||
func(r TestResource) GetCode(sym string) ([]byte, error) {
|
||||
var b []byte
|
||||
if sym == "_catch" {
|
||||
b = NewLine(b, MOUT, []string{"0", "repent"}, nil, nil)
|
||||
b = NewLine(b, HALT, nil, nil, nil)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
st := state.NewState(5)
|
||||
rs := TestResource{}
|
||||
ca := cache.NewCache()
|
||||
vm := NewVm(&st, &rs, ca, nil)
|
||||
|
||||
b := NewLine(nil, MOVE, []string{"foo"}, nil, nil)
|
||||
b = NewLine(b, HALT, nil, nil, nil)
|
||||
_, err := vm.Run(b, context.TODO())
|
||||
if err != nil {
|
||||
t.Errorf("run error: %v", err)
|
||||
}
|
||||
|
||||
b = []byte{0x01, 0x02}
|
||||
_, err = vm.Run(b, context.TODO())
|
||||
if err == nil {
|
||||
t.Errorf("no error on invalid opcode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunLoadRender(t *testing.T) {
|
||||
st := state.NewState(5)
|
||||
rs := TestResource{}
|
||||
ca := cache.NewCache()
|
||||
vm := NewVm(&st, &rs, ca, nil)
|
||||
|
||||
st.Down("bar")
|
||||
|
||||
var err error
|
||||
ctx := context.TODO()
|
||||
b := NewLine(nil, LOAD, []string{"one"}, []byte{0x0a}, nil)
|
||||
b = NewLine(b, MAP, []string{"one"}, nil, nil)
|
||||
b = NewLine(b, LOAD, []string{"two"}, []byte{0x0a}, nil)
|
||||
b = NewLine(b, MAP, []string{"two"}, nil, nil)
|
||||
b = NewLine(b, HALT, nil, nil, nil)
|
||||
b, err = vm.Run(b, ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := vm.Render(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect := "inky pinky one blinky two clyde"
|
||||
if r != expect {
|
||||
t.Fatalf("Expected\n\t%s\ngot\n\t%s\n", expect, r)
|
||||
}
|
||||
|
||||
b = NewLine(nil, LOAD, []string{"two"}, []byte{0x0a}, nil)
|
||||
b = NewLine(b, MAP, []string{"two"}, nil, nil)
|
||||
b = NewLine(b, HALT, nil, nil, nil)
|
||||
b, err = vm.Run(b, ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b = NewLine(nil, MAP, []string{"one"}, nil, nil)
|
||||
b = NewLine(b, HALT, nil, nil, nil)
|
||||
_, err = vm.Run(b, ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err = vm.Render(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect = "inky pinky one blinky two clyde"
|
||||
if r != expect {
|
||||
t.Fatalf("Expected %v, got %v", expect, r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunMultiple(t *testing.T) {
|
||||
st := state.NewState(5)
|
||||
rs := TestResource{}
|
||||
ca := cache.NewCache()
|
||||
vm := NewVm(&st, &rs, ca, nil)
|
||||
|
||||
ctx := context.TODO()
|
||||
b := NewLine(nil, MOVE, []string{"test"}, nil, nil)
|
||||
b = NewLine(b, LOAD, []string{"one"}, []byte{0x00}, nil)
|
||||
b = NewLine(b, LOAD, []string{"two"}, []byte{42}, nil)
|
||||
b = NewLine(b, HALT, nil, nil, nil)
|
||||
b, err := vm.Run(b, ctx)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(b) > 0 {
|
||||
t.Errorf("expected empty code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunReload(t *testing.T) {
|
||||
st := state.NewState(5)
|
||||
rs := TestResource{}
|
||||
ca := cache.NewCache()
|
||||
szr := render.NewSizer(128)
|
||||
vm := NewVm(&st, &rs, ca, szr)
|
||||
|
||||
ctx := context.TODO()
|
||||
b := NewLine(nil, MOVE, []string{"root"}, nil, nil)
|
||||
b = NewLine(b, LOAD, []string{"dyn"}, nil, []uint8{0})
|
||||
b = NewLine(b, MAP, []string{"dyn"}, nil, nil)
|
||||
b = NewLine(b, HALT, nil, nil, nil)
|
||||
_, err := vm.Run(b, ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := vm.Render(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r != "root" {
|
||||
t.Fatalf("expected result 'root', got %v", r)
|
||||
}
|
||||
dynVal = "baz"
|
||||
b = NewLine(nil, RELOAD, []string{"dyn"}, nil, nil)
|
||||
b = NewLine(b, HALT, nil, nil, nil)
|
||||
_, err = vm.Run(b, ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHalt(t *testing.T) {
|
||||
st := state.NewState(5)
|
||||
rs := TestResource{}
|
||||
ca := cache.NewCache()
|
||||
vm := NewVm(&st, &rs, ca, nil)
|
||||
|
||||
b := NewLine(nil, MOVE, []string{"root"}, nil, nil)
|
||||
b = NewLine(b, 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 = vm.Run(b, 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{}
|
||||
ca := cache.NewCache()
|
||||
vm := NewVm(&st, &rs, ca, nil)
|
||||
|
||||
input := []byte("bar")
|
||||
_ = st.SetInput(input)
|
||||
|
||||
bi := NewLine(nil, INCMP, []string{"bar", "baz"}, nil, nil)
|
||||
bi = NewLine(bi, HALT, nil, nil, nil)
|
||||
b, err := vm.Run(bi, 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{}
|
||||
ca := cache.NewCache()
|
||||
vm := NewVm(&st, &rs, ca, nil)
|
||||
|
||||
_ = 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"}, []byte{0x00}, nil)
|
||||
bi = NewLine(bi, LOAD, []string{"two"}, []byte{0x03}, nil)
|
||||
bi = NewLine(bi, MAP, []string{"one"}, nil, nil)
|
||||
bi = NewLine(bi, MAP, []string{"two"}, nil, nil)
|
||||
bi = NewLine(bi, HALT, nil, nil, nil)
|
||||
|
||||
var err error
|
||||
_, err = vm.Run(bi, 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{}
|
||||
ca := cache.NewCache()
|
||||
vm := NewVm(&st, &rs, ca, nil)
|
||||
|
||||
_ = st.SetInput([]byte("foo"))
|
||||
|
||||
var err error
|
||||
|
||||
st.Down("root")
|
||||
b := NewLine(nil, INCMP, []string{"bar", "baz"}, nil, nil)
|
||||
|
||||
b, err = vm.Run(b, context.TODO())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, _ := st.Where()
|
||||
if r != "_catch" {
|
||||
t.Fatalf("expected where-state _catch, got %v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunMenu(t *testing.T) {
|
||||
st := state.NewState(5)
|
||||
rs := TestResource{}
|
||||
ca := cache.NewCache()
|
||||
vm := NewVm(&st, &rs, ca, nil)
|
||||
|
||||
var err error
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
b := NewLine(nil, MOVE, []string{"foo"}, nil, nil)
|
||||
b = NewLine(b, MOUT, []string{"0", "one"}, nil, nil)
|
||||
b = NewLine(b, MOUT, []string{"1", "two"}, nil, nil)
|
||||
b = NewLine(b, HALT, nil, nil, nil)
|
||||
|
||||
b, err = vm.Run(b, ctx)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
l := len(b)
|
||||
if l != 0 {
|
||||
t.Errorf("expected empty remainder, got length %v: %v", l, b)
|
||||
}
|
||||
|
||||
r, err := vm.Render(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect := "inky pinky blinky clyde\n0:one\n1:two"
|
||||
if r != expect {
|
||||
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunMenuBrowse(t *testing.T) {
|
||||
log.Printf("This test is incomplete, it must check the output of a menu browser once one is implemented. For now it only checks whether it can execute the runner endpoints for the instrucitons.")
|
||||
st := state.NewState(5)
|
||||
rs := TestResource{}
|
||||
ca := cache.NewCache()
|
||||
vm := NewVm(&st, &rs, ca, nil)
|
||||
|
||||
var err error
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
b := NewLine(nil, MOVE, []string{"foo"}, nil, nil)
|
||||
b = NewLine(b, MOUT, []string{"0", "one"}, nil, nil)
|
||||
b = NewLine(b, MOUT, []string{"1", "two"}, nil, nil)
|
||||
b = NewLine(b, HALT, nil, nil, nil)
|
||||
|
||||
b, err = vm.Run(b, ctx)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
l := len(b)
|
||||
if l != 0 {
|
||||
t.Errorf("expected empty remainder, got length %v: %v", l, b)
|
||||
}
|
||||
|
||||
r, err := vm.Render(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect := "inky pinky blinky clyde\n0:one\n1:two"
|
||||
if r != expect {
|
||||
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunReturn(t *testing.T) {
|
||||
st := state.NewState(5)
|
||||
rs := TestResource{}
|
||||
ca := cache.NewCache()
|
||||
vm := NewVm(&st, &rs, ca, nil)
|
||||
|
||||
var err error
|
||||
|
||||
st.Down("root")
|
||||
st.SetInput([]byte("0"))
|
||||
b := NewLine(nil, INCMP, []string{"0", "bar"}, nil, nil)
|
||||
b = NewLine(b, HALT, nil, nil, nil)
|
||||
b = NewLine(b, INCMP, []string{"1", "_"}, nil, nil)
|
||||
b = NewLine(b, HALT, nil, nil, nil)
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
b, err = vm.Run(b, ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
location, _ := st.Where()
|
||||
if location != "bar" {
|
||||
t.Fatalf("expected location 'bar', got '%s'", location)
|
||||
}
|
||||
st.SetInput([]byte("1"))
|
||||
b, err = vm.Run(b, ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
location, _ = st.Where()
|
||||
if location != "root" {
|
||||
t.Fatalf("expected location 'root', got '%s'", location)
|
||||
}
|
||||
}
|
||||
251
vm/vm.go
Normal file
251
vm/vm.go
Normal file
@@ -0,0 +1,251 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 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...)
|
||||
}
|
||||
|
||||
// ParseOp verifies and extracts the expected opcode portion of an instruction
|
||||
func ParseOp(b []byte) (Opcode, []byte, error) {
|
||||
op, b, err := opSplit(b)
|
||||
if err != nil {
|
||||
return NOOP, b, err
|
||||
}
|
||||
return op, b, nil
|
||||
}
|
||||
|
||||
// ParseLoad parses and extracts the expected argument portion of a LOAD instruction
|
||||
func ParseLoad(b []byte) (string, uint32, []byte, error) {
|
||||
return parseSymLen(b)
|
||||
}
|
||||
|
||||
// ParseReload parses and extracts the expected argument portion of a RELOAD instruction
|
||||
func ParseReload(b []byte) (string, []byte, error) {
|
||||
return parseSym(b)
|
||||
}
|
||||
|
||||
// ParseMap parses and extracts the expected argument portion of a MAP instruction
|
||||
func ParseMap(b []byte) (string, []byte, error) {
|
||||
return parseSym(b)
|
||||
}
|
||||
|
||||
// ParseMove parses and extracts the expected argument portion of a MOVE instruction
|
||||
func ParseMove(b []byte) (string, []byte, error) {
|
||||
return parseSym(b)
|
||||
}
|
||||
|
||||
// ParseHalt parses and extracts the expected argument portion of a HALT instruction
|
||||
func ParseHalt(b []byte) ([]byte, error) {
|
||||
return parseNoArg(b)
|
||||
}
|
||||
|
||||
// ParseCatch parses and extracts the expected argument portion of a CATCH instruction
|
||||
func ParseCatch(b []byte) (string, uint32, bool, []byte, error) {
|
||||
return parseSymSig(b)
|
||||
}
|
||||
|
||||
// ParseCroak parses and extracts the expected argument portion of a CROAK instruction
|
||||
func ParseCroak(b []byte) (uint32, bool, []byte, error) {
|
||||
return parseSig(b)
|
||||
}
|
||||
|
||||
// ParseInCmp parses and extracts the expected argument portion of a INCMP instruction
|
||||
func ParseInCmp(b []byte) (string, string, []byte, error) {
|
||||
return parseTwoSym(b)
|
||||
}
|
||||
|
||||
// ParseMPrev parses and extracts the expected argument portion of a MPREV instruction
|
||||
func ParseMPrev(b []byte) (string, string, []byte, error) {
|
||||
return parseTwoSym(b)
|
||||
}
|
||||
|
||||
// ParseMNext parses and extracts the expected argument portion of a MNEXT instruction
|
||||
func ParseMNext(b []byte) (string, string, []byte, error) {
|
||||
return parseTwoSym(b)
|
||||
}
|
||||
|
||||
// ParseMSize parses and extracts the expected argument portion of a MSIZE instruction
|
||||
func ParseMSize(b []byte) (uint32, uint32, []byte, error) {
|
||||
if len(b) < 2 {
|
||||
return 0, 0, b, fmt.Errorf("argument too short")
|
||||
}
|
||||
r := uint32(b[0])
|
||||
rr := uint32(b[1])
|
||||
b = b[2:]
|
||||
return r, rr, b, nil
|
||||
}
|
||||
|
||||
// ParseMOut parses and extracts the expected argument portion of a MOUT instruction
|
||||
func ParseMOut(b []byte) (string, string, []byte, error) {
|
||||
return parseTwoSym(b)
|
||||
}
|
||||
|
||||
// noop
|
||||
func parseNoArg(b []byte) ([]byte, error) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// parse and extract two length-prefixed string values
|
||||
func parseSym(b []byte) (string, []byte, error) {
|
||||
sym, b, err := instructionSplit(b)
|
||||
if err != nil {
|
||||
return "", b, err
|
||||
}
|
||||
return sym, b, nil
|
||||
}
|
||||
|
||||
// parse and extract two length-prefixed string values
|
||||
func parseTwoSym(b []byte) (string, string, []byte, error) {
|
||||
symOne, b, err := instructionSplit(b)
|
||||
if err != nil {
|
||||
return "", "", b, err
|
||||
}
|
||||
symTwo, b, err := instructionSplit(b)
|
||||
if err != nil {
|
||||
return "", "", b, err
|
||||
}
|
||||
return symOne, symTwo, b, nil
|
||||
}
|
||||
|
||||
// parse and extract one length-prefixed string value, and one length-prefixed integer value
|
||||
func parseSymLen(b []byte) (string, uint32, []byte, error) {
|
||||
sym, b, err := instructionSplit(b)
|
||||
if err != nil {
|
||||
return "", 0, b, err
|
||||
}
|
||||
sz, b, err := intSplit(b)
|
||||
if err != nil {
|
||||
return "", 0, b, err
|
||||
}
|
||||
return sym, sz, b, nil
|
||||
}
|
||||
|
||||
// parse and extract one length-prefixed string value, and one single byte of integer
|
||||
func parseSymSig(b []byte) (string, uint32, bool, []byte, error) {
|
||||
sym, b, err := instructionSplit(b)
|
||||
if err != nil {
|
||||
return "", 0, false, b, err
|
||||
}
|
||||
sig, b, err := intSplit(b)
|
||||
if err != nil {
|
||||
return "", 0, false, b, err
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return "", 0, false, b, fmt.Errorf("instruction too short")
|
||||
}
|
||||
matchmode := b[0] > 0
|
||||
b = b[1:]
|
||||
|
||||
return sym, sig, matchmode, b, nil
|
||||
}
|
||||
|
||||
// parse and extract one single byte of integer
|
||||
func parseSig(b []byte) (uint32, bool, []byte, error) {
|
||||
sig, b, err := intSplit(b)
|
||||
if err != nil {
|
||||
return 0, false, b, err
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return 0, false, b, fmt.Errorf("instruction too short")
|
||||
}
|
||||
matchmode := b[0] > 0
|
||||
b = b[1:]
|
||||
|
||||
return sig, matchmode, b, nil
|
||||
}
|
||||
|
||||
|
||||
// split bytecode into head and b using length-prefixed bitfield
|
||||
func byteSplit(b []byte) ([]byte, []byte, error) {
|
||||
bitFieldSize := b[0]
|
||||
bitField := b[1:1+bitFieldSize]
|
||||
b = b[1+bitFieldSize:]
|
||||
return bitField, b, nil
|
||||
}
|
||||
|
||||
// split bytecode into head and b using length-prefixed integer
|
||||
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
|
||||
}
|
||||
}
|
||||
sz = binary.BigEndian.Uint32(r)
|
||||
b = b[l:]
|
||||
}
|
||||
return sz, b, nil
|
||||
}
|
||||
|
||||
// split bytecode into head and b using length-prefixed string
|
||||
func instructionSplit(b []byte) (string, []byte, error) {
|
||||
if len(b) == 0 {
|
||||
return "", nil, fmt.Errorf("argument is empty")
|
||||
}
|
||||
sz := uint8(b[0])
|
||||
if sz == 0 {
|
||||
return "", nil, fmt.Errorf("zero-length argument")
|
||||
}
|
||||
bSz := uint8(len(b))
|
||||
if bSz < sz {
|
||||
return "", nil, fmt.Errorf("corrupt instruction, len %v less than symbol length: %v", bSz, sz)
|
||||
}
|
||||
r := string(b[1:1+sz])
|
||||
return r, b[1+sz:], nil
|
||||
}
|
||||
|
||||
// check if the start of the given bytecode contains a valid opcode, extract and return it
|
||||
func opCheck(b []byte, opIn Opcode) ([]byte, error) {
|
||||
var bb []byte
|
||||
op, bb, err := opSplit(b)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
b = bb
|
||||
if op != opIn {
|
||||
return b, fmt.Errorf("not a %v instruction", op)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// split bytecode into head and b using opcode
|
||||
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
|
||||
}
|
||||
162
vm/vm_test.go
Normal file
162
vm/vm_test.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseNoArg(t *testing.T) {
|
||||
b := NewLine(nil, HALT, nil, nil, nil)
|
||||
_, b, _ = opSplit(b)
|
||||
b, err := ParseHalt(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(b) > 0 {
|
||||
t.Fatalf("expected empty code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSym(t *testing.T) {
|
||||
b := NewLine(nil, MAP, []string{"baz"}, nil, nil)
|
||||
_, b, _ = opSplit(b)
|
||||
sym, b, err := ParseMap(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sym != "baz" {
|
||||
t.Fatalf("expected sym baz, got %v", sym)
|
||||
}
|
||||
if len(b) > 0 {
|
||||
t.Fatalf("expected empty code")
|
||||
}
|
||||
|
||||
b = NewLine(nil, RELOAD, []string{"xyzzy"}, nil, nil)
|
||||
_, b, _ = opSplit(b)
|
||||
sym, b, err = ParseReload(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sym != "xyzzy" {
|
||||
t.Fatalf("expected sym xyzzy, got %v", sym)
|
||||
}
|
||||
if len(b) > 0 {
|
||||
t.Fatalf("expected empty code")
|
||||
}
|
||||
|
||||
b = NewLine(nil, MOVE, []string{"plugh"}, nil, nil)
|
||||
_, b, _ = opSplit(b)
|
||||
sym, b, err = ParseMove(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sym != "plugh" {
|
||||
t.Fatalf("expected sym plugh, got %v", sym)
|
||||
}
|
||||
if len(b) > 0 {
|
||||
t.Fatalf("expected empty code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTwoSym(t *testing.T) {
|
||||
b := NewLine(nil, INCMP, []string{"foo", "bar"}, nil, nil)
|
||||
_, b, _ = opSplit(b)
|
||||
one, two, b, err := ParseInCmp(b)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
if len(b) > 0 {
|
||||
t.Fatalf("expected empty code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSig(t *testing.T) {
|
||||
b := NewLine(nil, CROAK, nil, []byte{0x0b, 0x13}, []uint8{0x04})
|
||||
_, b, _ = opSplit(b)
|
||||
n, m, b, err := ParseCroak(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != 2835 {
|
||||
t.Fatalf("expected n 13, got %v", n)
|
||||
}
|
||||
if !m {
|
||||
t.Fatalf("expected m true")
|
||||
}
|
||||
if len(b) > 0 {
|
||||
t.Fatalf("expected empty code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSymSig(t *testing.T) {
|
||||
b := NewLine(nil, CATCH, []string{"baz"}, []byte{0x0a, 0x13}, []uint8{0x01})
|
||||
_, b, _ = opSplit(b)
|
||||
sym, n, m, b, err := ParseCatch(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sym != "baz" {
|
||||
t.Fatalf("expected sym baz, got %v", sym)
|
||||
}
|
||||
if n != 2579 {
|
||||
t.Fatalf("expected n 13, got %v", n)
|
||||
}
|
||||
if !m {
|
||||
t.Fatalf("expected m true")
|
||||
}
|
||||
if len(b) > 0 {
|
||||
t.Fatalf("expected empty code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSymAndLen(t *testing.T) {
|
||||
b := NewLine(nil, LOAD, []string{"foo"}, []byte{0x2a}, nil)
|
||||
_, b, _ = opSplit(b)
|
||||
sym, n, b, err := ParseLoad(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sym != "foo" {
|
||||
t.Fatalf("expected sym foo, got %v", sym)
|
||||
}
|
||||
if n != 42 {
|
||||
t.Fatalf("expected n 42, got %v", n)
|
||||
}
|
||||
|
||||
b = NewLine(nil, LOAD, []string{"bar"}, []byte{0x02, 0x9a}, nil)
|
||||
_, b, _ = opSplit(b)
|
||||
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)
|
||||
}
|
||||
if len(b) > 0 {
|
||||
t.Fatalf("expected empty code")
|
||||
}
|
||||
|
||||
b = NewLine(nil, LOAD, []string{"baz"}, []byte{0x0}, nil)
|
||||
_, b, _ = opSplit(b)
|
||||
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)
|
||||
}
|
||||
if len(b) > 0 {
|
||||
t.Fatalf("expected empty code")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user