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)