go-vise

Constrained Size Output Virtual Machine
Info | Log | Files | Refs | README | LICENSE

commit bf1d6344744b012cfa95512d80fa17cf8c9f8441
parent 957d59bb1a33310f9cbe83cb650ec15c16c1300e
Author: lash <dev@holbrook.no>
Date:   Sun, 16 Apr 2023 10:40:41 +0100

Add engine and state restart on empty termination node

Diffstat:
MREADME.md | 31++++++++++++++++++++++++-------
Mengine/engine.go | 50++++++++++++++++++++++++++++++++++++++++++--------
Mengine/engine_test.go | 43++++++++++++++++++++++++++++++++++++++++++-
Mstate/flag.go | 2--
Mstate/state.go | 14+++++++++++++-
Mvm/runner.go | 2--
6 files changed, 121 insertions(+), 21 deletions(-)

diff --git a/README.md b/README.md @@ -56,6 +56,8 @@ The signal flag arguments should only set a single flag to be tested. If more th First 8 flags are reserved and used for internal VM operations. +When a signal is caught, the *bytecode buffer is flushed* before the target symbol code is loaded. + ### Avoid duplicate menu items @@ -144,17 +146,17 @@ Currently the following rules apply for encoding in version `0`: This repository provides a `golang` reference implementation for the `vise` concept. -In this reference implementation some constraints apply - ### Structure -- `vm`: Defines instructions, and applies transformations according to the instructions. -- `state`: Holds the bytecode buffer, error states and navigation states. +- `asm`: Assembly parser and compiler. - `cache`: Holds and manages all loaded content. -- `resource`: Retrieves data and bytecode from external symbols, and retrieves templates. -- `render`: Renders menu and templates, and enforces output size constraints. - `engine`: Outermost interface. Orchestrates execution of bytecode against input. +- `persist`: Interface and reference implementation of `state` and `cache` persistence across asynchronous vm executions. +- `render`: Renders menu and templates, and enforces output size constraints. +- `resource`: Retrieves data and bytecode from external symbols, and retrieves templates. +- `state`: Holds the bytecode buffer, error states and navigation states. +- `vm`: Defines instructions, and applies transformations according to the instructions. ### Template rendering @@ -164,6 +166,21 @@ Template rendering is done using the `text/template` faciilty in the `golang` st It expects all replacement symbols to be available at time of rendering, and has no tolerance for missing ones. +### Runtime engine + +The runtime engine: + +* Validates client input +* Runs VM with client input +* Renders result +* Restarts execution from top if the vm has nothing more to do. + +There are two flavors of the engine: + +* `engine.Loop` - class used for continuous, in-memory interaction with the vm (e.g. terminal). +* `engine.RunPersisted` - method which combines single vm executions with persisted state (e.g. http). + + ## Bytecode examples (Minimal, WIP) @@ -182,7 +199,7 @@ It expects all replacement symbols to be available at time of rendering, and has ## Assembly examples -See `testdata/*.fst` +See `testdata/*.vis` ## Development tools diff --git a/engine/engine.go b/engine/engine.go @@ -28,6 +28,7 @@ type Engine struct { rs resource.Resource ca cache.Memory vm *vm.Vm + root string initd bool } @@ -44,11 +45,8 @@ func NewEngine(cfg Config, st *state.State, rs resource.Resource, ca cache.Memor ca: ca, vm: vm.NewVm(st, rs, ca, szr), } - var err error - if st.Moves == 0 { - err = engine.Init(cfg.Root, ctx) - } - return engine, err + engine.root = cfg.Root + return engine, nil } // Init must be explicitly called before using the Engine instance. @@ -62,6 +60,7 @@ func(en *Engine) Init(sym string, ctx context.Context) error { if sym == "" { return fmt.Errorf("start sym empty") } + inSave, _ := en.st.GetInput() err := en.st.SetInput([]byte{}) if err != nil { return err @@ -77,6 +76,10 @@ func(en *Engine) Init(sym string, ctx context.Context) error { } log.Printf("ended init VM run with code %x", b) en.st.SetCode(b) + err = en.st.SetInput(inSave) + if err != nil { + return err + } en.initd = true return nil } @@ -92,7 +95,17 @@ func(en *Engine) Init(sym string, ctx context.Context) error { // - no current bytecode is available // - input processing against bytcode failed func (en *Engine) Exec(input []byte, ctx context.Context) (bool, error) { - err := vm.ValidInput(input) + var err error + if en.st.Moves == 0 { + err = en.Init(en.root, ctx) + if err != nil { + return false, err + } + if len(input) == 0 { + return true, nil + } + } + err = vm.ValidInput(input) if err != nil { return true, err } @@ -109,6 +122,7 @@ func (en *Engine) Exec(input []byte, ctx context.Context) (bool, error) { if len(code) == 0 { return false, fmt.Errorf("no code to execute") } + log.Printf("start new VM run with code %x", code) code, err = en.vm.Run(code, ctx) if err != nil { @@ -124,13 +138,14 @@ func (en *Engine) Exec(input []byte, ctx context.Context) (bool, error) { if len(code) > 0 { log.Printf("terminated with code remaining: %x", code) } - return false, nil + return false, err } en.st.SetCode(code) if len(code) == 0 { log.Printf("runner finished with no remaining code") - return false, nil + err = en.reset(ctx) + return false, err } return true, nil @@ -149,3 +164,22 @@ func(en *Engine) WriteResult(w io.Writer, ctx context.Context) (int, error) { } return io.WriteString(w, r) } + +func(en *Engine) reset(ctx context.Context) error { + var err error + var isTop bool + for !isTop { + isTop, err = en.st.Top() + if err != nil { + return err + } + _, err = en.st.Up() + if err != nil { + return err + } + en.ca.Pop() + } + en.st.Restart() + en.initd = false + return en.Init(en.root, ctx) +} diff --git a/engine/engine_test.go b/engine/engine_test.go @@ -87,7 +87,11 @@ func TestEngineInit(t *testing.T) { if err != nil { t.Fatal(err) } -// + + err = en.Init("root", ctx) + if err != nil { + t.Fatal(err) + } w := bytes.NewBuffer(nil) _, err = en.WriteResult(w, ctx) if err != nil { @@ -152,3 +156,40 @@ func TestEngineExecInvalidInput(t *testing.T) { t.Fatalf("expected fail on invalid input") } } + +func TestEngineResumeTerminated(t *testing.T) { + generateTestData(t) + ctx := context.TODO() + st := state.NewState(17) + rs := NewFsWrapper(dataDir, &st) + ca := cache.NewCache().WithCacheSize(1024) + + en, err := NewEngine(Config{ + Root: "root", + }, &st, &rs, ca, ctx) + if err != nil { + t.Fatal(err) + } + err = en.Init("root", ctx) + if err != nil { + t.Fatal(err) + } + + _, err = en.Exec([]byte("1"), ctx) + if err != nil { + t.Fatal(err) + } + + _, err = en.Exec([]byte("1"), ctx) + if err != nil { + t.Fatal(err) + } + + location, idx := st.Where() + if location != "root" { + t.Fatalf("expected 'root', got %s", location) + } + if idx != 0 { + t.Fatalf("expected idx '0', got %v", idx) + } +} diff --git a/state/flag.go b/state/flag.go @@ -6,8 +6,6 @@ const ( FLAG_TERMINATE = 3 FLAG_DIRTY = 4 FLAG_LOADFAIL = 5 - FLAG_USERSTART = 9 - //FLAG_WRITEABLE = FLAG_LOADFAIL ) func IsWriteableFlag(flag uint32) bool { diff --git a/state/state.go b/state/state.go @@ -327,10 +327,22 @@ func(st *State) SetInput(input []byte) error { return nil } -func(st *State) Reset() error { +// Reset re-initializes the state to run from top node with accumulated client state. +func(st *State) Restart() error { + st.resetBaseFlags() + st.Moves = 0 + st.SizeIdx = 0 + st.input = []byte{} return nil } +// String implements String interface func(st State) String() string { return fmt.Sprintf("moves %v idx %v path: %s", st.Moves, st.SizeIdx, strings.Join(st.ExecPath, "/")) } + +// initializes all flags not in control of client. +func(st *State) resetBaseFlags() { + st.Flags[0] = 0 +} + diff --git a/vm/runner.go b/vm/runner.go @@ -195,7 +195,6 @@ func(vm *Vm) RunCatch(b []byte, ctx context.Context) ([]byte, error) { b = bh vm.st.Down(sym) vm.ca.Push() - vm.Reset() } return b, nil } @@ -213,7 +212,6 @@ func(vm *Vm) RunCroak(b []byte, ctx context.Context) ([]byte, error) { 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{}