Add engine and state restart on empty termination node

This commit is contained in:
lash 2023-04-16 10:40:41 +01:00
parent 957d59bb1a
commit bf1d634474
Signed by untrusted user who does not match committer: lash
GPG Key ID: 21D2E7BB88C2A746
6 changed files with 121 additions and 21 deletions

View File

@ -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. 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 ### 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. This repository provides a `golang` reference implementation for the `vise` concept.
In this reference implementation some constraints apply
### Structure ### Structure
- `vm`: Defines instructions, and applies transformations according to the instructions. - `asm`: Assembly parser and compiler.
- `state`: Holds the bytecode buffer, error states and navigation states.
- `cache`: Holds and manages all loaded content. - `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. - `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 ### 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. 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 ## Bytecode examples
(Minimal, WIP) (Minimal, WIP)
@ -182,7 +199,7 @@ It expects all replacement symbols to be available at time of rendering, and has
## Assembly examples ## Assembly examples
See `testdata/*.fst` See `testdata/*.vis`
## Development tools ## Development tools

View File

@ -28,6 +28,7 @@ type Engine struct {
rs resource.Resource rs resource.Resource
ca cache.Memory ca cache.Memory
vm *vm.Vm vm *vm.Vm
root string
initd bool initd bool
} }
@ -44,11 +45,8 @@ func NewEngine(cfg Config, st *state.State, rs resource.Resource, ca cache.Memor
ca: ca, ca: ca,
vm: vm.NewVm(st, rs, ca, szr), vm: vm.NewVm(st, rs, ca, szr),
} }
var err error engine.root = cfg.Root
if st.Moves == 0 { return engine, nil
err = engine.Init(cfg.Root, ctx)
}
return engine, err
} }
// Init must be explicitly called before using the Engine instance. // 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 == "" { if sym == "" {
return fmt.Errorf("start sym empty") return fmt.Errorf("start sym empty")
} }
inSave, _ := en.st.GetInput()
err := en.st.SetInput([]byte{}) err := en.st.SetInput([]byte{})
if err != nil { if err != nil {
return err 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) log.Printf("ended init VM run with code %x", b)
en.st.SetCode(b) en.st.SetCode(b)
err = en.st.SetInput(inSave)
if err != nil {
return err
}
en.initd = true en.initd = true
return nil return nil
} }
@ -92,7 +95,17 @@ func(en *Engine) Init(sym string, ctx context.Context) error {
// - no current bytecode is available // - no current bytecode is available
// - input processing against bytcode failed // - input processing against bytcode failed
func (en *Engine) Exec(input []byte, ctx context.Context) (bool, error) { 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 { if err != nil {
return true, err return true, err
} }
@ -109,6 +122,7 @@ func (en *Engine) Exec(input []byte, ctx context.Context) (bool, error) {
if len(code) == 0 { if len(code) == 0 {
return false, fmt.Errorf("no code to execute") return false, fmt.Errorf("no code to execute")
} }
log.Printf("start new VM run with code %x", code) log.Printf("start new VM run with code %x", code)
code, err = en.vm.Run(code, ctx) code, err = en.vm.Run(code, ctx)
if err != nil { if err != nil {
@ -124,13 +138,14 @@ func (en *Engine) Exec(input []byte, ctx context.Context) (bool, error) {
if len(code) > 0 { if len(code) > 0 {
log.Printf("terminated with code remaining: %x", code) log.Printf("terminated with code remaining: %x", code)
} }
return false, nil return false, err
} }
en.st.SetCode(code) en.st.SetCode(code)
if len(code) == 0 { if len(code) == 0 {
log.Printf("runner finished with no remaining code") log.Printf("runner finished with no remaining code")
return false, nil err = en.reset(ctx)
return false, err
} }
return true, nil return true, nil
@ -149,3 +164,22 @@ func(en *Engine) WriteResult(w io.Writer, ctx context.Context) (int, error) {
} }
return io.WriteString(w, r) 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)
}

View File

@ -87,7 +87,11 @@ func TestEngineInit(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
//
err = en.Init("root", ctx)
if err != nil {
t.Fatal(err)
}
w := bytes.NewBuffer(nil) w := bytes.NewBuffer(nil)
_, err = en.WriteResult(w, ctx) _, err = en.WriteResult(w, ctx)
if err != nil { if err != nil {
@ -152,3 +156,40 @@ func TestEngineExecInvalidInput(t *testing.T) {
t.Fatalf("expected fail on invalid input") 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)
}
}

View File

@ -6,8 +6,6 @@ const (
FLAG_TERMINATE = 3 FLAG_TERMINATE = 3
FLAG_DIRTY = 4 FLAG_DIRTY = 4
FLAG_LOADFAIL = 5 FLAG_LOADFAIL = 5
FLAG_USERSTART = 9
//FLAG_WRITEABLE = FLAG_LOADFAIL
) )
func IsWriteableFlag(flag uint32) bool { func IsWriteableFlag(flag uint32) bool {

View File

@ -327,10 +327,22 @@ func(st *State) SetInput(input []byte) error {
return nil 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 return nil
} }
// String implements String interface
func(st State) String() string { func(st State) String() string {
return fmt.Sprintf("moves %v idx %v path: %s", st.Moves, st.SizeIdx, strings.Join(st.ExecPath, "/")) 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
}

View File

@ -195,7 +195,6 @@ func(vm *Vm) RunCatch(b []byte, ctx context.Context) ([]byte, error) {
b = bh b = bh
vm.st.Down(sym) vm.st.Down(sym)
vm.ca.Push() vm.ca.Push()
vm.Reset()
} }
return b, nil return b, nil
} }
@ -213,7 +212,6 @@ func(vm *Vm) RunCroak(b []byte, ctx context.Context) ([]byte, error) {
if r { if r {
log.Printf("croak at flag %v, purging and moving to top", sig) log.Printf("croak at flag %v, purging and moving to top", sig)
vm.Reset() vm.Reset()
vm.st.Reset()
vm.pg.Reset() vm.pg.Reset()
vm.ca.Reset() vm.ca.Reset()
b = []byte{} b = []byte{}