go-vise

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

commit d396fd45d80568bbf80f4ab48724da30056ba6fb
parent 66c71be317bd578f223af0e808880417632926d9
Author: lash <dev@holbrook.no>
Date:   Sat, 31 Aug 2024 01:21:32 +0100

Merge branch 'lash/integrate-db' into dev-0.1.0

Diffstat:
M.gitignore | 3+++
Aasm/flag.go | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adb/db.go | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adb/error.go | 17+++++++++++++++++
Adb/fs.go | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adb/fs_test.go | 44++++++++++++++++++++++++++++++++++++++++++++
Adb/gdbm.go | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adb/gdbm_test.go | 45+++++++++++++++++++++++++++++++++++++++++++++
Adb/log.go | 9+++++++++
Adb/mem.go | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adb/mem_test.go | 39+++++++++++++++++++++++++++++++++++++++
Adb/pg.go | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adb/pg_test.go | 48++++++++++++++++++++++++++++++++++++++++++++++++
Mdev/asm/main.go | 135++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Adev/gdbm/main.go | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdev/interactive/main.go | 16+++++++++++++---
Mengine/default.go | 29+++++++++--------------------
Mengine/engine_test.go | 2+-
Mengine/persist.go | 6+++---
Mengine/persist_test.go | 41+++++++++++++++++------------------------
Aexamples/db/main.go | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/gdbm/Makefile | 11+++++++++++
Aexamples/gdbm/aiee.vis | 2++
Aexamples/gdbm/main.go | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/gdbm/menu | 2++
Aexamples/gdbm/menu.vis | 5+++++
Aexamples/gdbm/quit_menu | 2++
Aexamples/gdbm/root | 2++
Aexamples/gdbm/root.vis | 2++
Aexamples/gdbm/root_nor | 2++
Mexamples/languages/main.go | 10++++++----
Mexamples/longmenu/main.go | 11++++++++++-
Aexamples/preprocessor/Makefile | 10++++++++++
Aexamples/preprocessor/first | 1+
Aexamples/preprocessor/first.vis | 3+++
Aexamples/preprocessor/last | 1+
Aexamples/preprocessor/last.vis | 4++++
Aexamples/preprocessor/main.go | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/preprocessor/mid | 2++
Aexamples/preprocessor/mid.vis | 3+++
Aexamples/preprocessor/pp.csv | 3+++
Aexamples/preprocessor/root | 1+
Aexamples/preprocessor/root.vis | 5+++++
Mexamples/state_passive/main.go | 12+++++++-----
Mgo.mod | 10+++++++++-
Mgo.sum | 27+++++++++++++++++++++++++++
Dpersist/fs.go | 84-------------------------------------------------------------------------------
Mpersist/fs_test.go | 97+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Apersist/gdbm.go | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpersist/persist.go | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Aresource/db.go | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aresource/db_test.go | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mresource/fs.go | 15++++-----------
Aresource/gdbm.go | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mresource/mem.go | 2+-
Mresource/mem_test.go | 5+++--
Mresource/resource.go | 40+++++++++++++++++++++++++++++++---------
Mvm/runner.go | 18+++++++++---------
58 files changed, 2044 insertions(+), 233 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -3,3 +3,6 @@ examples/**/*.txt **/.state build/ doc/texinfo/**/*html +*.gdbm +.state +.store diff --git a/asm/flag.go b/asm/flag.go @@ -0,0 +1,124 @@ +package asm + +import ( + "encoding/csv" + "fmt" + "io" + "os" + "strconv" + + "git.defalsify.org/vise.git/state" +) + +// FlagParser is used to resolve flag strings to corresponding +// flag index integer values. +type FlagParser struct { + flag map[string]string + flagDescription map[uint32]string + hi uint32 +} + +// NewFlagParser creates a new FlagParser +func NewFlagParser() *FlagParser { + return &FlagParser{ + flag: make(map[string]string), + flagDescription: make(map[uint32]string), + } +} + +// GetFlag returns the flag index value for a given flag string +// as a numeric string. +// +// If flag string has not been registered, an error is returned. +func(pp *FlagParser) GetAsString(key string) (string, error) { + v, ok := pp.flag[key] + if !ok { + return "", fmt.Errorf("no flag registered under key: %s", key) + } + return v, nil +} + +// GetFlag returns the flag index integer value for a given +// flag string +// +// If flag string has not been registered, an error is returned. +func(pp *FlagParser) GetFlag(key string) (uint32, error) { + v, err := pp.GetAsString(key) + if err != nil { + return 0, err + } + r, err := strconv.Atoi(v) // cannot fail + return uint32(r), nil +} + +// GetDescription returns a flag description for a given flag index, +// if available. +// +// If no description has been provided, an error is returned. +func(pp *FlagParser) GetDescription(idx uint32) (string, error) { + v, ok := pp.flagDescription[idx] + if !ok { + return "", fmt.Errorf("no description for flag idx: %v", idx) + } + return v, nil +} + +// Last returns the highest registered flag index value +func(pp *FlagParser) Last() uint32 { + return pp.hi +} + +// Load parses a Comma Seperated Value file under the given filepath +// to provide mappings between flag strings and flag indices. +// +// The expected format is: +// +// Field 1: The literal string "flag" +// Field 2: Flag string +// Field 3: Flag index +// Field 4: Flag description (optional) +func(pp *FlagParser) Load(fp string) (int, error) { + var i int + f, err := os.Open(fp) + if err != nil { + return 0, err + } + defer f.Close() + r := csv.NewReader(f) + r.FieldsPerRecord = -1 + for i = 0; true; i++ { + v, err := r.Read() + if err != nil { + if err == io.EOF { + break + } + return 0, err + } + if v[0] == "flag" { + if len(v) < 3 { + return 0, fmt.Errorf("Not enough fields for flag setting in line %d", i) + } + vv, err := strconv.Atoi(v[2]) + if err != nil { + return 0, fmt.Errorf("Flag translation value must be numeric") + } + if vv < state.FLAG_USERSTART { + return 0, fmt.Errorf("Minimum flag value is FLAG_USERSTART (%d)", state.FLAG_USERSTART) + } + fl := uint32(vv) + pp.flag[v[1]] = v[2] + if fl > pp.hi { + pp.hi = fl + } + + if (len(v) > 3) { + pp.flagDescription[uint32(fl)] = v[3] + Logg.Debugf("added flag translation", "from", v[1], "to", v[2], "description", v[3]) + } else { + Logg.Debugf("added flag translation", "from", v[1], "to", v[2]) + } + } + } + + return i, nil +} diff --git a/db/db.go b/db/db.go @@ -0,0 +1,99 @@ +package db + +import ( + "context" + "errors" + + "git.defalsify.org/vise.git/lang" +) + +const ( + DATATYPE_UNKNOWN = 0 + DATATYPE_BIN = 1 + DATATYPE_MENU = 2 + DATATYPE_TEMPLATE = 4 + DATATYPE_STATE = 8 + DATATYPE_USERSTART = 16 +) + +const ( + datatype_sessioned_threshold = DATATYPE_TEMPLATE +) + +// Db abstracts all data storage and retrieval as a key-value store +type Db interface { + // Connect prepares the storage backend for use. May panic or error if called more than once. + Connect(ctx context.Context, connStr string) error + // Close implements io.Closer + Close() error + // Get retrieves the value belonging to a key. Errors if the key does not exist, or if the retrieval otherwise fails. + Get(ctx context.Context, key []byte) ([]byte, error) + // Put stores a value under a key. Any existing value will be replaced. Errors if the value could not be stored. + Put(ctx context.Context, key []byte, val []byte) error + // SetPrefix sets the storage context prefix to use for consecutive Get and Put operations. + SetPrefix(pfx uint8) + // SetSession sets the session context to use for consecutive Get and Put operations. + SetSession(sessionId string) +} + +// ToDbKey generates a key to use Db to store a value for a particular context. +// +// If language is nil, then default language storage context will be used. +// +// If language is not nil, and the context does not support language, the language value will silently will be ignored. +func ToDbKey(typ uint8, b []byte, l *lang.Language) []byte { + k := []byte{typ} + if l != nil && l.Code != "" { + k = append(k, []byte("_" + l.Code)...) + //s += "_" + l.Code + } + return append(k, b...) +} + +// baseDb is a base class for all Db implementations. +type baseDb struct { + pfx uint8 + sid []byte + lock uint8 +} + +func(db *baseDb) defaultLock() { + db.lock = DATATYPE_BIN | DATATYPE_MENU | DATATYPE_TEMPLATE +} + +// SetPrefix implements Db. +func(db *baseDb) SetPrefix(pfx uint8) { + db.pfx = pfx +} + +// SetSession implements Db. +func(db *baseDb) SetSession(sessionId string) { + db.sid = append([]byte(sessionId), 0x2E) +} + +// SetSafety disables modification of data that +func(db *baseDb) SetLock(pfx uint8, lock bool) { + if lock { + db.lock |= pfx + } else { + db.lock &= ^pfx + } +} + +func(db *baseDb) checkPut() bool { + return db.pfx & db.lock == 0 +} + +// ToKey creates a DbKey within the current session context. +func(db *baseDb) ToKey(key []byte) ([]byte, error) { + var b []byte + if db.pfx == DATATYPE_UNKNOWN { + return nil, errors.New("datatype prefix cannot be UNKNOWN") + } + if (db.pfx > datatype_sessioned_threshold) { + b = append(db.sid, key...) + } else { + b = key + } + return ToDbKey(db.pfx, b, nil), nil +} diff --git a/db/error.go b/db/error.go @@ -0,0 +1,17 @@ +package db + +import ( + "fmt" +) + +type ErrNotFound struct { + k []byte +} + +func NewErrNotFound(k []byte) error { + return ErrNotFound{k} +} + +func(e ErrNotFound) Error() string { + return fmt.Sprintf("key not found: %x", e.k) +} diff --git a/db/fs.go b/db/fs.go @@ -0,0 +1,80 @@ +package db + +import ( + "context" + "errors" + "io/ioutil" + "os" + "path" +) + +// pure filesystem backend implementation if the Db interface. +type fsDb struct { + baseDb + dir string +} + +// NewFsDb creates a filesystem backed Db implementation. +func NewFsDb() *fsDb { + db := &fsDb{} + db.baseDb.defaultLock() + return db +} + +// Connect implements Db +func(fdb *fsDb) Connect(ctx context.Context, connStr string) error { + if fdb.dir != "" { + panic("already connected") + } + err := os.MkdirAll(connStr, 0700) + if err != nil { + return err + } + fdb.dir = connStr + return nil +} + +// Get implements Db +func(fdb *fsDb) Get(ctx context.Context, key []byte) ([]byte, error) { + fp, err := fdb.pathFor(key) + if err != nil { + return nil, err + } + f, err := os.Open(fp) + if err != nil { + return nil, NewErrNotFound([]byte(fp)) + } + defer f.Close() + b, err := ioutil.ReadAll(f) + if err != nil { + return nil, err + } + return b, nil +} + +// Put implements Db +func(fdb *fsDb) Put(ctx context.Context, key []byte, val []byte) error { + if !fdb.checkPut() { + return errors.New("unsafe put and safety set") + } + fp, err := fdb.pathFor(key) + if err != nil { + return err + } + return ioutil.WriteFile(fp, val, 0600) +} + +// Close implements Db +func(fdb *fsDb) Close() error { + return nil +} + +// create a key safe for the filesystem +func(fdb *fsDb) pathFor(key []byte) (string, error) { + kb, err := fdb.ToKey(key) + if err != nil { + return "", err + } + kb[0] += 0x30 + return path.Join(fdb.dir, string(kb)), nil +} diff --git a/db/fs_test.go b/db/fs_test.go @@ -0,0 +1,44 @@ +package db + +import ( + "bytes" + "context" + "io/ioutil" + "testing" +) + +func TestPutGetFs(t *testing.T) { + var dbi Db + ctx := context.Background() + sid := "ses" + d, err := ioutil.TempDir("", "vise-db-*") + if err != nil { + t.Fatal(err) + } + db := NewFsDb() + db.SetPrefix(DATATYPE_USERSTART) + db.SetSession(sid) + + dbi = db + _ = dbi + + err = db.Connect(ctx, d) + if err != nil { + t.Fatal(err) + } + err = db.Put(ctx, []byte("foo"), []byte("bar")) + if err != nil { + t.Fatal(err) + } + v, err := db.Get(ctx, []byte("foo")) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(v, []byte("bar")) { + t.Fatalf("expected value 'bar', found '%s'", v) + } + _, err = db.Get(ctx, []byte("bar")) + if err == nil { + t.Fatal("expected get error for key 'bar'") + } +} diff --git a/db/gdbm.go b/db/gdbm.go @@ -0,0 +1,78 @@ +package db + +import ( + "context" + "errors" + "os" + + gdbm "github.com/graygnuorg/go-gdbm" +) + +// gdbmDb is a gdbm backend implementation of the Db interface. +type gdbmDb struct { + baseDb + conn *gdbm.Database + prefix uint8 +} + +func NewGdbmDb() *gdbmDb { + db := &gdbmDb{} + db.baseDb.defaultLock() + return db +} + +// Connect implements Db +func(gdb *gdbmDb) Connect(ctx context.Context, connStr string) error { + if gdb.conn != nil { + panic("already connected") + } + var db *gdbm.Database + _, err := os.Stat(connStr) + if err != nil { + if !errors.Is(os.ErrNotExist, err) { + return err + } + db, err = gdbm.Open(connStr, gdbm.ModeWrcreat) + } else { + db, err = gdbm.Open(connStr, gdbm.ModeWriter | gdbm.ModeReader) + } + + if err != nil { + return err + } + gdb.conn = db + return nil +} + +// Put implements Db +func(gdb *gdbmDb) Put(ctx context.Context, key []byte, val []byte) error { + if !gdb.checkPut() { + return errors.New("unsafe put and safety set") + } + k, err := gdb.ToKey(key) + if err != nil { + return err + } + return gdb.conn.Store(k, val, true) +} + +// Get implements Db +func(gdb *gdbmDb) Get(ctx context.Context, key []byte) ([]byte, error) { + k, err := gdb.ToKey(key) + if err != nil { + return nil, err + } + v, err := gdb.conn.Fetch(k) + if err != nil { + if errors.Is(gdbm.ErrItemNotFound, err) { + return nil, NewErrNotFound(k) + } + return nil, err + } + return v, nil +} + +// Close implements Db +func(gdb *gdbmDb) Close() error { + return gdb.Close() +} diff --git a/db/gdbm_test.go b/db/gdbm_test.go @@ -0,0 +1,45 @@ +package db + +import ( + "bytes" + "context" + "io/ioutil" + "testing" +) + +func TestPutGetGdbm(t *testing.T) { + var dbi Db + ctx := context.Background() + sid := "ses" + f, err := ioutil.TempFile("", "vise-db-*") + if err != nil { + t.Fatal(err) + } + db := NewGdbmDb() + db.SetPrefix(DATATYPE_USERSTART) + db.SetSession(sid) + + dbi = db + _ = dbi + + err = db.Connect(ctx, f.Name()) + if err != nil { + t.Fatal(err) + } + err = db.Put(ctx, []byte("foo"), []byte("bar")) + if err != nil { + t.Fatal(err) + } + v, err := db.Get(ctx, []byte("foo")) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(v, []byte("bar")) { + t.Fatalf("expected value 'bar', found '%s'", v) + } + _, err = db.Get(ctx, []byte("bar")) + if err == nil { + t.Fatal("expected get error for key 'bar'") + } + +} diff --git a/db/log.go b/db/log.go @@ -0,0 +1,9 @@ +package db + +import ( + "git.defalsify.org/vise.git/logging" +) + +var ( + Logg logging.Logger = logging.NewVanilla().WithDomain("db") +) diff --git a/db/mem.go b/db/mem.go @@ -0,0 +1,69 @@ +package db + +import ( + "context" + "encoding/hex" + "errors" +) + +// memDb is a memory backend implementation of the Db interface. +type memDb struct { + baseDb + store map[string][]byte +} + +// NewmemDb returns an already allocated memory backend (volatile) Db implementation. +func NewMemDb(ctx context.Context) *memDb { + db := &memDb{} + db.baseDb.defaultLock() + return db +} + +// Connect implements Db +func(mdb *memDb) Connect(ctx context.Context, connStr string) error { + if mdb.store != nil { + panic("already connected") + } + mdb.store = make(map[string][]byte) + return nil +} + +// convert to a supported map key type +func(mdb *memDb) toHexKey(key []byte) (string, error) { + k, err := mdb.ToKey(key) + return hex.EncodeToString(k), err +} + +// Get implements Db +func(mdb *memDb) Get(ctx context.Context, key []byte) ([]byte, error) { + k, err := mdb.toHexKey(key) + if err != nil { + return nil, err + } + Logg.TraceCtxf(ctx, "mem get", "k", k) + v, ok := mdb.store[k] + if !ok { + b, _ := hex.DecodeString(k) + return nil, NewErrNotFound(b) + } + return v, nil +} + +// Put implements Db +func(mdb *memDb) Put(ctx context.Context, key []byte, val []byte) error { + if !mdb.checkPut() { + return errors.New("unsafe put and safety set") + } + k, err := mdb.toHexKey(key) + if err != nil { + return err + } + mdb.store[k] = val + Logg.TraceCtxf(ctx, "mem put", "k", k, "v", val) + return nil +} + +// Close implements Db +func(mdb *memDb) Close() error { + return nil +} diff --git a/db/mem_test.go b/db/mem_test.go @@ -0,0 +1,39 @@ +package db + +import ( + "bytes" + "context" + "testing" +) + +func TestPutGetMem(t *testing.T) { + var dbi Db + ctx := context.Background() + sid := "ses" + db := NewMemDb(ctx) + db.SetPrefix(DATATYPE_USERSTART) + db.SetSession(sid) + + dbi = db + _ = dbi + + err := db.Connect(ctx, "") + if err != nil { + t.Fatal(err) + } + err = db.Put(ctx, []byte("foo"), []byte("bar")) + if err != nil { + t.Fatal(err) + } + v, err := db.Get(ctx, []byte("foo")) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(v, []byte("bar")) { + t.Fatalf("expected value 'bar', found '%s'", v) + } + _, err = db.Get(ctx, []byte("bar")) + if err == nil { + t.Fatal("expected get error for key 'bar'") + } +} diff --git a/db/pg.go b/db/pg.go @@ -0,0 +1,127 @@ +package db + +import ( + "context" + "errors" + "fmt" + + "github.com/jackc/pgx/v5/pgxpool" +) + +// pgDb is a Postgresql backend implementation of the Db interface. +type pgDb struct { + baseDb + conn *pgxpool.Pool + schema string + prefix uint8 +} + +// NewpgDb creates a new postgres backed Db implementation. +func NewPgDb() *pgDb { + db := &pgDb{ + schema: "public", + } + db.baseDb.defaultLock() + return db +} + +// WithSchema sets the Postgres schema to use for the storage table. +func(pdb *pgDb) WithSchema(schema string) *pgDb { + pdb.schema = schema + return pdb +} + +// Connect implements Db. +func(pdb *pgDb) Connect(ctx context.Context, connStr string) error { + if pdb.conn != nil { + panic("already connected") + } + var err error + conn, err := pgxpool.New(ctx, connStr) + if err != nil { + return err + } + pdb.conn = conn + return pdb.prepare(ctx) +} + +// Put implements Db. +func(pdb *pgDb) Put(ctx context.Context, key []byte, val []byte) error { + if !pdb.checkPut() { + return errors.New("unsafe put and safety set") + } + k, err := pdb.ToKey(key) + if err != nil { + return err + } + tx, err := pdb.conn.Begin(ctx) + if err != nil { + return err + } + query := fmt.Sprintf("INSERT INTO %s.kv_vise (key, value) VALUES ($1, $2) ON CONFLICT(key) DO UPDATE SET value = $2;", pdb.schema) + _, err = tx.Exec(ctx, query, k, val) + if err != nil { + tx.Rollback(ctx) + return err + } + tx.Commit(ctx) + return nil +} + +// Get implements Db. +func(pdb *pgDb) Get(ctx context.Context, key []byte) ([]byte, error) { + k, err := pdb.ToKey(key) + if err != nil { + return nil, err + } + tx, err := pdb.conn.Begin(ctx) + if err != nil { + return nil, err + } + query := fmt.Sprintf("SELECT value FROM %s.kv_vise WHERE key = $1", pdb.schema) + rs, err := tx.Query(ctx, query, k) + if err != nil { + return nil, err + } + defer rs.Close() + if !rs.Next() { + return nil, NewErrNotFound(k) + + } + r := rs.RawValues() + b := r[0] + return b, nil +} + +// Close implements Db. +func(pdb *pgDb) Close() error { + pdb.Close() + return nil +} + +// set up table +func(pdb *pgDb) prepare(ctx context.Context) error { + tx, err := pdb.conn.Begin(ctx) + if err != nil { + tx.Rollback(ctx) + return err + } + query := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s.kv_vise ( + id SERIAL NOT NULL, + key BYTEA NOT NULL UNIQUE, + value BYTEA NOT NULL + ); +`, pdb.schema) + _, err = tx.Exec(ctx, query) + if err != nil { + tx.Rollback(ctx) + return err + } + + err = tx.Commit(ctx) + if err != nil { + tx.Rollback(ctx) + return err + } + return nil +} diff --git a/db/pg_test.go b/db/pg_test.go @@ -0,0 +1,48 @@ +package db + +import ( + "bytes" + "context" + "testing" +) + +func TestPutGetPg(t *testing.T) { + var dbi Db + ses := "xyzzy" + db := NewPgDb().WithSchema("vvise") + db.SetPrefix(DATATYPE_USERSTART) + db.SetSession(ses) + ctx := context.Background() + + dbi = db + _ = dbi + + t.Skip("need postgresql mock") + err := db.Connect(ctx, "postgres://vise:esiv@localhost:5432/visedb") + if err != nil { + t.Fatal(err) + } + err = db.Put(ctx, []byte("foo"), []byte("bar")) + if err != nil { + t.Fatal(err) + } + b, err := db.Get(ctx, []byte("foo")) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(b, []byte("bar")) { + t.Fatalf("expected 'bar', got %x", b) + } + err = db.Put(ctx, []byte("foo"), []byte("plugh")) + if err != nil { + t.Fatal(err) + } + b, err = db.Get(ctx, []byte("foo")) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(b, []byte("plugh")) { + t.Fatalf("expected 'plugh', got %x", b) + } + +} diff --git a/dev/asm/main.go b/dev/asm/main.go @@ -1,27 +1,154 @@ package main import ( + "flag" "fmt" "io/ioutil" "log" "os" + "strconv" + "strings" + + "github.com/alecthomas/participle/v2" + "github.com/alecthomas/participle/v2/lexer" "git.defalsify.org/vise.git/asm" ) + +type arg struct { + One *string `(@Sym | @NumFirst)` + Two *string `((@Sym | @NumFirst) Whitespace?)?` + Three *string `((@Sym | @NumFirst) Whitespace?)?` +} + +type instruction struct { + OpCode string `@Ident` + OpArg arg `(Whitespace @@)?` + Comment string `Comment? EOL` +} + +type asmAsm struct { + Instructions []*instruction `@@*` +} + +type processor struct { + *asm.FlagParser + +} + +func newProcessor(fp string) (*processor, error) { + o := &processor{ + asm.NewFlagParser(), + } + _, err := o.Load(fp) + return o, err +} + + +func(p *processor) processFlag(s []string, one *string, two *string) ([]string, error) { + _, err := strconv.Atoi(*one) + if err != nil { + r, err := p.GetAsString(*one) + if err != nil { + return nil, err + } + log.Printf("translated flag %s to %s", *one, r) + s = append(s, r) + } else { + s = append(s, *one) + } + return append(s, *two), nil +} + +func(p *processor) pass(s []string, a arg) []string { + for _, r := range []*string{a.One, a.Two, a.Three} { + if r == nil { + break + } + s = append(s, *r) + } + return s +} + +func(pp *processor) run(b []byte) ([]byte, error) { + asmLexer := lexer.MustSimple([]lexer.SimpleRule{ + {"Comment", `(?:#)[^\n]*`}, + {"Ident", `^[A-Z]+`}, + {"NumFirst", `[0-9][a-zA-Z0-9]*`}, + {"Sym", `[a-zA-Z_\*\.\^\<\>][a-zA-Z0-9_]*`}, + {"Whitespace", `[ \t]+`}, + {"EOL", `[\n\r]+`}, + {"Quote", `["']`}, + }) + asmParser := participle.MustBuild[asmAsm]( + participle.Lexer(asmLexer), + participle.Elide("Comment", "Whitespace"), + ) + ast, err := asmParser.ParseString("preprocessor", string(b)) + if err != nil { + return nil, err + } + + b = []byte{} + for _, v := range ast.Instructions { + s := []string{v.OpCode} + if v.OpArg.One != nil { + switch v.OpCode { + case "CATCH": + s = append(s, *v.OpArg.One) + s, err = pp.processFlag(s, v.OpArg.Two, v.OpArg.Three) + if err != nil { + return nil, err + } + case "CROAK": + s, err = pp.processFlag(s, v.OpArg.One, v.OpArg.Two) + if err != nil { + return nil, err + } + default: + s = pp.pass(s, v.OpArg) + } + } + b = append(b, []byte(strings.Join(s, " "))...) + b = append(b, 0x0a) + } + + return b, nil +} + func main() { - if (len(os.Args) < 2) { + var ppfp string + flag.StringVar(&ppfp, "f", "", "preprocessor data to load") + flag.Parse() + if (len(flag.Args()) < 1) { os.Exit(1) } - fp := os.Args[1] + fp := flag.Arg(0) v, err := ioutil.ReadFile(fp) if err != nil { - fmt.Fprintf(os.Stderr, "read error: %v", err) + fmt.Fprintf(os.Stderr, "read error: %v\n", err) os.Exit(1) } + + if len(ppfp) > 0 { + pp, err := newProcessor(ppfp) + if err != nil { + fmt.Fprintf(os.Stderr, "preprocessor load error: %v\n", err) + os.Exit(1) + } + + v, err = pp.run(v) + if err != nil { + fmt.Fprintf(os.Stderr, "preprocess error: %v\n", err) + os.Exit(1) + } + } + log.Printf("preprocessor done") + n, err := asm.Parse(string(v), os.Stdout) if err != nil { - fmt.Fprintf(os.Stderr, "parse error: %v", err) + fmt.Fprintf(os.Stderr, "parse error: %v\n", err) os.Exit(1) } log.Printf("parsed total %v bytes", n) diff --git a/dev/gdbm/main.go b/dev/gdbm/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "flag" + "fmt" + "io" + "io/fs" + "log" + "os" + "path/filepath" + "path" + + gdbm "github.com/graygnuorg/go-gdbm" + + "git.defalsify.org/vise.git/db" +) + +var ( + binaryPrefix = ".bin" + templatePrefix = "" + scan = make(map[string]string) +) + + +type scanner struct { + db *gdbm.Database +} + +func newScanner(fp string) (*scanner, error) { + db, err := gdbm.Open(fp, gdbm.ModeNewdb) + if err != nil { + return nil, err + } + return &scanner{ + db: db, + }, nil +} + +func(sc *scanner) Close() error { + return sc.db.Close() +} + +func(sc *scanner) Scan(fp string, d fs.DirEntry, err error) error { + var typ uint8 + if err != nil { + return err + } + typ = db.DATATYPE_UNKNOWN + if d.IsDir() { + return nil + } + fx := path.Ext(fp) + fb := path.Base(fp) + switch fx { + case binaryPrefix: + typ = db.DATATYPE_BIN + case templatePrefix: + typ = db.DATATYPE_TEMPLATE + default: + log.Printf("skip foreign file: %s", fp) + return nil + } + f, err := os.Open(fp) + defer f.Close() + if err != nil{ + return err + } + v, err := io.ReadAll(f) + if err != nil{ + return err + } + + log.Printf("fx fb %s %s", fx, fb) + ft := fb[:len(fb)-len(fx)] + k := db.ToDbKey(typ, []byte(ft), nil) + err = sc.db.Store(k, v, true) + if err != nil { + return err + } + log.Printf("stored key %x for %s (%s)", k, fp, ft) + return nil +} + +func main() { + var dir string + var dbPath string + flag.StringVar(&dbPath, "d", "vise.gdbm", "database file path") + flag.Parse() + + dir = flag.Arg(0) + + o, err := newScanner(dbPath) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to open scanner") + os.Exit(1) + } + err = filepath.WalkDir(dir, o.Scan) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to open scanner") + os.Exit(1) + } +} diff --git a/dev/interactive/main.go b/dev/interactive/main.go @@ -7,24 +7,34 @@ import ( "os" "git.defalsify.org/vise.git/engine" + "git.defalsify.org/vise.git/db" ) func main() { + var store db.Db var dir string var root string var size uint var sessionId string - var persist bool + var persist 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.BoolVar(&persist, "persist", false, "use state persistence") + flag.StringVar(&persist, "p", "", "state persistence directory") flag.Parse() fmt.Fprintf(os.Stderr, "starting session at symbol '%s' using resource dir: %s\n", root, dir) ctx := context.Background() - en, err := engine.NewSizedEngine(dir, uint32(size), persist, &sessionId) + if persist != "" { + store = db.NewFsDb() + err := store.Connect(ctx, persist) + if err != nil { + fmt.Fprintf(os.Stderr, "db connect error: %v", err) + os.Exit(1) + } + } + en, err := engine.NewSizedEngine(dir, uint32(size), store, &sessionId) if err != nil { fmt.Fprintf(os.Stderr, "engine create error: %v", err) os.Exit(1) diff --git a/engine/default.go b/engine/default.go @@ -3,17 +3,16 @@ package engine import ( "context" "fmt" - "os" - "path" "git.defalsify.org/vise.git/cache" "git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/state" + "git.defalsify.org/vise.git/db" ) // NewDefaultEngine is a convenience function to instantiate a filesystem-backed engine with no output constraints. -func NewDefaultEngine(dir string, persisted bool, session *string) (EngineIsh, error) { +func NewDefaultEngine(dir string, persistDb db.Db, session *string) (EngineIsh, error) { var err error st := state.NewState(0) rs := resource.NewFsResource(dir) @@ -23,18 +22,13 @@ func NewDefaultEngine(dir string, persisted bool, session *string) (EngineIsh, e } if session != nil { cfg.SessionId = *session - } else if !persisted { + } else if persistDb != nil { return nil, fmt.Errorf("session must be set if persist is used") } ctx := context.TODO() var en EngineIsh - if persisted { - dp := path.Join(dir, ".state") - err = os.MkdirAll(dp, 0700) - if err != nil { - return nil, err - } - pr := persist.NewFsPersister(dp) + if persistDb != nil { + pr := persist.NewPersister(persistDb) en, err = NewPersistedEngine(ctx, cfg, pr, rs) if err != nil { Logg.Infof("persisted engine create error. trying again with persisting empty state first...") @@ -53,7 +47,7 @@ func NewDefaultEngine(dir string, persisted bool, session *string) (EngineIsh, e } // NewSizedEngine is a convenience function to instantiate a filesystem-backed engine with a specified output constraint. -func NewSizedEngine(dir string, size uint32, persisted bool, session *string) (EngineIsh, error) { +func NewSizedEngine(dir string, size uint32, persistDb db.Db, session *string) (EngineIsh, error) { var err error st := state.NewState(0) rs := resource.NewFsResource(dir) @@ -64,18 +58,13 @@ func NewSizedEngine(dir string, size uint32, persisted bool, session *string) (E } if session != nil { cfg.SessionId = *session - } else if !persisted { + } else if persistDb != nil { return nil, fmt.Errorf("session must be set if persist is used") } ctx := context.TODO() var en EngineIsh - if persisted { - dp := path.Join(dir, ".state") - err = os.MkdirAll(dp, 0700) - if err != nil { - return nil, err - } - pr := persist.NewFsPersister(dp) + if persistDb != nil { + pr := persist.NewPersister(persistDb) en, err = NewPersistedEngine(ctx, cfg, pr, rs) if err != nil { Logg.Infof("persisted engine create error. trying again with persisting empty state first...") diff --git a/engine/engine_test.go b/engine/engine_test.go @@ -82,7 +82,7 @@ func(fs FsWrapper) set_lang(ctx context.Context, sym string, input []byte) (reso }, nil } -func(fs FsWrapper) GetCode(sym string) ([]byte, error) { +func(fs FsWrapper) GetCode(ctx context.Context, sym string) ([]byte, error) { sym += ".bin" fp := path.Join(fs.Path, sym) r, err := ioutil.ReadFile(fp) diff --git a/engine/persist.go b/engine/persist.go @@ -11,12 +11,12 @@ import ( // PersistedEngine adds persisted state to the Engine object. It provides a persisted state option for synchronous/interactive clients. type PersistedEngine struct { *Engine - pr persist.Persister + pr *persist.Persister } // NewPersistedEngine creates a new PersistedEngine -func NewPersistedEngine(ctx context.Context, cfg Config, pr persist.Persister, rs resource.Resource) (PersistedEngine, error) { +func NewPersistedEngine(ctx context.Context, cfg Config, pr *persist.Persister, rs resource.Resource) (PersistedEngine, error) { err := pr.Load(cfg.SessionId) if err != nil { return PersistedEngine{}, err @@ -58,7 +58,7 @@ func(pe PersistedEngine) Finish() error { // initialized state actually is available for the identifier, otherwise the method will fail. // // It will also fail if execution by the underlying Engine fails. -func RunPersisted(cfg Config, rs resource.Resource, pr persist.Persister, input []byte, w io.Writer, ctx context.Context) error { +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 diff --git a/engine/persist_test.go b/engine/persist_test.go @@ -2,17 +2,18 @@ package engine import ( "context" - "io/ioutil" "os" "testing" "git.defalsify.org/vise.git/cache" "git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/state" + "git.defalsify.org/vise.git/db" ) func TestRunPersist(t *testing.T) { generateTestData(t) + ctx := context.Background() cfg := Config{ OutputSize: 83, SessionId: "xyzzy", @@ -20,28 +21,25 @@ func TestRunPersist(t *testing.T) { } rs := NewFsWrapper(dataDir, nil) - persistDir, err := ioutil.TempDir("", "vise_engine_persist") - if err != nil { - t.Fatal(err) - } - st := state.NewState(3) ca := cache.NewCache().WithCacheSize(1024) - pr := persist.NewFsPersister(persistDir).WithContent(&st, ca) + store := db.NewMemDb(context.Background()) + store.Connect(ctx, "") + pr := persist.NewPersister(store).WithContent(&st, ca) w := os.Stdout - ctx := context.TODO() + ctx = context.Background() st = state.NewState(cfg.FlagCount) ca = cache.NewCache() ca = ca.WithCacheSize(cfg.CacheSize) - pr = persist.NewFsPersister(persistDir).WithContent(&st, ca) - err = pr.Save(cfg.SessionId) + pr = persist.NewPersister(store).WithContent(&st, ca) + err := pr.Save(cfg.SessionId) if err != nil { t.Fatal(err) } - pr = persist.NewFsPersister(persistDir) + pr = persist.NewPersister(store) inputs := []string{ "", // trigger init, will not exec "1", @@ -55,7 +53,7 @@ func TestRunPersist(t *testing.T) { } } - pr = persist.NewFsPersister(persistDir) + pr = persist.NewPersister(store) err = pr.Load(cfg.SessionId) if err != nil { t.Fatal(err) @@ -73,6 +71,7 @@ func TestRunPersist(t *testing.T) { func TestEnginePersist(t *testing.T) { generateTestData(t) + ctx := context.Background() cfg := Config{ OutputSize: 83, SessionId: "xyzzy", @@ -80,23 +79,17 @@ func TestEnginePersist(t *testing.T) { } rs := NewFsWrapper(dataDir, nil) - persistDir, err := ioutil.TempDir("", "vise_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 := os.Stdout - ctx := context.TODO() + store := db.NewMemDb(context.Background()) + store.Connect(ctx, "") + pr := persist.NewPersister(store).WithContent(&st, ca) st = state.NewState(cfg.FlagCount) ca = cache.NewCache() ca = ca.WithCacheSize(cfg.CacheSize) - pr = persist.NewFsPersister(persistDir).WithContent(&st, ca) - err = pr.Save(cfg.SessionId) + pr = persist.NewPersister(store).WithContent(&st, ca) + err := pr.Save(cfg.SessionId) if err != nil { t.Fatal(err) } @@ -132,7 +125,7 @@ func TestEnginePersist(t *testing.T) { t.Fatalf("expected index '1', got %v", idx) } - pr = persist.NewFsPersister(persistDir) + pr = persist.NewPersister(store) en, err = NewPersistedEngine(ctx, cfg, pr, rs) if err != nil { t.Fatal(err) diff --git a/examples/db/main.go b/examples/db/main.go @@ -0,0 +1,164 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "os" + "path" + + testdataloader "github.com/peteole/testdata-loader" + + "git.defalsify.org/vise.git/asm" + "git.defalsify.org/vise.git/cache" + "git.defalsify.org/vise.git/engine" + "git.defalsify.org/vise.git/persist" + "git.defalsify.org/vise.git/resource" + "git.defalsify.org/vise.git/state" + "git.defalsify.org/vise.git/db" +) + +var ( + baseDir = testdataloader.GetBasePath() + scriptDir = path.Join(baseDir, "examples", "db") + store = db.NewFsDb() + pr = persist.NewPersister(store) + data_selector = []byte("my_data") +) + +func say(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var r resource.Result + store.SetPrefix(db.DATATYPE_USERSTART) + + st := pr.GetState() + if st.MatchFlag(state.FLAG_USERSTART, false) { + r.FlagSet = []uint32{8} + r.Content = "0" + return r, nil + } + if len(input) > 0 { + err := store.Put(ctx, data_selector, input) + if err != nil { + return r, err + } + } + + v, err := store.Get(ctx, data_selector) + if err != nil { + return r, err + } + + r.Content = string(v) + return r, nil +} + +func genCode(ctx context.Context, store db.Db) error { + b := bytes.NewBuffer(nil) + asm.Parse("LOAD say 0\n", b) + asm.Parse("MAP say\n", b) + asm.Parse("MOUT quit 0\n", b) + asm.Parse("HALT\n", b) + asm.Parse("INCMP argh 0\n", b) + asm.Parse("INCMP update *\n", b) + store.SetPrefix(db.DATATYPE_BIN) + err := store.Put(ctx, []byte("root"), b.Bytes()) + if err != nil { + return err + } + + b = bytes.NewBuffer(nil) + asm.Parse("HALT\n", b) + err = store.Put(ctx, []byte("argh"), b.Bytes()) + if err != nil { + return err + } + + b = bytes.NewBuffer(nil) + asm.Parse("RELOAD say\n", b) + asm.Parse("MOVE _\n", b) + err = store.Put(ctx, []byte("update"), b.Bytes()) + if err != nil { + return err + } + return nil +} + +func genMenu(ctx context.Context, store db.Db) error { + store.SetPrefix(db.DATATYPE_MENU) + return store.Put(ctx, []byte("quit"), []byte("give up")) +} + +func genTemplate(ctx context.Context, store db.Db) error { + store.SetPrefix(db.DATATYPE_TEMPLATE) + return store.Put(ctx, []byte("root"), []byte("current data is {{.say}}")) +} + +func main() { + ctx := context.Background() + root := "root" + fmt.Fprintf(os.Stderr, "starting session at symbol '%s' using resource dir: %s\n", root, scriptDir) + + dataDir := path.Join(scriptDir, ".store") + store.Connect(ctx, dataDir) + store.SetSession("xyzzy") + + store.SetLock(db.DATATYPE_TEMPLATE | db.DATATYPE_MENU | db.DATATYPE_BIN, false) + err := genCode(ctx, store) + if err != nil { + panic(err) + } + + err = genMenu(ctx, store) + if err != nil { + panic(err) + } + + err = genTemplate(ctx, store) + if err != nil { + panic(err) + } + store.SetLock(db.DATATYPE_TEMPLATE | db.DATATYPE_MENU | db.DATATYPE_BIN, true) + + tg, err := resource.NewDbFuncGetter(store, db.DATATYPE_TEMPLATE, db.DATATYPE_MENU, db.DATATYPE_BIN) + if err != nil { + panic(err) + } + rs := resource.NewMenuResource() + rs.WithTemplateGetter(tg.GetTemplate) + rs.WithMenuGetter(tg.GetMenu) + rs.WithCodeGetter(tg.GetCode) + rs.AddLocalFunc("say", say) + + ca := cache.NewCache() + if err != nil { + panic(err) + } + cfg := engine.Config{ + Root: "root", + } + + st := state.NewState(1) + 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", "err", err) + os.Exit(1) + } + en, err = engine.NewPersistedEngine(ctx, cfg, pr, rs) + } + + _, 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/gdbm/Makefile b/examples/gdbm/Makefile @@ -0,0 +1,11 @@ +INPUTS = $(wildcard ./*.vis) +TXTS = $(wildcard ./*.txt.orig) + +%.vis: + go run ../../dev/asm $(basename $@).vis > $(basename $@).bin + go run ../../dev/gdbm/main.go . + +all: $(INPUTS) $(TXTS) + +%.txt.orig: + cp -v $(basename $@).orig $(basename $@) diff --git a/examples/gdbm/aiee.vis b/examples/gdbm/aiee.vis @@ -0,0 +1,2 @@ +LOAD do 0 +HALT diff --git a/examples/gdbm/main.go b/examples/gdbm/main.go @@ -0,0 +1,76 @@ +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" + "git.defalsify.org/vise.git/db" +) + +var ( + baseDir = testdataloader.GetBasePath() + scriptDir = path.Join(baseDir, "examples", "gdbm") + dbFile = path.Join(scriptDir, "vise.gdbm") +) + +func do(ctx context.Context, sym string, input []byte) (resource.Result, error) { + return resource.Result{ + Content: "bye", + }, nil +} + +func main() { + ctx := context.Background() + root := "root" + fmt.Fprintf(os.Stderr, "starting session at symbol '%s' using resource dir: %s\n", root, scriptDir) + + st := state.NewState(0) + store := db.NewGdbmDb() + err := store.Connect(ctx, dbFile) + if err != nil { + panic(err) + } + + tg, err := resource.NewDbFuncGetter(store, db.DATATYPE_TEMPLATE, db.DATATYPE_BIN) + if err != nil { + panic(err) + } + rs := resource.NewMenuResource() + rs = rs.WithTemplateGetter(tg.GetTemplate) + rs = rs.WithCodeGetter(tg.GetCode) + + rsf := resource.NewFsResource(scriptDir) + rsf.AddLocalFunc("do", do) + rs = rs.WithMenuGetter(rsf.GetMenu) + rs = rs.WithEntryFuncGetter(rsf.FuncFor) + + ca := cache.NewCache() + if err != nil { + panic(err) + } + cfg := engine.Config{ + Root: "root", + Language: "nor", + } + en := engine.NewEngine(ctx, cfg, &st, rs, ca) + + + _, 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/gdbm/menu b/examples/gdbm/menu @@ -0,0 +1 @@ +welcome +\ No newline at end of file diff --git a/examples/gdbm/menu.vis b/examples/gdbm/menu.vis @@ -0,0 +1,5 @@ +MOUT again 0 +MOUT quit 1 +HALT +INCMP ^ 0 +INCMP aiee 1 diff --git a/examples/gdbm/quit_menu b/examples/gdbm/quit_menu @@ -0,0 +1 @@ +or quit +\ No newline at end of file diff --git a/examples/gdbm/root b/examples/gdbm/root @@ -0,0 +1 @@ +ready? +\ No newline at end of file diff --git a/examples/gdbm/root.vis b/examples/gdbm/root.vis @@ -0,0 +1,2 @@ +HALT +INCMP menu * diff --git a/examples/gdbm/root_nor b/examples/gdbm/root_nor @@ -0,0 +1 @@ +klar? +\ No newline at end of file diff --git a/examples/languages/main.go b/examples/languages/main.go @@ -15,6 +15,7 @@ import ( "git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/state" + "git.defalsify.org/vise.git/db" ) const ( @@ -96,19 +97,20 @@ func main() { ctx := context.Background() dp := path.Join(scriptDir, ".state") - err := os.MkdirAll(dp, 0700) + store := db.NewFsDb() + err := store.Connect(ctx, dp) if err != nil { - engine.Logg.ErrorCtxf(ctx, "cannot create state dir", "err", err) + engine.Logg.ErrorCtxf(ctx, "db connect fail", "err", err) os.Exit(1) } - pr := persist.NewFsPersister(dp) + pr := persist.NewPersister(store) 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) + engine.Logg.ErrorCtxf(ctx, "fail state save", "err", err) os.Exit(1) } en, err = engine.NewPersistedEngine(ctx, cfg, pr, rs) diff --git a/examples/longmenu/main.go b/examples/longmenu/main.go @@ -10,6 +10,7 @@ import ( testdataloader "github.com/peteole/testdata-loader" "git.defalsify.org/vise.git/engine" + "git.defalsify.org/vise.git/db" ) var ( baseDir = testdataloader.GetBasePath() @@ -31,7 +32,15 @@ func main() { fmt.Fprintf(os.Stderr, "starting session at symbol '%s' using resource dir: %s\n", root, dir) ctx := context.Background() - en, err := engine.NewSizedEngine(dir, uint32(size), persist, &sessionId) + dp := path.Join(scriptDir, ".state") + store := db.NewFsDb() + err := store.Connect(ctx, dp) + if err != nil { + fmt.Fprintf(os.Stderr, "db connect error: %v", err) + os.Exit(1) + } + defer store.Close() + en, err := engine.NewSizedEngine(dir, uint32(size), store, &sessionId) if err != nil { fmt.Fprintf(os.Stderr, "engine create error: %v", err) os.Exit(1) diff --git a/examples/preprocessor/Makefile b/examples/preprocessor/Makefile @@ -0,0 +1,10 @@ +INPUTS = $(wildcard ./*.vis) +TXTS = $(wildcard ./*.txt.orig) + +%.vis: + go run ../../dev/asm -f pp.csv $(basename $@).vis > $(basename $@).bin + +all: $(INPUTS) $(TXTS) + +%.txt.orig: + cp -v $(basename $@).orig $(basename $@) diff --git a/examples/preprocessor/first b/examples/preprocessor/first @@ -0,0 +1 @@ +this is the first page diff --git a/examples/preprocessor/first.vis b/examples/preprocessor/first.vis @@ -0,0 +1,3 @@ +LOAD flag_foo 0 +HALT +INCMP ^ * diff --git a/examples/preprocessor/last b/examples/preprocessor/last @@ -0,0 +1 @@ +this is the last page diff --git a/examples/preprocessor/last.vis b/examples/preprocessor/last.vis @@ -0,0 +1,4 @@ +LOAD flag_bar 0 +HALT +RELOAD flag_schmag +INCMP ^ * diff --git a/examples/preprocessor/main.go b/examples/preprocessor/main.go @@ -0,0 +1,101 @@ +package main + +import ( + "context" + "fmt" + "os" + "path" + "strings" + + testdataloader "github.com/peteole/testdata-loader" + + "git.defalsify.org/vise.git/asm" + "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", "preprocessor") +) + +type countResource struct { + parser *asm.FlagParser + count int +} + +func newCountResource(fp string) (*countResource, error) { + var err error + pfp := path.Join(fp, "pp.csv") + parser := asm.NewFlagParser() + _, err = parser.Load(pfp) + if err != nil { + return nil, err + } + return &countResource{ + count: 0, + parser: parser, + }, nil +} + +func(rsc* countResource) poke(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var r resource.Result + + ss := strings.Split(sym, "_") + + r.Content = "You will see this if this flag did not have a description" + r.FlagReset = []uint32{8, 9, 10} + v, err := rsc.parser.GetFlag(ss[1]) + if err != nil { + v = 8 + uint32(rsc.count) + 1 + r.FlagSet = []uint32{8 + uint32(rsc.count) + 1} + } + r.FlagSet = []uint32{uint32(v)} + s, err := rsc.parser.GetDescription(v) + if err == nil { + r.Content = s + } + + rsc.count++ + + 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(5) + st.UseDebug() + rsf := resource.NewFsResource(scriptDir) + rs, err := newCountResource(scriptDir) + if err != nil { + fmt.Fprintf(os.Stderr, "aux handler fail: %v\n", err) + os.Exit(1) + } + rsf.AddLocalFunc("flag_foo", rs.poke) + rsf.AddLocalFunc("flag_bar", rs.poke) + rsf.AddLocalFunc("flag_schmag", rs.poke) + rsf.AddLocalFunc("flag_start", rs.poke) + ca := cache.NewCache() + cfg := engine.Config{ + Root: "root", + } + ctx := context.Background() + en := engine.NewEngine(ctx, cfg, &st, rsf, ca) + + _, 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/preprocessor/mid b/examples/preprocessor/mid @@ -0,0 +1,2 @@ +this is the middle page +{{.flag_schmag}} diff --git a/examples/preprocessor/mid.vis b/examples/preprocessor/mid.vis @@ -0,0 +1,3 @@ +MAP flag_schmag +HALT +MOVE ^ diff --git a/examples/preprocessor/pp.csv b/examples/preprocessor/pp.csv @@ -0,0 +1,3 @@ +flag,foo,8 +flag,bar,10,and this is the description of the flag 'bar' +flag,baz,12 diff --git a/examples/preprocessor/root b/examples/preprocessor/root @@ -0,0 +1 @@ +that's it diff --git a/examples/preprocessor/root.vis b/examples/preprocessor/root.vis @@ -0,0 +1,5 @@ +CROAK baz 1 +CATCH last bar 1 +CATCH first foo 0 +LOAD flag_schmag 0 +MOVE mid diff --git a/examples/state_passive/main.go b/examples/state_passive/main.go @@ -13,6 +13,7 @@ import ( "git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/cache" "git.defalsify.org/vise.git/persist" + "git.defalsify.org/vise.git/db" ) const ( @@ -24,7 +25,7 @@ const ( type fsData struct { path string - persister persist.Persister + persister *persist.Persister } func (fsd *fsData) peek(ctx context.Context, sym string, input []byte) (resource.Result, error) { @@ -91,14 +92,15 @@ func main() { } dp := path.Join(dir, ".state") - - err := os.MkdirAll(dp, 0700) + store := db.NewFsDb() + err := store.Connect(ctx, dp) if err != nil { - fmt.Fprintf(os.Stderr, "state dir create exited with error: %v\n", err) + fmt.Fprintf(os.Stderr, "db connect fail: %s", err) os.Exit(1) } - pr := persist.NewFsPersister(dp) + pr := persist.NewPersister(store) en, err := engine.NewPersistedEngine(ctx, cfg, pr, rs) + if err != nil { pr = pr.WithContent(&st, ca) err = pr.Save(cfg.SessionId) diff --git a/go.mod b/go.mod @@ -6,11 +6,19 @@ require ( github.com/alecthomas/participle/v2 v2.0.0 github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c github.com/fxamacker/cbor/v2 v2.4.0 + github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 + github.com/jackc/pgx/v5 v5.6.0 github.com/peteole/testdata-loader v0.3.0 + gopkg.in/leonelquinteros/gotext.v1 v1.3.1 ) require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect 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 + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum @@ -4,14 +4,41 @@ github.com/alecthomas/participle/v2 v2.0.0/go.mod h1:rAKZdJldHu8084ojcWevWAL8KmE github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE= github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c/go.mod h1:rGod7o6KPeJ+hyBpHfhi4v7blx9sf+QsHsA7KAsdN6U= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo= +github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc= gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/persist/fs.go b/persist/fs.go @@ -1,84 +0,0 @@ -package persist - -import ( - "io/ioutil" - "path" - "path/filepath" - "github.com/fxamacker/cbor/v2" - - "git.defalsify.org/vise.git/cache" - "git.defalsify.org/vise.git/state" -) - -// FsPersister is an implementation of Persister that saves state to the file system. -type FsPersister struct { - State *state.State - Memory *cache.Cache - dir string -} - -// NewFsPersister creates a new FsPersister. -// -// The filesystem store will be at the given directory. The directory must exist. -func NewFsPersister(dir string) *FsPersister { - fp, err := filepath.Abs(dir) - if err != nil { - panic(err) - } - return &FsPersister{ - dir: fp, - } -} - -// WithContent sets a current State and Cache object. -// -// This method is normally called before Serialize / Save. -func(p *FsPersister) WithContent(st *state.State, ca *cache.Cache) *FsPersister { - p.State = st - p.Memory = ca - return p -} - -// GetState implements the Persister interface. -func(p *FsPersister) GetState() *state.State { - return p.State -} - -// GetMemory implements the Persister interface. -func(p *FsPersister) GetMemory() cache.Memory { - return p.Memory -} - -// Serialize implements the Persister interface. -func(p *FsPersister) Serialize() ([]byte, error) { - return cbor.Marshal(p) -} - -// Deserialize implements the Persister interface. -func(p *FsPersister) Deserialize(b []byte) error { - err := cbor.Unmarshal(b, p) - return err -} - -// Save implements the Persister interface. -func(p *FsPersister) Save(key string) error { - b, err := p.Serialize() - if err != nil { - return err - } - fp := path.Join(p.dir, key) - Logg.Debugf("saved state and cache", "key", key, "bytecode", p.State.Code, "flags", p.State.Flags) - return ioutil.WriteFile(fp, b, 0600) -} - -// Load implements the Persister interface. -func(p *FsPersister) Load(key string) error { - fp := path.Join(p.dir, key) - b, err := ioutil.ReadFile(fp) - if err != nil { - return err - } - err = p.Deserialize(b) - Logg.Debugf("loaded state and cache", "key", key, "bytecode", p.State.Code) - return err -} diff --git a/persist/fs_test.go b/persist/fs_test.go @@ -2,13 +2,14 @@ package persist import ( "bytes" - "io/ioutil" + "context" "reflect" "testing" "git.defalsify.org/vise.git/cache" "git.defalsify.org/vise.git/state" "git.defalsify.org/vise.git/vm" + "git.defalsify.org/vise.git/db" ) func TestSerializeState(t *testing.T) { @@ -27,31 +28,39 @@ func TestSerializeState(t *testing.T) { ca.Add("inky", "pinky", 13) ca.Add("blinky", "clyde", 42) - pr := NewFsPersister(".").WithContent(&st, ca) + ctx := context.Background() + store := db.NewMemDb(ctx) + store.Connect(ctx, "") + pr := NewPersister(store).WithContext(context.Background()).WithSession("xyzzy").WithContent(&st, ca) v, err := pr.Serialize() if err != nil { t.Error(err) } - prnew := NewFsPersister(".") + prnew := NewPersister(store).WithSession("xyzzy") err = prnew.Deserialize(v) if err != nil { t.Fatal(err) - } - if !reflect.DeepEqual(prnew.State.ExecPath, pr.State.ExecPath) { - t.Fatalf("expected %s, got %s", prnew.State.ExecPath, pr.State.ExecPath) } - if !bytes.Equal(prnew.State.Code, pr.State.Code) { - t.Fatalf("expected %x, got %x", prnew.State.Code, pr.State.Code) + stNew := prnew.GetState() + stOld := pr.GetState() + caNew := prnew.GetMemory() + caOld := pr.GetMemory() + + if !reflect.DeepEqual(stNew.ExecPath, stOld.ExecPath) { + t.Fatalf("expected %s, got %s", stNew.ExecPath, stOld.ExecPath) + } + if !bytes.Equal(stNew.Code, stOld.Code) { + t.Fatalf("expected %x, got %x", stNew.Code, stOld.Code) } - if prnew.State.BitSize != pr.State.BitSize { - t.Fatalf("expected %v, got %v", prnew.State.BitSize, pr.State.BitSize) + if stNew.BitSize != stOld.BitSize { + t.Fatalf("expected %v, got %v", stNew.BitSize, stOld.BitSize) } - if prnew.State.SizeIdx != pr.State.SizeIdx { - t.Fatalf("expected %v, got %v", prnew.State.SizeIdx, pr.State.SizeIdx) + if stNew.SizeIdx != stOld.SizeIdx { + t.Fatalf("expected %v, got %v", stNew.SizeIdx, stOld.SizeIdx) } - if !reflect.DeepEqual(prnew.Memory, pr.Memory) { - t.Fatalf("expected %v, got %v", prnew.Memory, pr.Memory) + if !reflect.DeepEqual(caNew, caOld) { + t.Fatalf("expected %v, got %v", caNew, caOld) } } @@ -71,57 +80,61 @@ func TestSaveLoad(t *testing.T) { ca.Add("inky", "pinky", 13) ca.Add("blinky", "clyde", 42) - dir, err := ioutil.TempDir("", "vise_persist") - if err != nil { - t.Error(err) - } - pr := NewFsPersister(dir).WithContent(&st, ca) - err = pr.Save("xyzzy") + ctx := context.Background() + store := db.NewMemDb(ctx) + store.Connect(ctx, "") + pr := NewPersister(store).WithContent(&st, ca) + err := pr.Save("xyzzy") if err != nil { - t.Error(err) + t.Fatal(err) } - prnew := NewFsPersister(dir) + prnew := NewPersister(store) err = prnew.Load("xyzzy") if err != nil { - t.Error(err) + t.Fatal(err) } - if !reflect.DeepEqual(prnew.State.ExecPath, pr.State.ExecPath) { - t.Fatalf("expected %s, got %s", prnew.State.ExecPath, pr.State.ExecPath) + stNew := prnew.GetState() + stOld := pr.GetState() + caNew := prnew.GetMemory() + caOld := pr.GetMemory() + + if !reflect.DeepEqual(stNew.ExecPath, stOld.ExecPath) { + t.Fatalf("expected %s, got %s", stNew.ExecPath, stOld.ExecPath) } - if !bytes.Equal(prnew.State.Code, pr.State.Code) { - t.Fatalf("expected %x, got %x", prnew.State.Code, pr.State.Code) + if !bytes.Equal(stNew.Code, stOld.Code) { + t.Fatalf("expected %x, got %x", stNew.Code, stOld.Code) } - if prnew.State.BitSize != pr.State.BitSize { - t.Fatalf("expected %v, got %v", prnew.State.BitSize, pr.State.BitSize) + if stNew.BitSize != stOld.BitSize { + t.Fatalf("expected %v, got %v", stNew.BitSize, stOld.BitSize) } - if prnew.State.SizeIdx != pr.State.SizeIdx { - t.Fatalf("expected %v, got %v", prnew.State.SizeIdx, pr.State.SizeIdx) + if stNew.SizeIdx != stOld.SizeIdx { + t.Fatalf("expected %v, got %v", stNew.SizeIdx, stOld.SizeIdx) } - if !reflect.DeepEqual(prnew.Memory, pr.Memory) { - t.Fatalf("expected %v, got %v", prnew.Memory, pr.Memory) + if !reflect.DeepEqual(caNew, caOld) { + t.Fatalf("expected %v, got %v", caNew, caOld) } } func TestSaveLoadFlags(t *testing.T) { + ctx := context.Background() 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") + store := db.NewMemDb(ctx) + store.Connect(ctx, "") + pr := NewPersister(store).WithContent(&st, ca) + err := pr.Save("xyzzy") if err != nil { - t.Error(err) + t.Fatal(err) } - prnew := NewFsPersister(dir) + prnew := NewPersister(store) + err = prnew.Load("xyzzy") if err != nil { - t.Error(err) + t.Fatal(err) } stnew := prnew.GetState() if !stnew.GetFlag(8) { diff --git a/persist/gdbm.go b/persist/gdbm.go @@ -0,0 +1,92 @@ +package persist + +import ( + "github.com/fxamacker/cbor/v2" + gdbm "github.com/graygnuorg/go-gdbm" + + "git.defalsify.org/vise.git/cache" + "git.defalsify.org/vise.git/state" + "git.defalsify.org/vise.git/db" +) + +// gdbmPersister is an implementation of Persister that saves state to the file system. +type gdbmPersister struct { + State *state.State + Memory *cache.Cache + db *gdbm.Database +} + +func NewGdbmPersiser(fp string) *gdbmPersister { + gdb, err := gdbm.Open(fp, gdbm.ModeReader) + if err != nil { + panic(err) + } + return NewGdbmPersisterFromDatabase(gdb) +} + +func NewGdbmPersisterFromDatabase(gdb *gdbm.Database) *gdbmPersister { + return &gdbmPersister{ + db: gdb, + } +} + +// WithContent sets a current State and Cache object. +// +// This method is normally called before Serialize / Save. +func(p *gdbmPersister) WithContent(st *state.State, ca *cache.Cache) *gdbmPersister { + p.State = st + p.Memory = ca + return p +} + +// TODO: DRY +// GetState implements the Persister interface. +func(p *gdbmPersister) GetState() *state.State { + return p.State +} + +// GetMemory implements the Persister interface. +func(p *gdbmPersister) GetMemory() cache.Memory { + return p.Memory +} + +// Serialize implements the Persister interface. +func(p *gdbmPersister) Serialize() ([]byte, error) { + return cbor.Marshal(p) +} + +// Deserialize implements the Persister interface. +func(p *gdbmPersister) Deserialize(b []byte) error { + err := cbor.Unmarshal(b, p) + return err +} + +// Save implements the Persister interface. +func(p *gdbmPersister) Save(key string) error { + b, err := p.Serialize() + if err != nil { + return err + } + k := db.ToDbKey(db.DATATYPE_STATE, []byte(key), nil) + err = p.db.Store(k, b, true) + if err != nil { + return err + } + Logg.Debugf("saved state and cache", "key", key, "bytecode", p.State.Code, "flags", p.State.Flags) + return nil +} + +// Load implements the Persister interface. +func(p *gdbmPersister) Load(key string) error { + k := db.ToDbKey(db.DATATYPE_STATE, []byte(key), nil) + b, err := p.db.Fetch(k) + if err != nil { + return err + } + err = p.Deserialize(b) + if err != nil { + return err + } + Logg.Debugf("loaded state and cache", "key", key, "bytecode", p.State.Code) + return nil +} diff --git a/persist/persist.go b/persist/persist.go @@ -1,17 +1,88 @@ package persist import ( - "git.defalsify.org/vise.git/cache" + "context" + + "github.com/fxamacker/cbor/v2" + + "git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/state" + "git.defalsify.org/vise.git/cache" ) -// Persister interface defines the methods needed for a component that can store the execution state to a storage location. -type Persister interface { - Serialize() ([]byte, error) // Output serializes representation of the state. - Deserialize(b []byte) error // Restore state from a serialized state. - Save(key string) error // Serialize and commit the state representation to persisted storage. - Load(key string) error // Load the state representation from persisted storage and Deserialize. - GetState() *state.State // Get the currently loaded State object. - GetMemory() cache.Memory // Get the currently loaded Cache object. +type Persister struct { + State *state.State + Memory *cache.Cache + ctx context.Context + db db.Db +} + +func NewPersister(db db.Db) *Persister { + return &Persister{ + db: db, + ctx: context.Background(), + } +} + +func(p *Persister) WithContext(ctx context.Context) *Persister { + p.ctx = ctx + return p +} + +func(p *Persister) WithSession(sessionId string) *Persister { + p.db.SetSession(sessionId) + return p +} + +// WithContent sets a current State and Cache object. +// +// This method is normally called before Serialize / Save. +func(p *Persister) WithContent(st *state.State, ca *cache.Cache) *Persister { + p.State = st + p.Memory = ca + return p } +// GetState implements the Persister interface. +func(p *Persister) GetState() *state.State { + return p.State +} + +// GetMemory implements the Persister interface. +func(p *Persister) GetMemory() cache.Memory { + return p.Memory +} + +// Serialize implements the Persister interface. +func(p *Persister) Serialize() ([]byte, error) { + return cbor.Marshal(p) +} + +// Deserialize implements the Persister interface. +func(p *Persister) Deserialize(b []byte) error { + err := cbor.Unmarshal(b, p) + return err +} + +func(p *Persister) Save(key string) error { + b, err := p.Serialize() + if err != nil { + return err + } + p.db.SetPrefix(db.DATATYPE_STATE) + return p.db.Put(p.ctx, []byte(key), b) +} + +func(p *Persister) Load(key string) error { + p.db.SetPrefix(db.DATATYPE_STATE) + b, err := p.db.Get(p.ctx, []byte(key)) + if err != nil { + return err + } + err = p.Deserialize(b) + if err != nil { + return err + } + Logg.Debugf("loaded state and cache", "key", key, "bytecode", p.State.Code) + return nil +} diff --git a/resource/db.go b/resource/db.go @@ -0,0 +1,68 @@ +package resource + +import ( + "context" + "errors" + "fmt" + + "git.defalsify.org/vise.git/db" +) + +const ( + resource_max_datatype = db.DATATYPE_TEMPLATE +) + +type dbGetter struct { + typs uint8 + db db.Db +} + +func NewDbFuncGetter(store db.Db, typs... uint8) (*dbGetter, error) { + var v uint8 + g := &dbGetter{ + db: store, + } + for _, v = range(typs) { + if v > resource_max_datatype { + return nil, fmt.Errorf("datatype %d is not a resource", v) + } + g.typs |= v + } + return g, nil +} + +func(g *dbGetter) fn(ctx context.Context, sym string) ([]byte, error) { + return g.db.Get(ctx, []byte(sym)) +} + +func(g *dbGetter) sfn(ctx context.Context, sym string) (string, error) { + b, err := g.fn(ctx, sym) + if err != nil { + return "", err + } + return string(b), nil +} + +func(g *dbGetter) GetTemplate(ctx context.Context, sym string) (string, error) { + if g.typs & db.DATATYPE_TEMPLATE == 0{ + return "", errors.New("not a template getter") + } + g.db.SetPrefix(db.DATATYPE_TEMPLATE) + return g.sfn(ctx, sym) +} + +func(g *dbGetter) GetMenu(ctx context.Context, sym string) (string, error) { + if g.typs & db.DATATYPE_MENU == 0{ + return "", errors.New("not a menu getter") + } + g.db.SetPrefix(db.DATATYPE_MENU) + return g.sfn(ctx, sym) +} + +func(g *dbGetter) GetCode(ctx context.Context, sym string) ([]byte, error) { + if g.typs & db.DATATYPE_BIN == 0{ + return nil, errors.New("not a code getter") + } + g.db.SetPrefix(db.DATATYPE_BIN) + return g.fn(ctx, sym) +} diff --git a/resource/db_test.go b/resource/db_test.go @@ -0,0 +1,90 @@ +package resource + +import ( + "bytes" + "context" + "testing" + + "git.defalsify.org/vise.git/db" +) + +func TestDb(t *testing.T) { + ctx := context.Background() + store := db.NewMemDb(ctx) + store.Connect(ctx, "") + tg, err := NewDbFuncGetter(store, db.DATATYPE_TEMPLATE) + if err != nil { + t.Fatal(err) + } + rs := NewMenuResource() + rs.WithTemplateGetter(tg.GetTemplate) + + s, err := rs.GetTemplate(ctx, "foo") + if err == nil { + t.Fatal("expected error") + } + + + store.SetPrefix(db.DATATYPE_TEMPLATE) + err = store.Put(ctx, []byte("foo"), []byte("bar")) + if err == nil { + t.Fatal("expected error") + } + store.SetLock(db.DATATYPE_TEMPLATE, false) + err = store.Put(ctx, []byte("foo"), []byte("bar")) + if err != nil { + t.Fatal(err) + } + store.SetLock(db.DATATYPE_TEMPLATE, true) + s, err = rs.GetTemplate(ctx, "foo") + if err != nil { + t.Fatal(err) + } + if s != "bar" { + t.Fatalf("expected 'bar', got %s", s) + } + + // test support check + store.SetPrefix(db.DATATYPE_BIN) + store.SetLock(db.DATATYPE_BIN, false) + err = store.Put(ctx, []byte("xyzzy"), []byte("deadbeef")) + if err != nil { + t.Fatal(err) + } + store.SetLock(db.DATATYPE_BIN, true) + + rs.WithCodeGetter(tg.GetCode) + b, err := rs.GetCode(ctx, "xyzzy") + if err == nil { + t.Fatal("expected error") + } + + tg, err = NewDbFuncGetter(store, db.DATATYPE_TEMPLATE, db.DATATYPE_BIN) + if err != nil { + t.Fatal(err) + } + rs.WithTemplateGetter(tg.GetTemplate) + + rs.WithCodeGetter(tg.GetCode) + b, err = rs.GetCode(ctx, "xyzzy") + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(b, []byte("deadbeef")) { + t.Fatalf("expected 'deadbeef', got %x", b) + } + + tg, err = NewDbFuncGetter(store, db.DATATYPE_TEMPLATE, db.DATATYPE_BIN, db.DATATYPE_MENU) + if err != nil { + t.Fatal(err) + } + store.SetPrefix(db.DATATYPE_MENU) + store.SetLock(db.DATATYPE_MENU, false) + err = store.Put(ctx, []byte("inky"), []byte("pinky")) + if err != nil { + t.Fatal(err) + } + store.SetLock(db.DATATYPE_MENU, true) + rs.WithMenuGetter(tg.GetMenu) + +} diff --git a/resource/fs.go b/resource/fs.go @@ -16,7 +16,6 @@ import ( type FsResource struct { MenuResource Path string - fns map[string]EntryFunc // languageStrict bool } @@ -60,7 +59,7 @@ func(fsr FsResource) GetTemplate(ctx context.Context, sym string) (string, error return strings.TrimSpace(s), err } -func(fsr FsResource) GetCode(sym string) ([]byte, error) { +func(fsr FsResource) GetCode(ctx context.Context, sym string) ([]byte, error) { fb := sym + ".bin" fp := path.Join(fsr.Path, fb) return ioutil.ReadFile(fp) @@ -95,19 +94,13 @@ func(fsr FsResource) GetMenu(ctx context.Context, sym string) (string, error) { return strings.TrimSpace(s), err } -func(fsr *FsResource) AddLocalFunc(sym string, fn EntryFunc) { - if fsr.fns == nil { - fsr.fns = make(map[string]EntryFunc) - } - fsr.fns[sym] = fn -} func(fsr FsResource) FuncFor(sym string) (EntryFunc, error) { - fn, ok := fsr.fns[sym] - if ok { + fn, err := fsr.MenuResource.FallbackFunc(sym) + if err == nil { return fn, nil } - _, err := fsr.getFuncNoCtx(sym, nil, nil) + _, err = fsr.getFuncNoCtx(sym, nil, nil) if err != nil { return nil, fmt.Errorf("unknown sym: %s", sym) } diff --git a/resource/gdbm.go b/resource/gdbm.go @@ -0,0 +1,92 @@ +package resource + +import ( + "context" + "fmt" + + gdbm "github.com/graygnuorg/go-gdbm" + "git.defalsify.org/vise.git/lang" + "git.defalsify.org/vise.git/db" +) + +type gdbmResource struct { + db *gdbm.Database + fns map[string]EntryFunc +} + +func NewGdbmResource(fp string) *gdbmResource { + gdb, err := gdbm.Open(fp, gdbm.ModeReader) + if err != nil { + panic(err) + } + return NewGdbmResourceFromDatabase(gdb) +} + +func NewGdbmResourceFromDatabase(gdb *gdbm.Database) *gdbmResource { + return &gdbmResource{ + db: gdb, + } +} + +func(dbr *gdbmResource) GetTemplate(ctx context.Context, sym string) (string, error) { + var ln lang.Language + v := ctx.Value("Language") + if v != nil { + ln = v.(lang.Language) + } + k := db.ToDbKey(db.DATATYPE_TEMPLATE, []byte(sym), &ln) + r, err := dbr.db.Fetch(k) + if err != nil { + if err.(*gdbm.GdbmError).Is(gdbm.ErrItemNotFound) { + k = db.ToDbKey(db.DATATYPE_TEMPLATE, []byte(sym), nil) + r, err = dbr.db.Fetch(k) + if err != nil { + return "", err + } + } + } + return string(r), nil +} + +func(dbr *gdbmResource) GetCode(sym string) ([]byte, error) { + k := db.ToDbKey(db.DATATYPE_BIN, []byte(sym), nil) + return dbr.db.Fetch(k) +} + +func(dbr *gdbmResource) GetMenu(ctx context.Context, sym string) (string, error) { + msym := sym + "_menu" + var ln lang.Language + v := ctx.Value("Language") + if v != nil { + ln = v.(lang.Language) + } + k := db.ToDbKey(db.DATATYPE_TEMPLATE, []byte(msym), &ln) + r, err := dbr.db.Fetch(k) + if err != nil { + if err.(*gdbm.GdbmError).Is(gdbm.ErrItemNotFound) { + return sym, nil + } + return "", err + } + return string(r), nil + +} + +func(dbr gdbmResource) FuncFor(sym string) (EntryFunc, error) { + fn, ok := dbr.fns[sym] + if !ok { + return nil, fmt.Errorf("function %s not found", sym) + } + return fn, nil +} + +func(dbr *gdbmResource) AddLocalFunc(sym string, fn EntryFunc) { + if dbr.fns == nil { + dbr.fns = make(map[string]EntryFunc) + } + dbr.fns[sym] = fn +} + +func(dbr *gdbmResource) String() string { + return fmt.Sprintf("gdbm: %v", dbr.db) +} diff --git a/resource/mem.go b/resource/mem.go @@ -34,7 +34,7 @@ func(mr MemResource) getTemplate(ctx context.Context, sym string) (string, error return r, nil } -func(mr MemResource) getCode(sym string) ([]byte, error) { +func(mr MemResource) getCode(ctx context.Context, sym string) ([]byte, error) { r, ok := mr.bytecodes[sym] if !ok { return nil, fmt.Errorf("unknown bytecode: %s", sym) diff --git a/resource/mem_test.go b/resource/mem_test.go @@ -36,7 +36,8 @@ func TestMemResourceCode(t *testing.T) { rs := NewMemResource() rs.AddBytecode("foo", []byte("bar")) - r, err := rs.GetCode("foo") + ctx := context.Background() + r, err := rs.GetCode(ctx, "foo") if err != nil { t.Fatal(err) } @@ -44,7 +45,7 @@ func TestMemResourceCode(t *testing.T) { fmt.Errorf("expected 'bar', got %x", r) } - _, err = rs.GetCode("bar") + _, err = rs.GetCode(ctx, "bar") if err == nil { t.Fatalf("expected error") } diff --git a/resource/resource.go b/resource/resource.go @@ -2,8 +2,10 @@ package resource import ( "context" + "fmt" ) + // Result contains the results of an external code operation. type Result struct { Content string // content value for symbol after execution. @@ -14,7 +16,7 @@ type Result struct { // EntryFunc is a function signature for retrieving value for a key type EntryFunc func(ctx context.Context, sym string, input []byte) (Result, error) -type CodeFunc func(sym string) ([]byte, error) +type CodeFunc func(ctx context.Context, sym string) ([]byte, error) type MenuFunc func(ctx context.Context, sym string) (string, error) type TemplateFunc func(ctx context.Context, sym string) (string, error) type FuncForFunc func(sym string) (EntryFunc, error) @@ -22,7 +24,7 @@ type FuncForFunc func(sym string) (EntryFunc, error) // Resource implementation are responsible for retrieving values and templates for symbols, and can render templates from value dictionaries. type Resource interface { GetTemplate(ctx context.Context, sym string) (string, error) // Get the template for a given symbol. - GetCode(sym string) ([]byte, error) // Get the bytecode for the given symbol. + GetCode(ctx context.Context, sym string) ([]byte, error) // Get the bytecode for the given symbol. GetMenu(ctx context.Context, sym string) (string, error) // Receive menu test for menu symbol. FuncFor(sym string) (EntryFunc, error) // Resolve symbol content point for. } @@ -36,11 +38,14 @@ type MenuResource struct { templateFunc TemplateFunc menuFunc MenuFunc funcFunc FuncForFunc + fns map[string]EntryFunc } // NewMenuResource creates a new MenuResource instance. func NewMenuResource() *MenuResource { - return &MenuResource{} + rs := &MenuResource{} + rs.funcFunc = rs.FallbackFunc + return rs } // WithCodeGetter sets the code symbol resolver method. @@ -67,22 +72,39 @@ func(m *MenuResource) WithMenuGetter(menuGetter MenuFunc) *MenuResource { return m } -// FuncFor implements Resource interface +// FuncFor implements Resource interface. func(m MenuResource) FuncFor(sym string) (EntryFunc, error) { return m.funcFunc(sym) } -// GetCode implements Resource interface -func(m MenuResource) GetCode(sym string) ([]byte, error) { - return m.codeFunc(sym) +// GetCode implements Resource interface. +func(m MenuResource) GetCode(ctx context.Context, sym string) ([]byte, error) { + return m.codeFunc(ctx, sym) } -// GetTemplate implements Resource interface +// GetTemplate implements Resource interface. func(m MenuResource) GetTemplate(ctx context.Context, sym string) (string, error) { return m.templateFunc(ctx, sym) } -// GetMenu implements Resource interface +// GetMenu implements Resource interface. func(m MenuResource) GetMenu(ctx context.Context, sym string) (string, error) { return m.menuFunc(ctx, sym) } + +// AddLocalFunc associates a handler function with a external function symbol to be returned by FallbackFunc. +func(m *MenuResource) AddLocalFunc(sym string, fn EntryFunc) { + if m.fns == nil { + m.fns = make(map[string]EntryFunc) + } + m.fns[sym] = fn +} + +// FallbackFunc returns the default handler function for a given external function symbol. +func(m *MenuResource) FallbackFunc(sym string) (EntryFunc, error) { + fn, ok := m.fns[sym] + if !ok { + return nil, fmt.Errorf("unknown function: %s", sym) + } + return fn, nil +} diff --git a/vm/runner.go b/vm/runner.go @@ -246,9 +246,9 @@ func(vm *Vm) runCatch(ctx context.Context, b []byte) ([]byte, error) { if err != nil { return b, err } - Logg.InfoCtxf(ctx, "catch!", "flag", sig, "sym", sym, "target", actualSym) + Logg.InfoCtxf(ctx, "catch!", "flag", sig, "sym", sym, "target", actualSym, "mode", mode) sym = actualSym - bh, err := vm.rs.GetCode(sym) + bh, err := vm.rs.GetCode(ctx, sym) if err != nil { return b, err } @@ -270,7 +270,7 @@ func(vm *Vm) runCroak(ctx context.Context, b []byte) ([]byte, error) { vm.ca.Reset() b = []byte{} } - return []byte{}, nil + return b, nil } // executes the LOAD opcode @@ -323,7 +323,7 @@ func(vm *Vm) runMove(ctx context.Context, b []byte) ([]byte, error) { if err != nil { return b, err } - code, err := vm.rs.GetCode(sym) + code, err := vm.rs.GetCode(ctx, sym) if err != nil { return b, err } @@ -385,7 +385,7 @@ func(vm *Vm) runInCmp(ctx context.Context, b []byte) ([]byte, error) { vm.Reset() - code, err := vm.rs.GetCode(sym) + code, err := vm.rs.GetCode(ctx, sym) if err != nil { return b, err } @@ -494,17 +494,17 @@ func(vm *Vm) refresh(key string, rs resource.Resource, ctx context.Context) (str _ = vm.st.SetFlag(state.FLAG_LOADFAIL) return "", NewExternalCodeError(key, err).WithCode(r.Status) } - for _, flag := range r.FlagSet { + for _, flag := range r.FlagReset { if !state.IsWriteableFlag(flag) { continue } - vm.st.SetFlag(flag) + vm.st.ResetFlag(flag) } - for _, flag := range r.FlagReset { + for _, flag := range r.FlagSet { if !state.IsWriteableFlag(flag) { continue } - vm.st.ResetFlag(flag) + vm.st.SetFlag(flag) } haveLang := vm.st.MatchFlag(state.FLAG_LANG, true)