Move source files to root dir

This commit is contained in:
lash
2023-04-12 18:09:37 +01:00
parent e340210d8f
commit df9b30287c
42 changed files with 0 additions and 0 deletions

167
vm/debug.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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")
}
}