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:
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)