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:
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{}