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.
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

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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
}

View File

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