commit a2d947e1069a2952eb402573d08cf8d3cd8ec5c9
parent 064418cb8329d170eec45236a1c9ddd3025fd93d
Author: lash <dev@holbrook.no>
Date: Thu, 13 Apr 2023 00:38:33 +0100
Add persisted state engine runner
Diffstat:
12 files changed, 186 insertions(+), 40 deletions(-)
diff --git a/dev/interactive/main.go b/dev/interactive/main.go
@@ -21,7 +21,7 @@ func main() {
ctx := context.Background()
en := engine.NewSizedEngine(dir, uint32(size))
- err := engine.Loop(&en, root, ctx, os.Stdin, os.Stdout)
+ err := engine.Loop(&en, os.Stdin, os.Stdout, ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "loop exited with error: %v", err)
os.Exit(1)
diff --git a/engine/default.go b/engine/default.go
@@ -1,6 +1,8 @@
package engine
import (
+ "context"
+
"git.defalsify.org/festive/cache"
"git.defalsify.org/festive/resource"
"git.defalsify.org/festive/state"
@@ -11,7 +13,11 @@ func NewDefaultEngine(dir string) Engine {
st := state.NewState(0)
rs := resource.NewFsResource(dir)
ca := cache.NewCache()
- return NewEngine(Config{}, &st, &rs, ca)
+ cfg := Config{
+ Root: "root",
+ }
+ ctx := context.TODO()
+ return NewEngine(cfg, &st, &rs, ca, ctx)
}
// NewSizedEngine is a convenience function to instantiate a filesystem-backed engine with a specified output constraint.
@@ -21,6 +27,8 @@ func NewSizedEngine(dir string, size uint32) Engine {
ca := cache.NewCache()
cfg := Config{
OutputSize: size,
+ Root: "root",
}
- return NewEngine(cfg, &st, &rs, ca)
+ ctx := context.TODO()
+ return NewEngine(cfg, &st, &rs, ca, ctx)
}
diff --git a/engine/engine.go b/engine/engine.go
@@ -16,6 +16,10 @@ import (
// Config globally defines behavior of all components driven by the engine.
type Config struct {
OutputSize uint32 // Maximum size of output from a single rendered page
+ SessionId string
+ Root string
+ FlagCount uint32
+ CacheSize uint32
}
// Engine is an execution engine that handles top-level errors when running client inputs against code in the bytecode buffer.
@@ -24,10 +28,11 @@ type Engine struct {
rs resource.Resource
ca cache.Memory
vm *vm.Vm
+ initd bool
}
// NewEngine creates a new Engine
-func NewEngine(cfg Config, st *state.State, rs resource.Resource, ca cache.Memory) Engine {
+func NewEngine(cfg Config, st *state.State, rs resource.Resource, ca cache.Memory, ctx context.Context) Engine {
var szr *render.Sizer
if cfg.OutputSize > 0 {
szr = render.NewSizer(cfg.OutputSize)
@@ -38,6 +43,9 @@ func NewEngine(cfg Config, st *state.State, rs resource.Resource, ca cache.Memor
ca: ca,
vm: vm.NewVm(st, rs, ca, szr),
}
+ if cfg.Root != "" {
+ engine.Init(cfg.Root, ctx)
+ }
return engine
}
@@ -45,6 +53,13 @@ func NewEngine(cfg Config, st *state.State, rs resource.Resource, ca cache.Memor
//
// It loads and executes code for the start node.
func(en *Engine) Init(sym string, ctx context.Context) error {
+ if en.initd {
+ log.Printf("already initialized")
+ return nil
+ }
+ if sym == "" {
+ return fmt.Errorf("start sym empty")
+ }
err := en.st.SetInput([]byte{})
if err != nil {
return err
@@ -55,6 +70,7 @@ func(en *Engine) Init(sym string, ctx context.Context) error {
return err
}
en.st.SetCode(b)
+ en.initd = true
return nil
}
diff --git a/engine/engine_test.go b/engine/engine_test.go
@@ -40,12 +40,18 @@ func(fs FsWrapper) inky(sym string, ctx context.Context) (string, error) {
return "tinkywinky", nil
}
+func(fs FsWrapper) pinky(sym string, ctx context.Context) (string, error) {
+ return "xyzzy", nil
+}
+
func(fs FsWrapper) FuncFor(sym string) (resource.EntryFunc, error) {
switch sym {
case "one":
return fs.one, nil
case "inky":
return fs.inky, nil
+ case "pinky":
+ return fs.pinky, nil
}
return nil, fmt.Errorf("function for %v not found", sym)
}
@@ -75,7 +81,7 @@ func TestEngineInit(t *testing.T) {
rs := NewFsWrapper(dataDir, &st)
ca := cache.NewCache().WithCacheSize(1024)
- en := NewEngine(Config{}, &st, &rs, ca)
+ en := NewEngine(Config{}, &st, &rs, ca, ctx)
err := en.Init("root", ctx)
if err != nil {
t.Fatal(err)
@@ -129,7 +135,7 @@ func TestEngineExecInvalidInput(t *testing.T) {
rs := NewFsWrapper(dataDir, &st)
ca := cache.NewCache().WithCacheSize(1024)
- en := NewEngine(Config{}, &st, &rs, ca)
+ en := NewEngine(Config{}, &st, &rs, ca, ctx)
err := en.Init("root", ctx)
if err != nil {
t.Fatal(err)
diff --git a/engine/loop.go b/engine/loop.go
@@ -18,13 +18,8 @@ import (
// Any error not handled by the engine will terminate the oop and return an error.
//
// Rendered output is written to the provided writer.
-func Loop(en *Engine, startSym string, ctx context.Context, reader io.Reader, writer io.Writer) error {
- err := en.Init(startSym, ctx)
- if err != nil {
- return fmt.Errorf("cannot init: %v\n", err)
- }
-
- err = en.WriteResult(writer, ctx)
+func Loop(en *Engine, reader io.Reader, writer io.Writer, ctx context.Context) error {
+ err := en.WriteResult(writer, ctx)
if err != nil {
return err
}
diff --git a/engine/loop_test.go b/engine/loop_test.go
@@ -19,8 +19,11 @@ func TestLoopTop(t *testing.T) {
st := state.NewState(0)
rs := resource.NewFsResource(dataDir)
ca := cache.NewCache().WithCacheSize(1024)
-
- en := NewEngine(Config{}, &st, &rs, ca)
+
+ cfg := Config{
+ Root: "root",
+ }
+ en := NewEngine(cfg, &st, &rs, ca, ctx)
err := en.Init("root", ctx)
if err != nil {
t.Fatal(err)
@@ -36,7 +39,7 @@ func TestLoopTop(t *testing.T) {
outputBuf := bytes.NewBuffer(nil)
log.Printf("running with input: %s", inputBuf.Bytes())
- err = Loop(&en, "root", ctx, inputBuf, outputBuf)
+ err = Loop(&en, inputBuf, outputBuf, ctx)
if err != nil {
t.Fatal(err)
}
@@ -53,7 +56,10 @@ func TestLoopBackForth(t *testing.T) {
rs := resource.NewFsResource(dataDir)
ca := cache.NewCache().WithCacheSize(1024)
- en := NewEngine(Config{}, &st, &rs, ca)
+ cfg := Config{
+ Root: "root",
+ }
+ en := NewEngine(cfg, &st, &rs, ca, ctx)
err := en.Init("root", ctx)
if err != nil {
t.Fatal(err)
@@ -70,7 +76,7 @@ func TestLoopBackForth(t *testing.T) {
outputBuf := bytes.NewBuffer(nil)
log.Printf("running with input: %s", inputBuf.Bytes())
- err = Loop(&en, "root", ctx, inputBuf, outputBuf)
+ err = Loop(&en, inputBuf, outputBuf, ctx)
if err != nil {
t.Fatal(err)
}
@@ -85,8 +91,9 @@ func TestLoopBrowse(t *testing.T) {
cfg := Config{
OutputSize: 68,
+ Root: "root",
}
- en := NewEngine(cfg, &st, &rs, ca)
+ en := NewEngine(cfg, &st, &rs, ca, ctx)
err := en.Init("root", ctx)
if err != nil {
t.Fatal(err)
@@ -104,7 +111,7 @@ func TestLoopBrowse(t *testing.T) {
outputBuf := bytes.NewBuffer(nil)
log.Printf("running with input: %s", inputBuf.Bytes())
- err = Loop(&en, "root", ctx, inputBuf, outputBuf)
+ err = Loop(&en, inputBuf, outputBuf, ctx)
if err != nil {
t.Fatal(err)
}
diff --git a/engine/persist.go b/engine/persist.go
@@ -0,0 +1,28 @@
+package engine
+
+import (
+ "context"
+ "io"
+ "log"
+
+ "git.defalsify.org/festive/persist"
+ "git.defalsify.org/festive/resource"
+)
+
+func RunPersisted(cfg Config, rs resource.Resource, pr persist.Persister, input []byte, w io.Writer, ctx context.Context) error {
+ err := pr.Load(cfg.SessionId)
+ if err != nil {
+ return err
+ }
+ st := pr.GetState()
+ log.Printf("st %v", st)
+ en := NewEngine(cfg, pr.GetState(), rs, pr.GetMemory(), ctx)
+
+ if len(input) > 0 {
+ _, err = en.Exec(input, ctx)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/engine/persist_test.go b/engine/persist_test.go
@@ -0,0 +1,68 @@
+package engine
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "io/ioutil"
+ "os"
+ "testing"
+
+ "git.defalsify.org/festive/cache"
+ "git.defalsify.org/festive/persist"
+ "git.defalsify.org/festive/state"
+)
+
+func TestPersist(t *testing.T) {
+ generateTestData(t)
+ cfg := Config{
+ OutputSize: 128,
+ SessionId: "xyzzy",
+ Root: "root",
+ }
+ rs := NewFsWrapper(dataDir, nil)
+
+ persistDir, err := ioutil.TempDir("", "festive_engine_persist")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ st := state.NewState(3)
+ ca := cache.NewCache().WithCacheSize(1024)
+ pr := persist.NewFsPersister(persistDir).WithContent(&st, ca)
+
+ w := bytes.NewBuffer(nil)
+ ctx := context.TODO()
+
+
+ err = RunPersisted(cfg, rs, pr, []byte{}, w, ctx)
+ if err != nil {
+ if !errors.Is(err, os.ErrNotExist) {
+ t.Fatal(err)
+ }
+ st := state.NewState(cfg.FlagCount)
+ ca := cache.NewCache()
+ if cfg.CacheSize > 0 {
+ ca = ca.WithCacheSize(cfg.CacheSize)
+ }
+ pr = persist.NewFsPersister(persistDir).WithContent(&st, ca)
+ err = pr.Save(cfg.SessionId)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ pr = persist.NewFsPersister(persistDir)
+ inputs := []string{
+ "",
+ "1",
+ "2",
+ "00",
+ }
+ for _, v := range inputs {
+ err = RunPersisted(cfg, rs, pr, []byte(v), w, ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+}
diff --git a/persist/fs.go b/persist/fs.go
@@ -2,6 +2,7 @@ package persist
import (
"io/ioutil"
+ "log"
"path"
"path/filepath"
"github.com/fxamacker/cbor/v2"
@@ -32,6 +33,14 @@ func(p *FsPersister) WithContent(st *state.State, ca *cache.Cache) *FsPersister
return p
}
+func(p *FsPersister) GetState() *state.State {
+ return p.State
+}
+
+func(p *FsPersister) GetMemory() cache.Memory {
+ return p.Memory
+}
+
func(p *FsPersister) Serialize() ([]byte, error) {
return cbor.Marshal(p)
}
@@ -47,6 +56,7 @@ func(p *FsPersister) Save(key string) error {
return err
}
fp := path.Join(p.dir, key)
+ log.Printf("saved key %v", key)
return ioutil.WriteFile(fp, b, 0600)
}
@@ -57,5 +67,6 @@ func(p *FsPersister) Load(key string) error {
return err
}
err = p.Deserialize(b)
+ log.Printf("loaded key %v", key)
return err
}
diff --git a/persist/persist.go b/persist/persist.go
@@ -1,9 +1,16 @@
package persist
+import (
+ "git.defalsify.org/festive/cache"
+ "git.defalsify.org/festive/state"
+)
+
type Persister interface {
Serialize() ([]byte, error)
Deserialize(b []byte) error
Save(key string) error
Load(key string) error
+ GetState() *state.State
+ GetMemory() cache.Memory
}
diff --git a/state/state.go b/state/state.go
@@ -33,7 +33,7 @@ type State struct {
ExecPath []string // Command symbols stack
BitSize uint32 // size of (32-bit capacity) bit flag byte array
SizeIdx uint16
- flags []byte // Error state
+ Flags []byte // Error state
input []byte // Last input
}
@@ -64,9 +64,9 @@ func NewState(BitSize uint32) State {
}
byteSize := toByteSize(BitSize + 8)
if byteSize > 0 {
- st.flags = make([]byte, byteSize)
+ st.Flags = make([]byte, byteSize)
} else {
- st.flags = []byte{}
+ st.Flags = []byte{}
}
return st
}
@@ -80,14 +80,14 @@ func(st *State) SetFlag(bitIndex uint32) (bool, error) {
if bitIndex + 1 > st.BitSize {
return false, fmt.Errorf("bit index %v is out of range of bitfield size %v", bitIndex, st.BitSize)
}
- r := getFlag(bitIndex, st.flags)
+ r := getFlag(bitIndex, st.Flags)
if r {
return false, nil
}
byteIndex := bitIndex / 8
localBitIndex := bitIndex % 8
- b := st.flags[byteIndex]
- st.flags[byteIndex] = b | (1 << localBitIndex)
+ b := st.Flags[byteIndex]
+ st.Flags[byteIndex] = b | (1 << localBitIndex)
return true, nil
}
@@ -101,14 +101,14 @@ func(st *State) ResetFlag(bitIndex uint32) (bool, error) {
if bitIndex + 1 > st.BitSize {
return false, fmt.Errorf("bit index %v is out of range of bitfield size %v", bitIndex, st.BitSize)
}
- r := getFlag(bitIndex, st.flags)
+ r := getFlag(bitIndex, st.Flags)
if !r {
return false, nil
}
byteIndex := bitIndex / 8
localBitIndex := bitIndex % 8
- b := st.flags[byteIndex]
- st.flags[byteIndex] = b & (^(1 << localBitIndex))
+ b := st.Flags[byteIndex]
+ st.Flags[byteIndex] = b & (^(1 << localBitIndex))
return true, nil
}
@@ -119,7 +119,7 @@ func(st *State) GetFlag(bitIndex uint32) (bool, error) {
if bitIndex + 1 > st.BitSize {
return false, fmt.Errorf("bit index %v is out of range of bitfield size %v", bitIndex, st.BitSize)
}
- return getFlag(bitIndex, st.flags), nil
+ return getFlag(bitIndex, st.Flags), nil
}
// FlagBitSize reports the amount of bits available in the bit field index.
@@ -129,7 +129,7 @@ func(st *State) FlagBitSize() uint32 {
// FlagBitSize reports the amount of bits available in the bit field index.
func(st *State) FlagByteSize() uint8 {
- return uint8(len(st.flags))
+ return uint8(len(st.Flags))
}
// MatchFlag matches the current state of the given flag.
@@ -169,7 +169,7 @@ func(st *State) GetIndex(flags []byte) bool {
var i uint32
for i = 0; i < st.BitSize; i++ {
testVal := flags[byteIndex] & (1 << localIndex)
- if (testVal & st.flags[byteIndex]) > 0 {
+ if (testVal & st.Flags[byteIndex]) > 0 {
return true
}
globalIndex += 1
diff --git a/state/state_test.go b/state/state_test.go
@@ -8,16 +8,16 @@ import (
// Check creation
func TestNewState(t *testing.T) {
st := NewState(5)
- if len(st.flags) != 2 {
- t.Fatalf("invalid state flag length: %v", len(st.flags))
+ if len(st.Flags) != 2 {
+ t.Fatalf("invalid state flag length: %v", len(st.Flags))
}
st = NewState(8)
- if len(st.flags) != 2 {
- t.Fatalf("invalid state flag length: %v", len(st.flags))
+ if len(st.Flags) != 2 {
+ t.Fatalf("invalid state flag length: %v", len(st.Flags))
}
st = NewState(17)
- if len(st.flags) != 4 {
- t.Fatalf("invalid state flag length: %v", len(st.flags))
+ if len(st.Flags) != 4 {
+ t.Fatalf("invalid state flag length: %v", len(st.Flags))
}
}
@@ -98,8 +98,8 @@ func TestStateflags(t *testing.T) {
if err == nil {
t.Fatalf("Expected out of range for bit index 17")
}
- if !bytes.Equal(st.flags[:3], []byte{0x04, 0x04, 0x01}) {
- t.Fatalf("Expected 0x040401, got %v", st.flags[:3])
+ if !bytes.Equal(st.Flags[:3], []byte{0x04, 0x04, 0x01}) {
+ t.Fatalf("Expected 0x040401, got %v", st.Flags[:3])
}
}