go-vise

Constrained Size Output Virtual Machine
Info | Log | Files | Refs | README | LICENSE

commit 8879cbfba0a2e64da855ae597d98c09fa6432b47
parent ef9a4c3073e7e878846693d4aa7fd0cf1cc977a7
Author: lash <dev@holbrook.no>
Date:   Thu, 15 Aug 2024 15:39:24 +0100

Fix croak, add clean quit and add examples.

Add language example, swap mixed-up menu elements

State example

Remove superfluous dirty flag return

Fix broken CROAK asm parse, update example to use CROAK

Add croak asm test

Enable clean quit on empty code, fix menu properly

Move old state example

Add state debug dump feature, state example

Update debug setter for merged examples

Allow output on quit

Flush lastvalue on read

Add example to illustrate use of language in external function

Add gettext example

Diffstat:
Masm/asm.go | 53++++++++++++++++++++++++++++++++++++++++-------------
Masm/asm_test.go | 10++++++++++
Mcache/cache.go | 25+++++++++++++++++++++++++
Mcache/memory.go | 3+++
Aengine/debug.go | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mengine/engine.go | 39+++++++++++++++++++++++++++++++++++----
Mengine/engine_test.go | 4++--
Mengine/loop.go | 14++++++++------
Aexamples/languages/Makefile | 10++++++++++
Aexamples/languages/first | 3+++
Aexamples/languages/first.vis | 8++++++++
Aexamples/languages/first_nor | 3+++
Aexamples/languages/locale/nor/default.po | 14++++++++++++++
Aexamples/languages/main.go | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/languages/quit.vis | 2++
Aexamples/languages/root.vis | 3+++
Aexamples/languages/swap_menu | 1+
Aexamples/languages/swap_menu_nor | 1+
Aexamples/languages/swaplang.vis | 2++
Aexamples/quit/Makefile | 10++++++++++
Aexamples/quit/main.go | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/quit/out | 1+
Aexamples/quit/out.vis | 2++
Aexamples/quit/root | 1+
Aexamples/quit/root.vis | 2++
Mexamples/session/main.go | 4++--
Aexamples/state/Makefile | 10++++++++++
Aexamples/state/bar.vis | 2++
Aexamples/state/baz.vis | 2++
Aexamples/state/foo.vis | 2++
Aexamples/state/main.go | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/state/menu | 2++
Aexamples/state/menu.vis | 11+++++++++++
Aexamples/state/quit | 1+
Aexamples/state/quit.vis | 0
Aexamples/state/root.vis | 2++
Aexamples/state_passive/Makefile | 10++++++++++
Aexamples/state_passive/complete | 1+
Aexamples/state_passive/complete.vis | 3+++
Aexamples/state_passive/main.go | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/state_passive/root | 1+
Aexamples/state_passive/root.vis | 3+++
Mgo.mod | 6+++++-
Mgo.sum | 4++++
Mpersist/fs.go | 2+-
Mpersist/fs_test.go | 26++++++++++++++++++++++++++
Mresource/fs.go | 1+
Mstate/debug.go | 7++++++-
Mstate/debug_test.go | 3++-
Mstate/state.go | 5++---
Mtestdata/testdata.go | 22+++++++++++-----------
Mvm/runner.go | 5++---
Mvm/runner_test.go | 23++++++++++++-----------
53 files changed, 796 insertions(+), 59 deletions(-)

diff --git a/asm/asm.go b/asm/asm.go @@ -183,6 +183,25 @@ func parseSized(b *bytes.Buffer, arg Arg) (int, error) { return rn, nil } +func parseFlagged(b *bytes.Buffer, arg Arg) (int, error) { + var rn int + + n, err := writeSize(b, *arg.Size) + rn += n + if err != nil { + return rn, err + } + + n, err = b.Write([]byte{uint8(*arg.Flag)}) + rn += n + if err != nil { + return rn, err + } + + return rn, nil + +} + func parseOne(op vm.Opcode, instruction *Instruction, w io.Writer) (int, error) { a := instruction.OpArg var n_buf int @@ -226,25 +245,33 @@ func parseOne(op vm.Opcode, instruction *Instruction, w io.Writer) (int, error) // Catch CATCH, LOAD and twosyms with integer-as-string if a.Size != nil { log.Printf("have size %v", instruction) - if a.Flag != nil { - n, err := parseSig(b, a) - n_buf += n - if err != nil { - return n_out, err - } - } else if op == vm.LOAD { - n, err := parseSized(b, a) + if a.Sym == nil { + n, err := parseFlagged(b, a) n_buf += n if err != nil { return n_out, err } } else { - n, err := parseTwoSym(b, a) - n_buf += n - if err != nil { - return n_out, err - } + if a.Flag != nil { + n, err := parseSig(b, a) + n_buf += n + if err != nil { + return n_out, err + } + } else if op == vm.LOAD { + n, err := parseSized(b, a) + n_buf += n + if err != nil { + return n_out, err + } + } else { + n, err := parseTwoSym(b, a) + n_buf += n + if err != nil { + return n_out, err + } + } } return flush(b, w) } diff --git a/asm/asm_test.go b/asm/asm_test.go @@ -338,6 +338,16 @@ func TestParseSig(t *testing.T) { } } +func TestParseCroak(t *testing.T) { + b := bytes.NewBuffer(nil) + s := "CROAK 2 1\n" + Parse(s, b) + expect := vm.NewLine(nil, vm.CROAK, nil, []byte{0x02}, []uint8{0x1}) + if !bytes.Equal(b.Bytes(), expect) { + t.Fatalf("expected %x, got %x", expect, b) + } +} + func TestParseNoarg(t *testing.T) { var b []byte b = vm.NewLine(b, vm.HALT, nil, nil, nil) diff --git a/cache/cache.go b/cache/cache.go @@ -5,11 +5,13 @@ import ( ) // Cache stores loaded content, enforcing size limits and keeping track of size usage. +// TODO: hide values from client, while allowing cbor serialization type Cache struct { CacheSize uint32 // Total allowed cumulative size of values (not code) in cache CacheUseSize uint32 // Currently used bytes by all values (not code) in cache Cache []map[string]string // All loaded cache items Sizes map[string]uint16 // Size limits for all loaded symbols. + LastValue string // last inserted value } // NewCache creates a new ready-to-use cache object @@ -63,6 +65,7 @@ func(ca *Cache) Add(key string, value string, sizeLimit uint16) error { ca.Cache[len(ca.Cache)-1][key] = value ca.CacheUseSize += sz ca.Sizes[key] = sizeLimit + ca.LastValue = value return nil } @@ -168,6 +171,16 @@ func(ca *Cache) Check(key string) bool { return ca.frameOf(key) == -1 } +// Last returns the last inserted value +// +// The stored last inserter value will be reset to an empty string +// TODO: needs to be invalidated when out of scope +func(ca *Cache) Last() string { + s := ca.LastValue + ca.LastValue = "" + return s +} + // bytes that will be added to cache use size for string // returns 0 if capacity would be exceeded func(ca *Cache) checkCapacity(v string) uint32 { @@ -192,3 +205,15 @@ func(ca *Cache) frameOf(key string) int { } return -1 } + +func(ca *Cache) Levels() uint32 { + return uint32(len(ca.Cache)) +} + +func(ca *Cache) Keys(level uint32) []string { + var r []string + for k := range ca.Cache[level] { + r = append(r, k) + } + return r +} diff --git a/cache/memory.go b/cache/memory.go @@ -9,4 +9,7 @@ type Memory interface { Push() error Pop() error Reset() + Levels() uint32 + Keys(level uint32) []string + Last() string } diff --git a/engine/debug.go b/engine/debug.go @@ -0,0 +1,47 @@ +package engine + +import ( + "fmt" + "io" + "os" + + "git.defalsify.org/vise.git/cache" + "git.defalsify.org/vise.git/state" +) + +type Debug interface { + Break(*state.State, cache.Memory) +} + +type SimpleDebug struct { + pfx string + w io.Writer +} + +func NewSimpleDebug(w io.Writer) Debug { + if w == nil { + w = os.Stderr + } + return &SimpleDebug{ + w: w, + pfx: "DUMP>", + } +} + +func (dbg* SimpleDebug) Break(st *state.State, ca cache.Memory) { + fmt.Fprintf(dbg.w, "%s State:\n", dbg.pfx) + for _, s := range state.FlagDebugger.AsList(st.Flags, st.BitSize - 8) { + fmt.Fprintf(dbg.w, "%s\t%s\n", dbg.pfx, s) + } + for i := uint32(0); i < ca.Levels(); i++ { + fmt.Fprintf(dbg.w, "%s Cache[%d]:\n", dbg.pfx, i) + ks := ca.Keys(i) + for _, k := range ks { + v, err := ca.Get(k) + if err != nil { + continue + } + fmt.Fprintf(dbg.w, "%s\t%s: %v\n", dbg.pfx, k, v) + } + } +} diff --git a/engine/engine.go b/engine/engine.go @@ -36,9 +36,11 @@ type Engine struct { rs resource.Resource ca cache.Memory vm *vm.Vm + dbg Debug root string session string initd bool + exit string } // NewEngine creates a new Engine @@ -74,6 +76,10 @@ func NewEngine(ctx context.Context, cfg Config, st *state.State, rs resource.Res return engine } +func (en *Engine) SetDebugger(debugger Debug) { + en.dbg = debugger +} + // Finish implements EngineIsh interface func(en *Engine) Finish() error { Logg.Tracef("that's a wrap", "engine", en) @@ -87,7 +93,7 @@ func(en *Engine) restore() { return } if en.root != location { - Logg.Infof("restoring state: %s", location) + Logg.Infof("restoring state", "sym", location) en.root = "." } } @@ -106,11 +112,13 @@ func(en *Engine) Init(ctx context.Context) (bool, error) { if sym == "" { return false, fmt.Errorf("start sym empty") } + inSave, _ := en.st.GetInput() err := en.st.SetInput([]byte{}) if err != nil { return false, err } + 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) @@ -188,10 +196,18 @@ func(en *Engine) exec(ctx context.Context, input []byte) (bool, error) { en.st.SetCode(code) if len(code) == 0 { - Logg.Infof("runner finished with no remaining code") + 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 } @@ -202,6 +218,7 @@ func(en *Engine) exec(ctx context.Context, input []byte) (bool, error) { // - 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 *Engine) 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) } @@ -210,7 +227,21 @@ func(en *Engine) WriteResult(ctx context.Context, w io.Writer) (int, error) { if err != nil { return 0, err } - return io.WriteString(w, r) + 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. @@ -230,5 +261,5 @@ func(en *Engine) reset(ctx context.Context) (bool, error) { } en.st.Restart() en.initd = false - return en.Init(ctx) + return false, nil } diff --git a/engine/engine_test.go b/engine/engine_test.go @@ -209,8 +209,8 @@ func TestEngineResumeTerminated(t *testing.T) { } location, idx := st.Where() - if location != "root" { - t.Fatalf("expected 'root', got %s", location) + if location != "" { + t.Fatalf("expected '', got %s", location) } if idx != 0 { t.Fatalf("expected idx '0', got %v", idx) diff --git a/engine/loop.go b/engine/loop.go @@ -19,12 +19,13 @@ import ( // Rendered output is written to the provided writer. func Loop(ctx context.Context, en EngineIsh, reader io.Reader, writer io.Writer) error { defer en.Finish() - var err error - _, err = en.WriteResult(ctx, writer) + l, err := en.WriteResult(ctx, writer) if err != nil { return err } - writer.Write([]byte{0x0a}) + if l > 0 { + writer.Write([]byte{0x0a}) + } running := true bufReader := bufio.NewReader(reader) @@ -42,12 +43,13 @@ func Loop(ctx context.Context, en EngineIsh, reader io.Reader, writer io.Writer) if err != nil { return fmt.Errorf("unexpected termination: %v\n", err) } - _, err = en.WriteResult(ctx, writer) + l, err := en.WriteResult(ctx, writer) if err != nil { return err } - writer.Write([]byte{0x0a}) - + if l > 0 { + writer.Write([]byte{0x0a}) + } } return nil } diff --git a/examples/languages/Makefile b/examples/languages/Makefile @@ -0,0 +1,10 @@ +INPUTS = $(wildcard ./*.vis) +TXTS = $(wildcard ./*.txt.orig) + +%.vis: + go run ../../dev/asm $(basename $@).vis > $(basename $@).bin + +all: $(INPUTS) $(TXTS) + +%.txt.orig: + cp -v $(basename $@).orig $(basename $@) diff --git a/examples/languages/first b/examples/languages/first @@ -0,0 +1,3 @@ +{{.msg}} +{{.momsg}} +Change language diff --git a/examples/languages/first.vis b/examples/languages/first.vis @@ -0,0 +1,8 @@ +RELOAD msg +MAP msg +RELOAD momsg +MAP momsg +MOUT swap 0 +HALT +INCMP swaplang 0 +INCMP quit * diff --git a/examples/languages/first_nor b/examples/languages/first_nor @@ -0,0 +1,3 @@ +{{.msg}} +{{.momsg}} +Du ser norsk nĂ¥ diff --git a/examples/languages/locale/nor/default.po b/examples/languages/locale/nor/default.po @@ -0,0 +1,14 @@ +msgid "" +msgstr "" +"Project-Id-Version: GE vise language example\n" +"PO-Revision-Date: 2024-08-18 00:54:55\n" +"Last-Translator: Louis Holbrook\n" +"Language-Team: \n" +"Language: nor\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: vim\n" + +msgid "This message is translated using gettext" +msgstr "Denne meldingen er oversatt med gettext" diff --git a/examples/languages/main.go b/examples/languages/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "context" + "fmt" + "os" + "path" + + testdataloader "github.com/peteole/testdata-loader" + gotext "gopkg.in/leonelquinteros/gotext.v1" + + "git.defalsify.org/vise.git/cache" + "git.defalsify.org/vise.git/lang" + "git.defalsify.org/vise.git/persist" + "git.defalsify.org/vise.git/engine" + "git.defalsify.org/vise.git/resource" + "git.defalsify.org/vise.git/state" +) + +const ( + USERFLAG_FLIP = iota + state.FLAG_USERSTART +) + +var ( + baseDir = testdataloader.GetBasePath() + scriptDir = path.Join(baseDir, "examples", "languages") + translationDir = path.Join(scriptDir, "locale") +) + +func codeFromCtx(ctx context.Context) string { + var code string + engine.Logg.DebugCtxf(ctx, "in msg", "ctx", ctx, "val", code) + if ctx.Value("Language") != nil { + lang := ctx.Value("Language").(lang.Language) + code = lang.Code + } + return code +} + +type langController struct { + translations map[string]gotext.Locale + State *state.State +} + +func(l *langController) lang(ctx context.Context, sym string, input []byte) (resource.Result, error) { + lang := "nor" + var rs resource.Result + if l.State.MatchFlag(USERFLAG_FLIP, true) { + lang = "eng" + rs.FlagReset = append(rs.FlagReset, USERFLAG_FLIP) + } else { + rs.FlagSet = append(rs.FlagSet, USERFLAG_FLIP) + } + rs.Content = lang + rs.FlagSet = append(rs.FlagSet, state.FLAG_LANG) + return rs, nil +} + +func msg(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var r resource.Result + switch codeFromCtx(ctx) { + case "nor": + r.Content = "Denne meldingen er fra en ekstern funksjon" + default: + r.Content = "This message is from an external function" + } + return r, nil +} + +func(l *langController) moMsg(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var r resource.Result + code := codeFromCtx(ctx) + o := gotext.NewLocale(translationDir, code) + o.AddDomain("default") + r.Content = o.Get("This message is translated using gettext") + engine.Logg.DebugCtxf(ctx, "lang", "code", code, "translateor", o) + return r, nil +} + +func empty(ctx context.Context, sym string, input []byte) (resource.Result, error) { + return resource.Result{ + Content: "", + }, nil +} + +func main() { + st := state.NewState(1) + state.FlagDebugger.Register(USERFLAG_FLIP, "FLIP") + rs := resource.NewFsResource(scriptDir) + + ca := cache.NewCache() + cfg := engine.Config{ + Root: "root", + SessionId: "default", + } + ctx := context.Background() + + dp := path.Join(scriptDir, ".state") + err := os.MkdirAll(dp, 0700) + if err != nil { + engine.Logg.ErrorCtxf(ctx, "cannot create state dir", "err", err) + os.Exit(1) + } + pr := persist.NewFsPersister(dp) + en, err := engine.NewPersistedEngine(ctx, cfg, pr, rs) + if err != nil { + engine.Logg.Infof("persisted engine create error. trying again with persisting empty state first...") + pr = pr.WithContent(&st, ca) + err = pr.Save(cfg.SessionId) + if err != nil { + engine.Logg.ErrorCtxf(ctx, "fail state save: %v", err) + os.Exit(1) + } + en, err = engine.NewPersistedEngine(ctx, cfg, pr, rs) + } + pr.State.UseDebug() + + aux := &langController{ + State: pr.State, + } + rs.AddLocalFunc("swaplang", aux.lang) + rs.AddLocalFunc("msg", msg) + rs.AddLocalFunc("momsg", aux.moMsg) + rs.AddLocalFunc("empty", empty) + + _, err = en.Init(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "engine init fail: %v\n", err) + os.Exit(1) + } + + err = engine.Loop(ctx, &en, os.Stdin, os.Stdout) + if err != nil { + fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err) + os.Exit(1) + } +} diff --git a/examples/languages/quit.vis b/examples/languages/quit.vis @@ -0,0 +1,2 @@ +LOAD empty 0 +HALT diff --git a/examples/languages/root.vis b/examples/languages/root.vis @@ -0,0 +1,3 @@ +LOAD msg 50 +LOAD momsg 0 +MOVE first diff --git a/examples/languages/swap_menu b/examples/languages/swap_menu @@ -0,0 +1 @@ +swap language diff --git a/examples/languages/swap_menu_nor b/examples/languages/swap_menu_nor @@ -0,0 +1 @@ +bytt tilbake diff --git a/examples/languages/swaplang.vis b/examples/languages/swaplang.vis @@ -0,0 +1,2 @@ +LOAD swaplang 0 +MOVE ^ diff --git a/examples/quit/Makefile b/examples/quit/Makefile @@ -0,0 +1,10 @@ +INPUTS = $(wildcard ./*.vis) +TXTS = $(wildcard ./*.txt.orig) + +%.vis: + go run ../../dev/asm $(basename $@).vis > $(basename $@).bin + +all: $(INPUTS) $(TXTS) + +%.txt.orig: + cp -v $(basename $@).orig $(basename $@) diff --git a/examples/quit/main.go b/examples/quit/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "context" + "fmt" + "os" + "path" + + testdataloader "github.com/peteole/testdata-loader" + + "git.defalsify.org/vise.git/cache" + "git.defalsify.org/vise.git/engine" + "git.defalsify.org/vise.git/resource" + "git.defalsify.org/vise.git/state" +) + +var ( + baseDir = testdataloader.GetBasePath() + scriptDir = path.Join(baseDir, "examples", "quit") +) + +func quit(ctx context.Context, sym string, input []byte) (resource.Result, error) { + return resource.Result{ + Content: "quitter!", + }, nil +} + +func main() { + st := state.NewState(0) + st.UseDebug() + ca := cache.NewCache() + rs := resource.NewFsResource(scriptDir) + cfg := engine.Config{ + Root: "root", + } + ctx := context.Background() + en := engine.NewEngine(ctx, cfg, &st, rs, ca) + + rs.AddLocalFunc("quitcontent", quit) + + var err error + _, err = en.Init(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "engine init fail: %v\n", err) + os.Exit(1) + } + + err = engine.Loop(ctx, &en, os.Stdin, os.Stdout) + if err != nil { + fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err) + os.Exit(1) + } +} diff --git a/examples/quit/out b/examples/quit/out @@ -0,0 +1 @@ +foo bar baz diff --git a/examples/quit/out.vis b/examples/quit/out.vis @@ -0,0 +1,2 @@ +LOAD quitcontent 0 +HALT diff --git a/examples/quit/root b/examples/quit/root @@ -0,0 +1 @@ +press any key diff --git a/examples/quit/root.vis b/examples/quit/root.vis @@ -0,0 +1,2 @@ +HALT +MOVE out diff --git a/examples/session/main.go b/examples/session/main.go @@ -5,7 +5,6 @@ import ( "flag" "fmt" "io/ioutil" - "log" "os" "path" @@ -32,7 +31,7 @@ func save(ctx context.Context, sym string, input []byte) (resource.Result, error } fp := path.Join(sessionDir, "data.txt") if len(input) > 0 { - log.Printf("write data %s session %s", input, sessionId) + engine.Logg.Debugf("write data %s session %s", input, sessionId) err = ioutil.WriteFile(fp, input, 0600) if err != nil { return emptyResult, err @@ -61,6 +60,7 @@ func main() { fmt.Fprintf(os.Stderr, "starting session at symbol '%s' using resource dir: %s\n", root, scriptDir) st := state.NewState(0) + st.UseDebug() rs := resource.NewFsResource(scriptDir) rs.AddLocalFunc("do_save", save) ca := cache.NewCache() diff --git a/examples/state/Makefile b/examples/state/Makefile @@ -0,0 +1,10 @@ +INPUTS = $(wildcard ./*.vis) +TXTS = $(wildcard ./*.txt.orig) + +%.vis: + go run ../../dev/asm $(basename $@).vis > $(basename $@).bin + +all: $(INPUTS) $(TXTS) + +%.txt.orig: + cp -v $(basename $@).orig $(basename $@) diff --git a/examples/state/bar.vis b/examples/state/bar.vis @@ -0,0 +1,2 @@ +LOAD do_bar 0 +MOVE _ diff --git a/examples/state/baz.vis b/examples/state/baz.vis @@ -0,0 +1,2 @@ +LOAD do_baz 0 +MOVE _ diff --git a/examples/state/foo.vis b/examples/state/foo.vis @@ -0,0 +1,2 @@ +LOAD do_foo 0 +MOVE _ diff --git a/examples/state/main.go b/examples/state/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "context" + "fmt" + "os" + "path" + + testdataloader "github.com/peteole/testdata-loader" + + "git.defalsify.org/vise.git/cache" + "git.defalsify.org/vise.git/engine" + "git.defalsify.org/vise.git/resource" + "git.defalsify.org/vise.git/state" +) + +const ( + USER_FOO = iota + state.FLAG_USERSTART + USER_BAR + USER_BAZ +) + +var ( + baseDir = testdataloader.GetBasePath() + scriptDir = path.Join(baseDir, "examples", "state") +) + +type flagResource struct { + st *state.State +} + +func(f *flagResource) get(ctx context.Context, sym string, input []byte) (resource.Result, error) { + return resource.Result{ + Content: state.FlagDebugger.AsString(f.st.Flags, 3), + }, nil +} + + +func(f *flagResource) do(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var r resource.Result + + engine.Logg.DebugCtxf(ctx, "in do", "sym", sym) + + switch(sym) { + case "do_foo": + if f.st.MatchFlag(USER_FOO, false) { + r.FlagSet = append(r.FlagSet, USER_FOO) + } else { + r.FlagReset = append(r.FlagReset, USER_FOO) + } + case "do_bar": + if f.st.MatchFlag(USER_BAR, false) { + r.FlagSet = append(r.FlagSet, USER_BAR) + } else { + r.FlagReset = append(r.FlagReset, USER_BAR) + } + case "do_baz": + if f.st.MatchFlag(USER_BAZ, false) { + r.FlagSet = append(r.FlagSet, USER_BAZ) + } else { + r.FlagReset = append(r.FlagReset, USER_BAZ) + } + } + return r, nil +} + +func main() { + root := "root" + fmt.Fprintf(os.Stderr, "starting session at symbol '%s' using resource dir: %s\n", root, scriptDir) + + st := state.NewState(3) + st.UseDebug() + rs := resource.NewFsResource(scriptDir) + ca := cache.NewCache() + cfg := engine.Config{ + Root: "root", + } + ctx := context.Background() + en := engine.NewEngine(ctx, cfg, &st, rs, ca) + en.SetDebugger(engine.NewSimpleDebug(nil)) + + aux := &flagResource{st: &st} + rs.AddLocalFunc("do_foo", aux.do) + rs.AddLocalFunc("do_bar", aux.do) + rs.AddLocalFunc("do_baz", aux.do) + rs.AddLocalFunc("states", aux.get) + + state.FlagDebugger.Register(USER_FOO, "FOO") + state.FlagDebugger.Register(USER_BAR, "BAR") + state.FlagDebugger.Register(USER_BAZ, "BAZ") + var err error + _, err = en.Init(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "engine init fail: %v\n", err) + os.Exit(1) + } + err = engine.Loop(ctx, &en, os.Stdin, os.Stdout) + if err != nil { + fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err) + os.Exit(1) + } +} diff --git a/examples/state/menu b/examples/state/menu @@ -0,0 +1,2 @@ +choose state to toggle +now set: {{.states}} diff --git a/examples/state/menu.vis b/examples/state/menu.vis @@ -0,0 +1,11 @@ +RELOAD states +MAP states +MOUT foo 0 +MOUT bar 1 +MOUT baz 2 +MOUT this_sucks_i_quit 99 +HALT +INCMP foo 0 +INCMP bar 1 +INCMP baz 2 +INCMP quit * diff --git a/examples/state/quit b/examples/state/quit @@ -0,0 +1 @@ +bye diff --git a/examples/state/quit.vis b/examples/state/quit.vis diff --git a/examples/state/root.vis b/examples/state/root.vis @@ -0,0 +1,2 @@ +LOAD states 0 +MOVE menu diff --git a/examples/state_passive/Makefile b/examples/state_passive/Makefile @@ -0,0 +1,10 @@ +INPUTS = $(wildcard ./*.vis) +TXTS = $(wildcard ./*.txt.orig) + +%.vis: + go run ../../dev/asm $(basename $@).vis > $(basename $@).bin + +all: $(INPUTS) $(TXTS) + +%.txt.orig: + cp -v $(basename $@).orig $(basename $@) diff --git a/examples/state_passive/complete b/examples/state_passive/complete @@ -0,0 +1 @@ +yes: {{.peek}} diff --git a/examples/state_passive/complete.vis b/examples/state_passive/complete.vis @@ -0,0 +1,3 @@ +LOAD peek 0 +MAP peek +HALT diff --git a/examples/state_passive/main.go b/examples/state_passive/main.go @@ -0,0 +1,147 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io/ioutil" + "os" + "path" + + "git.defalsify.org/vise.git/resource" + "git.defalsify.org/vise.git/state" + "git.defalsify.org/vise.git/engine" + "git.defalsify.org/vise.git/cache" + "git.defalsify.org/vise.git/persist" +) + +const ( + USERFLAG_ONE = iota + state.FLAG_USERSTART + USERFLAG_TWO + USERFLAG_THREE + USERFLAG_DONE +) + +type fsData struct { + path string + persister persist.Persister +} + +func (fsd *fsData) peek(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + fp := fsd.path + "_data" + f, err := os.Open(fp) + if err != nil { + return res, err + } + r, err := ioutil.ReadAll(f) + if err != nil { + return res, err + } + res.Content = string(r) + return res, nil +} + +func (fsd *fsData) poke(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + fp := fsd.path + "_data" + f, err := os.OpenFile(fp, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return res, err + } + f.Write([]byte("*")) + f.Close() + + st := fsd.persister.GetState() + for i := 8; i < 12; i++ { + v := uint32(i) + engine.Logg.DebugCtxf(ctx, "checking flag", "flag", v) + if st.MatchFlag(v, true) { + engine.Logg.DebugCtxf(ctx, "match on flag", "flag", v) + res.FlagReset = append(res.FlagReset, v) + res.FlagSet = append(res.FlagSet, v + 1) + break + } + } + if len(res.FlagSet) == 0 { + res.FlagSet = append(res.FlagSet, 8) + } + return res, nil +} + +func main() { + var dir string + var root string + var size uint + var sessionId string + flag.StringVar(&dir, "d", ".", "resource dir to read from") + flag.UintVar(&size, "s", 0, "max size of output") + flag.StringVar(&root, "root", "root", "entry point symbol") + flag.StringVar(&sessionId, "session-id", "default", "session id") + flag.Parse() + fmt.Fprintf(os.Stderr, "starting session at symbol '%s' using resource dir: %s\n", root, dir) + + ctx := context.Background() + st := state.NewState(4) + rs := resource.NewFsResource(dir) + ca := cache.NewCache() + cfg := engine.Config{ + Root: "root", + SessionId: sessionId, + } + + dp := path.Join(dir, ".state") + + err := os.MkdirAll(dp, 0700) + if err != nil { + fmt.Fprintf(os.Stderr, "state dir create exited with error: %v\n", err) + os.Exit(1) + } + pr := persist.NewFsPersister(dp) + en, err := engine.NewPersistedEngine(ctx, cfg, pr, rs) + if err != nil { + pr = pr.WithContent(&st, ca) + err = pr.Save(cfg.SessionId) + en, err = engine.NewPersistedEngine(ctx, cfg, pr, rs) + if err != nil { + fmt.Fprintf(os.Stderr, "engine create exited with error: %v\n", err) + os.Exit(1) + } + } + + fp := path.Join(dp, sessionId) + aux := &fsData{ + path: fp, + persister: pr, + } + rs.AddLocalFunc("poke", aux.poke) + rs.AddLocalFunc("peek", aux.peek) + + cont, err := en.Init(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "engine init exited with error: %v\n", err) + os.Exit(1) + } + if !cont { + _, err = en.WriteResult(ctx, os.Stdout) + if err != nil { + fmt.Fprintf(os.Stderr, "dead init write error: %v\n", err) + os.Exit(1) + } + stnew := pr.GetState() + stnew.ResetFlag(state.FLAG_TERMINATE) + stnew.Up() + err = en.Finish() + if err != nil { + fmt.Fprintf(os.Stderr, "engine finish error: %v\n", err) + os.Exit(1) + } + os.Stdout.Write([]byte{0x0a}) + os.Exit(0) + } + err = engine.Loop(ctx, en, os.Stdin, os.Stdout) + if err != nil { + fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err) + os.Exit(1) + } +} diff --git a/examples/state_passive/root b/examples/state_passive/root @@ -0,0 +1 @@ +one more time! diff --git a/examples/state_passive/root.vis b/examples/state_passive/root.vis @@ -0,0 +1,3 @@ +CATCH complete 11 1 +LOAD poke 0 +CROAK 11 0 diff --git a/go.mod b/go.mod @@ -9,4 +9,8 @@ require ( github.com/peteole/testdata-loader v0.3.0 ) -require github.com/x448/float16 v0.8.4 // indirect +require ( + github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect + github.com/x448/float16 v0.8.4 // indirect + gopkg.in/leonelquinteros/gotext.v1 v1.3.1 // indirect +) diff --git a/go.sum b/go.sum @@ -7,7 +7,11 @@ github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c/go.mod h1:rGod7 github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY= +github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk= github.com/peteole/testdata-loader v0.3.0 h1:8jckE9KcyNHgyv/VPoaljvKZE0Rqr8+dPVYH6rfNr9I= github.com/peteole/testdata-loader v0.3.0/go.mod h1:Mt0ZbRtb56u8SLJpNP+BnQbENljMorYBpqlvt3cS83U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc= +gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU= diff --git a/persist/fs.go b/persist/fs.go @@ -67,7 +67,7 @@ func(p *FsPersister) Save(key string) error { return err } fp := path.Join(p.dir, key) - Logg.Debugf("saved state and cache", "key", key, "bytecode", p.State.Code) + Logg.Debugf("saved state and cache", "key", key, "bytecode", p.State.Code, "flags", p.State.Flags) return ioutil.WriteFile(fp, b, 0600) } diff --git a/persist/fs_test.go b/persist/fs_test.go @@ -102,3 +102,29 @@ func TestSaveLoad(t *testing.T) { t.Fatalf("expected %v, got %v", prnew.Memory, pr.Memory) } } + +func TestSaveLoadFlags(t *testing.T) { + st := state.NewState(2) + st.SetFlag(8) + ca := cache.NewCache() + + dir, err := ioutil.TempDir("", "vise_persist") + if err != nil { + t.Error(err) + } + pr := NewFsPersister(dir).WithContent(&st, ca) + err = pr.Save("xyzzy") + if err != nil { + t.Error(err) + } + + prnew := NewFsPersister(dir) + err = prnew.Load("xyzzy") + if err != nil { + t.Error(err) + } + stnew := prnew.GetState() + if !stnew.GetFlag(8) { + t.Fatalf("expected flag 8 set") + } +} diff --git a/resource/fs.go b/resource/fs.go @@ -70,6 +70,7 @@ func(fsr FsResource) GetMenu(ctx context.Context, sym string) (string, error) { fp := path.Join(fsr.Path, sym + "_menu") fpl := fp v := ctx.Value("Language") + Logg.DebugCtxf(ctx, "getmenu", "lang", v, "path", fp) if v != nil { lang := v.(lang.Language) fpl += "_" + lang.Code diff --git a/state/debug.go b/state/debug.go @@ -19,6 +19,7 @@ func newFlagDebugger() flagDebugger { fd.register(FLAG_DIRTY, "INTERNAL_DIRTY") fd.register(FLAG_WAIT, "INTERNAL_WAIT") fd.register(FLAG_LOADFAIL, "INTERNAL_LOADFAIL") + fd.register(FLAG_LANG, "INTERNAL_LANG") return fd } @@ -35,6 +36,10 @@ func(fd *flagDebugger) Register(flag uint32, name string) error { } func(fd *flagDebugger) AsString(flags []byte, length uint32) string { + return strings.Join(fd.AsList(flags, length), ",") +} + +func(fd *flagDebugger) AsList(flags []byte, length uint32) []string { var r []string var i uint32 for i = 0; i < length + 8; i++ { @@ -43,7 +48,7 @@ func(fd *flagDebugger) AsString(flags []byte, length uint32) string { r = append(r, s) } } - return strings.Join(r, ",") + return r } var ( diff --git a/state/debug_test.go b/state/debug_test.go @@ -38,7 +38,8 @@ func TestDebugState(t *testing.T) { if err != nil { t.Fatal(err) } - st := NewState(1).WithDebug() + st := NewState(1) + st.UseDebug() st.SetFlag(FLAG_DIRTY) st.SetFlag(8) st.Down("root") diff --git a/state/state.go b/state/state.go @@ -75,9 +75,8 @@ func NewState(BitSize uint32) State { return st } -func(st State) WithDebug() State { +func(st *State) UseDebug() { st.debug = true - return st } // SetFlag sets the flag at the given bit field index @@ -145,7 +144,7 @@ func(st *State) FlagByteSize() uint8 { // // The flag is specified given its bit index in the bit field. // -// If invertMatch is set, a positive result will be returned if the flag is not set. +// If matchSet is not set, a positive result will be returned if the flag is not set. func(st *State) MatchFlag(sig uint32, matchSet bool) bool { r := st.GetFlag(sig) return matchSet == r diff --git a/testdata/testdata.go b/testdata/testdata.go @@ -51,9 +51,9 @@ func out(sym string, b []byte, tpl string, data map[string]string) error { func root() error { b := []byte{} - b = vm.NewLine(b, vm.MOUT, []string{"1", "do the foo"}, nil, nil) - b = vm.NewLine(b, vm.MOUT, []string{"2", "go to the bar"}, nil, nil) - b = vm.NewLine(b, vm.MOUT, []string{"3", "language template"}, nil, nil) + b = vm.NewLine(b, vm.MOUT, []string{"do the foo", "1"}, nil, nil) + b = vm.NewLine(b, vm.MOUT, []string{"go to the bar", "2"}, nil, nil) + b = vm.NewLine(b, vm.MOUT, []string{"language template", "3"}, nil, nil) b = vm.NewLine(b, vm.HALT, nil, nil, nil) b = vm.NewLine(b, vm.INCMP, []string{"foo", "1"}, nil, nil) b = vm.NewLine(b, vm.INCMP, []string{"bar", "2"}, nil, nil) @@ -66,9 +66,9 @@ func root() error { func foo() error { b := []byte{} - b = vm.NewLine(b, vm.MOUT, []string{"0", "to foo"}, nil, nil) - b = vm.NewLine(b, vm.MOUT, []string{"1", "go bar"}, nil, nil) - b = vm.NewLine(b, vm.MOUT, []string{"2", "see long"}, nil, nil) + b = vm.NewLine(b, vm.MOUT, []string{"to foo", "0"}, nil, nil) + b = vm.NewLine(b, vm.MOUT, []string{"go bar", "1"}, nil, nil) + b = vm.NewLine(b, vm.MOUT, []string{"see long", "2"}, nil, nil) b = vm.NewLine(b, vm.LOAD, []string{"inky"}, []byte{20}, nil) b = vm.NewLine(b, vm.HALT, nil, nil, nil) b = vm.NewLine(b, vm.INCMP, []string{"_", "0"}, nil, nil) @@ -111,9 +111,9 @@ func baz() error { func long() error { b := []byte{} - b = vm.NewLine(b, vm.MOUT, []string{"0", "back"}, nil, nil) - b = vm.NewLine(b, vm.MNEXT, []string{"00", "nexxt"}, nil, nil) - b = vm.NewLine(b, vm.MPREV, []string{"11", "prevvv"}, nil, nil) + b = vm.NewLine(b, vm.MOUT, []string{"back", "0"}, nil, nil) + b = vm.NewLine(b, vm.MNEXT, []string{"nexxt", "00"}, nil, nil) + b = vm.NewLine(b, vm.MPREV, []string{"prevvv", "11"}, nil, nil) b = vm.NewLine(b, vm.LOAD, []string{"longdata"}, []byte{0x00}, nil) b = vm.NewLine(b, vm.MAP, []string{"longdata"}, nil, nil) b = vm.NewLine(b, vm.HALT, nil, nil, nil) @@ -140,7 +140,7 @@ POO 222 func defaultCatch() error { b := []byte{} - b = vm.NewLine(b, vm.MOUT, []string{"0", "back"}, nil, nil) + b = vm.NewLine(b, vm.MOUT, []string{"back", "0"}, nil, nil) b = vm.NewLine(b, vm.HALT, nil, nil, nil) b = vm.NewLine(b, vm.INCMP, []string{"_", "*"}, nil, nil) @@ -151,7 +151,7 @@ func defaultCatch() error { func lang() error { b := []byte{} - b = vm.NewLine(b, vm.MOUT, []string{"0", "back"}, nil, nil) + b = vm.NewLine(b, vm.MOUT, []string{"back", "0"}, nil, nil) b = vm.NewLine(b, vm.LOAD, []string{"inky"}, []byte{20}, nil) b = vm.NewLine(b, vm.MAP, []string{"inky"}, nil, nil) b = vm.NewLine(b, vm.HALT, nil, nil, nil) diff --git a/vm/runner.go b/vm/runner.go @@ -265,9 +265,8 @@ func(vm *Vm) runCroak(ctx context.Context, b []byte) ([]byte, error) { } r := vm.st.MatchFlag(sig, mode) if r { - Logg.InfoCtxf(ctx, "croak at flag %v, purging and moving to top", "signal", sig) + Logg.InfoCtxf(ctx, "croak! purging and moving to top", "signal", sig) vm.Reset() - vm.pg.Reset() vm.ca.Reset() b = []byte{} } @@ -419,7 +418,7 @@ func(vm *Vm) runMSink(ctx context.Context, b []byte) ([]byte, error) { // executes the MOUT opcode func(vm *Vm) runMOut(ctx context.Context, b []byte) ([]byte, error) { - choice, title, b, err := ParseMOut(b) + title, choice, b, err := ParseMOut(b) if err != nil { return b, err } diff --git a/vm/runner_test.go b/vm/runner_test.go @@ -48,16 +48,16 @@ func NewTestResource(st *state.State) TestResource { b = NewLine(nil, HALT, nil, nil, nil) tr.AddBytecode("one", b) - b = NewLine(nil, MOUT, []string{"0", "repent"}, nil, nil) + b = NewLine(nil, MOUT, []string{"repent", "0"}, nil, nil) b = NewLine(b, HALT, nil, nil, nil) tr.AddBytecode("_catch", b) - b = NewLine(nil, MOUT, []string{"0", "repent"}, nil, nil) + b = NewLine(nil, MOUT, []string{"repent", "0"}, nil, nil) b = NewLine(b, HALT, nil, nil, nil) b = NewLine(b, MOVE, []string{"_"}, nil, nil) tr.AddBytecode("flagCatch", b) - b = NewLine(nil, MOUT, []string{"1", "oo"}, nil, nil) + b = NewLine(nil, MOUT, []string{"oo", "1"}, nil, nil) b = NewLine(b, HALT, nil, nil, nil) tr.AddBytecode("ouf", b) @@ -155,16 +155,16 @@ func(r TestResource) getCode(sym string) ([]byte, error) { var b []byte switch sym { case "_catch": - b = NewLine(b, MOUT, []string{"0", "repent"}, nil, nil) + b = NewLine(b, MOUT, []string{"repent", "0"}, nil, nil) b = NewLine(b, HALT, nil, nil, nil) case "flagCatch": - b = NewLine(b, MOUT, []string{"0", "repent"}, nil, nil) + b = NewLine(b, MOUT, []string{"repent", "0"}, nil, nil) b = NewLine(b, HALT, nil, nil, nil) b = NewLine(b, MOVE, []string{"_"}, nil, nil) case "root": b = r.RootCode case "ouf": - b = NewLine(b, MOUT, []string{"1", "oo"}, nil, nil) + b = NewLine(b, MOUT, []string{"oo", "1"}, nil, nil) b = NewLine(b, HALT, nil, nil, nil) } @@ -420,8 +420,8 @@ func TestRunMenu(t *testing.T) { rs.AddBytecode("foo", []byte{}) b := NewLine(nil, MOVE, []string{"foo"}, nil, nil) - b = NewLine(b, MOUT, []string{"0", "one"}, nil, nil) - b = NewLine(b, MOUT, []string{"1", "two"}, nil, nil) + b = NewLine(b, MOUT, []string{"one", "0"}, nil, nil) + b = NewLine(b, MOUT, []string{"two", "1"}, nil, nil) b = NewLine(b, HALT, nil, nil, nil) b, err = vm.Run(ctx, b) @@ -456,8 +456,8 @@ func TestRunMenuBrowse(t *testing.T) { rs.AddBytecode("foo", []byte{}) b := NewLine(nil, MOVE, []string{"foo"}, nil, nil) - b = NewLine(b, MOUT, []string{"0", "one"}, nil, nil) - b = NewLine(b, MOUT, []string{"1", "two"}, nil, nil) + b = NewLine(b, MOUT, []string{"one", "0"}, nil, nil) + b = NewLine(b, MOUT, []string{"two", "1"}, nil, nil) b = NewLine(b, HALT, nil, nil, nil) b, err = vm.Run(ctx, b) @@ -703,7 +703,8 @@ func TestSetLang(t *testing.T) { } func TestLoadError(t *testing.T) { - st := state.NewState(0).WithDebug() + st := state.NewState(0) + st.UseDebug() rs := NewTestResource(&st) ca := cache.NewCache() vm := NewVm(&st, &rs, ca, nil)