commit 41dc0ccbaaf9366b8d92c4770dc7ee8754b2d862
parent accdb96d175cad33402d326f105f0d16bd409eb8
Author: lash <dev@holbrook.no>
Date: Mon, 2 Sep 2024 00:37:30 +0100
Add replacement engine, temporary name DbEngine
Diffstat:
5 files changed, 481 insertions(+), 1 deletion(-)
diff --git a/engine/db.go b/engine/db.go
@@ -0,0 +1,401 @@
+package engine
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "os"
+
+ "git.defalsify.org/vise.git/cache"
+ "git.defalsify.org/vise.git/persist"
+ "git.defalsify.org/vise.git/resource"
+ "git.defalsify.org/vise.git/render"
+ "git.defalsify.org/vise.git/state"
+ "git.defalsify.org/vise.git/vm"
+)
+
+type DbEngine struct {
+ st *state.State
+ ca cache.Memory
+ vm *vm.Vm
+ rs resource.Resource
+ pe *persist.Persister
+ cfg Config
+ dbg Debug
+ first resource.EntryFunc
+ initd bool
+ exit string
+}
+
+func NewDbEngine(cfg Config, rs resource.Resource) *DbEngine {
+ if rs == nil {
+ panic("resource cannot be nil")
+ }
+ en := &DbEngine{
+ rs: rs,
+ cfg: cfg,
+ }
+ if en.cfg.Root == "" {
+ en.cfg.Root = "root"
+ }
+ return en
+}
+
+func(en *DbEngine) WithState(st *state.State) *DbEngine {
+ if en.st != nil {
+ panic("state already set")
+ }
+ if st == nil {
+ panic("state argument is nil")
+ }
+ en.st = st
+ return en
+}
+
+func(en *DbEngine) WithCache(ca cache.Memory) *DbEngine {
+ if en.ca != nil {
+ panic("cache already set")
+ }
+ if ca == nil {
+ panic("cache argument is nil")
+ }
+ en.ca = ca
+ return en
+}
+
+func(en *DbEngine) WithResource(rs resource.Resource) *DbEngine {
+ if en.rs != nil {
+ panic("resource already set")
+ }
+ if rs == nil {
+ panic("resource argument is nil")
+ }
+ en.rs = rs
+ return en
+}
+
+func(en *DbEngine) WithPersister(pe *persist.Persister) *DbEngine {
+ if en.pe != nil {
+ panic("persister already set")
+ }
+ if pe == nil {
+ panic("persister argument is nil")
+ }
+ en.pe = pe
+ return en
+}
+
+func(en *DbEngine) WithDebug(dbg Debug) *DbEngine {
+ if en.dbg != nil {
+ panic("debugger already set")
+ }
+ if dbg == nil {
+ logg.Infof("debug argument was nil, using default debugger")
+ dbg = NewSimpleDebug(os.Stderr)
+ }
+ en.dbg = dbg
+ return en
+}
+
+func(en *DbEngine) WithFirst(fn resource.EntryFunc) *DbEngine {
+ if en.first != nil {
+ panic("firstfunc already set")
+ }
+ if fn == nil {
+ panic("firstfunc argument is nil")
+ }
+ en.first = fn
+ return en
+}
+
+func(en *DbEngine) ensureState() {
+ if en.st == nil {
+ st := state.NewState(en.cfg.FlagCount)
+ en.st = &st
+ en.st.SetLanguage(en.cfg.Language)
+ if en.st.Language != nil {
+ en.st.SetFlag(state.FLAG_LANG)
+ }
+ } else {
+ if (en.cfg.Language != "") {
+ if en.st.Language == nil {
+ en.st.SetLanguage(en.cfg.Language)
+ en.st.SetFlag(state.FLAG_LANG)
+ } else {
+ logg.Warnf("language '%s'set in config, but will be ignored because state language has already been set.", )
+ }
+ }
+ }
+
+}
+
+func(en *DbEngine) ensureMemory() {
+ if en.ca == nil {
+ ca := cache.NewCache()
+ if en.cfg.CacheSize > 0 {
+ ca.WithCacheSize(en.cfg.CacheSize)
+ }
+ en.ca = ca
+ }
+}
+
+func(en *DbEngine) setupVm() {
+ var szr *render.Sizer
+ if en.cfg.OutputSize > 0 {
+ szr = render.NewSizer(en.cfg.OutputSize)
+ }
+ en.vm = vm.NewVm(en.st, en.rs, en.ca, szr)
+}
+
+func(en *DbEngine) prepare() {
+ en.ensureState()
+ en.ensureMemory()
+ en.setupVm()
+}
+
+// execute the first function, if set
+func(en *DbEngine) runFirst(ctx context.Context) (bool, error) {
+ var err error
+ var r bool
+ if en.first == nil {
+ return true, nil
+ }
+ logg.DebugCtxf(ctx, "start pre-VM check")
+ rs := resource.NewMenuResource()
+ rs.AddLocalFunc("_first", en.first)
+ en.st.Down("_first")
+ pvm := vm.NewVm(en.st, rs, en.ca, nil)
+ b := vm.NewLine(nil, vm.LOAD, []string{"_first"}, []byte{0}, nil)
+ b = vm.NewLine(b, vm.HALT, nil, nil, nil)
+ b, err = pvm.Run(ctx, b)
+ if err != nil {
+ return false, err
+ }
+ if len(b) > 0 {
+ // TODO: typed error
+ err = fmt.Errorf("Pre-VM code cannot have remaining bytecode after execution, had: %x", b)
+ } else {
+ if en.st.MatchFlag(state.FLAG_TERMINATE, true) {
+ en.exit = en.ca.Last()
+ logg.InfoCtxf(ctx, "Pre-VM check says not to continue execution", "state", en.st)
+ } else {
+ r = true
+ }
+ }
+ if err != nil {
+ en.st.Invalidate()
+ en.ca.Invalidate()
+ }
+ en.st.ResetFlag(state.FLAG_TERMINATE)
+ en.st.ResetFlag(state.FLAG_DIRTY)
+ logg.DebugCtxf(ctx, "end pre-VM check")
+ return r, err
+}
+
+// Finish implements EngineIsh interface
+func(en *DbEngine) Finish() error {
+ var perr error
+ if en.pe != nil {
+ perr = en.pe.Save(en.cfg.SessionId)
+ }
+ err := en.rs.Close()
+ if err != nil {
+ logg.Errorf("resource close failed!", "err", err)
+ }
+ if perr != nil {
+ logg.Errorf("persistence failed!", "err", perr)
+ err = perr
+ }
+ if err == nil {
+ logg.Tracef("that's a wrap", "engine", en)
+ }
+ return err
+}
+
+// change root to current state location if non-empty.
+func(en *DbEngine) restore() {
+ location, _ := en.st.Where()
+ if len(location) == 0 {
+ return
+ }
+ if en.cfg.Root != location {
+ logg.Infof("restoring state", "sym", location)
+ en.cfg.Root = "."
+ }
+}
+
+// Init must be explicitly called before using the Engine instance.
+//
+// It loads and executes code for the start node.
+func(en *DbEngine) Init(ctx context.Context) (bool, error) {
+ en.prepare()
+ en.restore()
+ if en.initd {
+ logg.DebugCtxf(ctx, "already initialized")
+ return true, nil
+ }
+
+ sym := en.cfg.Root
+ if sym == "" {
+ return false, fmt.Errorf("start sym empty")
+ }
+
+ inSave, _ := en.st.GetInput()
+ err := en.st.SetInput([]byte{})
+ if err != nil {
+ return false, err
+ }
+
+ r, err := en.runFirst(ctx)
+ if err != nil {
+ return false, err
+ }
+ if !r {
+ return false, nil
+ }
+
+ b := vm.NewLine(nil, vm.MOVE, []string{sym}, nil, nil)
+ logg.DebugCtxf(ctx, "start new init VM run", "code", b)
+ b, err = en.vm.Run(ctx, b)
+ if err != nil {
+ return false, err
+ }
+
+ logg.DebugCtxf(ctx, "end new init VM run", "code", b)
+ en.st.SetCode(b)
+ err = en.st.SetInput(inSave)
+ if err != nil {
+ return false, err
+ }
+ return len(b) > 0, nil
+}
+
+// Exec processes user input against the current state of the virtual machine environment.
+//
+// If successfully executed, output of the last execution is available using the WriteResult call.
+//
+// A bool return valus of false indicates that execution should be terminated. Calling Exec again has undefined effects.
+//
+// Fails if:
+// - input is formally invalid (too long etc)
+// - no current bytecode is available
+// - input processing against bytcode failed
+func (en *DbEngine) Exec(ctx context.Context, input []byte) (bool, error) {
+ var err error
+ if en.st.Language != nil {
+ ctx = context.WithValue(ctx, "Language", *en.st.Language)
+ }
+ if en.st.Moves == 0 {
+ cont, err := en.Init(ctx)
+ if err != nil {
+ return false, err
+ }
+ return cont, nil
+ }
+ err = vm.ValidInput(input)
+ if err != nil {
+ return true, err
+ }
+ err = en.st.SetInput(input)
+ if err != nil {
+ return false, err
+ }
+ return en.exec(ctx, input)
+}
+
+// backend for Exec, after the input validity check
+func(en *DbEngine) exec(ctx context.Context, input []byte) (bool, error) {
+ logg.InfoCtxf(ctx, "new VM execution with input", "input", string(input))
+ code, err := en.st.GetCode()
+ if err != nil {
+ return false, err
+ }
+ if len(code) == 0 {
+ return false, fmt.Errorf("no code to execute")
+ }
+
+ logg.Debugf("start new VM run", "code", code)
+ code, err = en.vm.Run(ctx, code)
+ if err != nil {
+ return false, err
+ }
+ logg.Debugf("end new VM run", "code", code)
+
+ v := en.st.MatchFlag(state.FLAG_TERMINATE, true)
+ if v {
+ if len(code) > 0 {
+ logg.Debugf("terminated with code remaining", "code", code)
+ }
+ return false, err
+ }
+
+ en.st.SetCode(code)
+ if len(code) == 0 {
+ logg.Infof("runner finished with no remaining code", "state", en.st)
+ if en.st.MatchFlag(state.FLAG_DIRTY, true) {
+ logg.Debugf("have output for quitting")
+ en.exit = en.ca.Last()
+ }
+ _, err = en.reset(ctx)
+ return false, err
+ }
+
+ if en.dbg != nil {
+ en.dbg.Break(en.st, en.ca)
+ }
+ return true, nil
+}
+
+// WriteResult writes the output of the last vm execution to the given writer.
+//
+// Fails if
+// - required data inputs to the template are not available.
+// - the template for the given node point is note available for retrieval using the resource.Resource implementer.
+// - the supplied writer fails to process the writes.
+func(en *DbEngine) WriteResult(ctx context.Context, w io.Writer) (int, error) {
+ var l int
+ if en.st.Language != nil {
+ ctx = context.WithValue(ctx, "Language", *en.st.Language)
+ }
+ logg.TraceCtxf(ctx, "render with state", "state", en.st)
+ r, err := en.vm.Render(ctx)
+ if err != nil {
+ return 0, err
+ }
+ if len(r) > 0 {
+ l, err = io.WriteString(w, r)
+ if err != nil {
+ return l, err
+ }
+ }
+ if len(en.exit) > 0 {
+ logg.TraceCtxf(ctx, "have exit", "exit", en.exit)
+ n, err := io.WriteString(w, en.exit)
+ if err != nil {
+ return l, err
+ }
+ l += n
+ }
+ return l, nil
+}
+
+// start execution over at top node while keeping current state of client error flags.
+func(en *DbEngine) reset(ctx context.Context) (bool, error) {
+ var err error
+ var isTop bool
+ for !isTop {
+ isTop, err = en.st.Top()
+ if err != nil {
+ return false, err
+ }
+ _, err = en.st.Up()
+ if err != nil {
+ return false, err
+ }
+ en.ca.Pop()
+ }
+ en.st.Restart()
+ en.initd = false
+ return false, nil
+}
+
diff --git a/engine/db_test.go b/engine/db_test.go
@@ -0,0 +1,52 @@
+package engine
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "git.defalsify.org/vise.git/resource"
+ "git.defalsify.org/vise.git/vm"
+)
+
+func codeGet(ctx context.Context, s string) ([]byte, error) {
+ var b []byte
+ var err error
+ switch s {
+ case "root":
+ b = vm.NewLine(nil, vm.HALT, nil, nil, nil)
+ b = vm.NewLine(b, vm.LOAD, []string{"foo"}, []byte{0x0}, nil)
+ default:
+ err = fmt.Errorf("unknown code symbol '%s'", s)
+ }
+ return b, err
+}
+
+func TestDbEngineMinimal(t *testing.T) {
+ ctx := context.Background()
+ cfg := Config{}
+ rs := resource.NewMenuResource()
+ en := NewDbEngine(cfg, rs)
+ cont, err := en.Init(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if cont {
+ t.Fatalf("expected not continue")
+ }
+}
+
+func TestDbEngineRoot(t *testing.T) {
+ ctx := context.Background()
+ cfg := Config{}
+ rs := resource.NewMenuResource()
+ rs.WithCodeGetter(codeGet)
+ en := NewDbEngine(cfg, rs)
+ cont, err := en.Init(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !cont {
+ t.Fatalf("expected continue")
+ }
+}
diff --git a/engine/engine.go b/engine/engine.go
@@ -85,7 +85,6 @@ func(en *Engine) Finish() error {
return nil
}
-
// change root to current state location if non-empty.
func(en *Engine) restore() {
location, _ := en.st.Where()
diff --git a/resource/db.go b/resource/db.go
@@ -146,3 +146,8 @@ func(g *DbResource) FuncFor(ctx context.Context, sym string) (EntryFunc, error)
}, nil
}, nil
}
+
+// Close implements the Resource interface.
+func(g *DbResource) Close() error {
+ return g.db.Close()
+}
diff --git a/resource/resource.go b/resource/resource.go
@@ -45,6 +45,10 @@ type Resource interface {
GetMenu(ctx context.Context, menuSym string) (string, error)
// FuncFor retrieves the external function (EntryFunc) associated with the given symbol.
FuncFor(ctx context.Context, loadSym string) (EntryFunc, error)
+ // Close implements the io.Closer interface.
+ //
+ // Safely shuts down retrieval backend.
+ Close() error
}
// MenuResource contains the base definition for building Resource implementations.
@@ -57,10 +61,24 @@ type MenuResource struct {
fns map[string]EntryFunc
}
+var (
+ noBinFunc = func(ctx context.Context, s string) ([]byte, error) {
+ logg.WarnCtxf(ctx, "no resource getter set!", "s", s)
+ return []byte{}, nil
+ }
+ noStrFunc = func(ctx context.Context, s string) (string, error) {
+ logg.WarnCtxf(ctx, "no resource getter set!", "s", s)
+ return "", nil
+ }
+)
+
// NewMenuResource creates a new MenuResource instance.
func NewMenuResource() *MenuResource {
rs := &MenuResource{}
rs.funcFunc = rs.FallbackFunc
+ rs.codeFunc = noBinFunc
+ rs.templateFunc = noStrFunc
+ rs.menuFunc = noStrFunc
return rs
}
@@ -124,3 +142,8 @@ func(m *MenuResource) FallbackFunc(ctx context.Context, sym string) (EntryFunc,
}
return fn, nil
}
+
+// Close implements the Resource interface.
+func(m *MenuResource) Close() error {
+ return nil
+}