commit 00876115566638e66631e314820773450c177294
parent 0ee0159b513122c43680c1e98a2838a322d32ba9
Author: lash <dev@holbrook.no>
Date:   Fri, 30 Aug 2024 16:23:24 +0100
Merge branch 'lash/data-db' into lash/gdbm-resources
Diffstat:
30 files changed, 954 insertions(+), 10 deletions(-)
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)", 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,53 @@
+package db
+
+import (
+	"context"
+	"errors"
+
+	"git.defalsify.org/vise.git/lang"
+)
+
+const (
+	DATATYPE_UNKNOWN = iota
+	DATATYPE_BIN
+	DATATYPE_TEMPLATE
+	DATATYPE_STATE
+	DATATYPE_USERSTART
+)
+
+type Db interface {
+	Connect(ctx context.Context, connStr string) error
+	Close() error
+	Get(ctx context.Context, key []byte) ([]byte, error)
+	Put(ctx context.Context, key []byte, val []byte) error
+}
+
+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...)
+}
+
+type BaseDb struct {
+	pfx uint8
+	sid []byte
+}
+
+func(db *BaseDb) SetPrefix(pfx uint8) {
+	db.pfx = pfx
+}
+
+func(db *BaseDb) SetSession(sessionId string) {
+	db.sid = append([]byte(sessionId), 0x2E)
+}
+
+func(db *BaseDb) ToKey(key []byte) ([]byte, error) {
+	if db.pfx == DATATYPE_UNKNOWN {
+		return nil, errors.New("datatype prefix cannot be UNKNOWN")
+	}
+	b := append(db.sid, 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,64 @@
+package db
+
+import (
+	"context"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path"
+)
+
+type FsDb struct {
+	BaseDb
+	dir string
+}
+
+func(fdb *FsDb) Connect(ctx context.Context, connStr string) error {
+	fi, err := os.Stat(connStr)
+	if err != nil {
+		return err
+	}
+	if !fi.IsDir()  {
+		return fmt.Errorf("fs db %s is not a directory", connStr)
+	}
+	fdb.dir = connStr
+	return nil
+}
+
+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, err
+	}
+	defer f.Close()
+	b, err := ioutil.ReadAll(f)
+	if err != nil {
+		return nil, err
+	}
+	return b, nil
+}
+
+func(fdb *FsDb) Put(ctx context.Context, key []byte, val []byte) error {
+	fp, err := fdb.pathFor(key)
+	if err != nil {
+		return err
+	}
+	return ioutil.WriteFile(fp, val, 0600)
+}
+
+func(fdb *FsDb) Close() error {
+	return nil
+}	
+ 
+func(fdb *FsDb) pathFor(key []byte) (string, error) {
+	kb, err := fdb.ToKey(key)
+	if err != nil {
+		return "", err
+	}
+	kb[0] += 30
+	return path.Join(fdb.dir, string(kb)), nil
+}
diff --git a/db/fs_test.go b/db/fs_test.go
@@ -0,0 +1,39 @@
+package db
+
+import (
+	"bytes"
+	"context"
+	"io/ioutil"
+	"testing"
+)
+
+func TestPutGetFs(t *testing.T) {
+	ctx := context.Background()
+	sid := "ses"
+	d, err := ioutil.TempDir("", "vise-db-*")
+	if err != nil {
+		t.Fatal(err)
+	}
+	db := &FsDb{}
+	db.SetPrefix(DATATYPE_USERSTART)
+	db.SetSession(sid)
+	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,50 @@
+package db
+
+import (
+	"context"
+	"errors"
+
+	gdbm "github.com/graygnuorg/go-gdbm"
+)
+
+type GdbmDb struct {
+	BaseDb
+	conn *gdbm.Database
+	prefix uint8
+}
+
+func(gdb *GdbmDb) Connect(ctx context.Context, connStr string) error {
+	db, err := gdbm.Open(connStr, gdbm.ModeWrcreat)
+	if err != nil {
+		return err
+	}
+	gdb.conn = db
+	return nil
+}
+
+func(gdb *GdbmDb) Put(ctx context.Context, key []byte, val []byte) error {
+	k, err := gdb.ToKey(key)
+	if err != nil {
+		return err
+	}
+	return gdb.conn.Store(k, val, true)
+}
+
+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
+}
+
+func(gdb *GdbmDb) Close() error {
+	return gdb.Close()
+}
diff --git a/db/gdbm_test.go b/db/gdbm_test.go
@@ -0,0 +1,40 @@
+package db
+
+import (
+	"bytes"
+	"context"
+	"io/ioutil"
+	"testing"
+)
+
+func TestPutGetGdbm(t *testing.T) {
+	ctx := context.Background()
+	sid := "ses"
+	f, err := ioutil.TempFile("", "vise-db-*")
+	if err != nil {
+		t.Fatal(err)
+	}
+	db := &GdbmDb{}
+	db.SetPrefix(DATATYPE_USERSTART)
+	db.SetSession(sid)
+	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/pg.go b/db/pg.go
@@ -0,0 +1,139 @@
+package db 
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/jackc/pgx/v5/pgxpool"
+)
+
+type PgDb struct {
+	BaseDb
+	conn *pgxpool.Pool
+	schema string
+	prefix uint8
+}
+
+func NewPgDb() *PgDb {
+	return &PgDb{
+		schema: "public",
+	}
+}
+
+func(pdb *PgDb) WithSchema(schema string) *PgDb {
+	pdb.schema = schema
+	return pdb
+}
+
+func(pdb *PgDb) Connect(ctx context.Context, connStr string) error {
+	var err error
+	conn, err := pgxpool.New(ctx, connStr)
+	if err != nil {
+		return err
+	}
+	pdb.conn = conn
+	return pdb.prepare(ctx)
+}
+
+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_domain (
+//		id SERIAL PRIMARY KEY,
+//		name VARCHAR(256) NOT NULL
+//	);
+//`, pdb.schema)
+//	_, err = tx.Exec(ctx, query)
+//	if err != nil {
+//		tx.Rollback(ctx)
+//		return err
+//	}
+//
+//	query = fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s.kv_vise (
+//		id SERIAL NOT NULL,
+//		domain_id INT NOT NULL,
+//		key VARCHAR(256) NOT NULL,
+//		value BYTEA NOT NULL,
+//		constraint fk_domain
+//			FOREIGN KEY (domain_id)
+//			REFERENCES %s.kv_vise_domain(id)
+//	);
+//`, pdb.schema, pdb.schema)
+//	_, err = tx.Exec(ctx, query)
+//	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 {
+		//if !errors.Is(pgx.ErrTxCommitRollback) {
+			tx.Rollback(ctx)
+			return err
+		//}
+	}
+	return nil
+}
+
+func(pdb *PgDb) Put(ctx context.Context, key []byte, val []byte) error {
+	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
+}
+
+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
+}
+
+func(pdb *PgDb) Close() error {
+	pdb.Close()
+	return nil
+}
diff --git a/db/pg_test.go b/db/pg_test.go
@@ -0,0 +1,43 @@
+package db
+
+import (
+	"bytes"
+	"context"
+	"testing"
+)
+
+func TestPutGetPg(t *testing.T) {
+	//t.Skip("need postgresql mock")
+	ses := "xyzzy"
+	db := NewPgDb().WithSchema("vvise")
+	db.SetPrefix(DATATYPE_USERSTART)
+	db.SetSession(ses)
+	ctx := context.Background()
+	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/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/reuse/Makefile b/examples/reuse/Makefile
@@ -0,0 +1,10 @@
+INPUTS = $(wildcard ./*.vis)
+TXTS = $(wildcard ./*.txt.orig)
+
+%.vis:
+	go run ../../dev/asm $(basename $@).vis > $(basename $@).bin
+
+all: $(INPUTS) $(TXTS)
+
+%.txt.orig:
+	cp -v $(basename $@).orig $(basename $@)
diff --git a/examples/reuse/bar.vis b/examples/reuse/bar.vis
@@ -0,0 +1,3 @@
+LOAD do_bar 0
+MAP do_bar
+HALT
diff --git a/examples/reuse/foo.vis b/examples/reuse/foo.vis
@@ -0,0 +1,3 @@
+LOAD do_foo 0
+MAP do_foo
+HALT
diff --git a/examples/reuse/main.go b/examples/reuse/main.go
@@ -0,0 +1,59 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"path"
+
+	testdataloader "github.com/peteole/testdata-loader"
+
+	"git.defalsify.org/vise.git/cache"
+	"git.defalsify.org/vise.git/engine"
+	"git.defalsify.org/vise.git/resource"
+	"git.defalsify.org/vise.git/state"
+)
+
+const (
+	USERFLAG = iota + state.FLAG_USERSTART
+)
+
+var (
+	baseDir = testdataloader.GetBasePath()
+	scriptDir = path.Join(baseDir, "examples", "reuse")
+	emptyResult = resource.Result{}
+)
+
+func same(ctx context.Context, sym string, input []byte) (resource.Result, error) {
+	return resource.Result{
+		Content: "You came through the symbol " + sym,
+	}, nil
+}
+
+func main() {
+	root := "root"
+	fmt.Fprintf(os.Stderr, "starting session at symbol '%s' using resource dir: %s\n", root, scriptDir)
+
+	st := state.NewState(0)
+	rs := resource.NewFsResource(scriptDir)
+	rs.AddLocalFunc("do_foo", same)
+	rs.AddLocalFunc("do_bar", same)
+	ca := cache.NewCache()
+	cfg := engine.Config{
+		Root: "root",
+	}
+	ctx := context.Background()
+	en := engine.NewEngine(ctx, cfg, &st, rs, ca)
+	var err error
+	_, err = en.Init(ctx)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "engine init fail: %v\n", err)
+		os.Exit(1)
+	}
+
+	err = engine.Loop(ctx, &en, os.Stdin, os.Stdout)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err)
+		os.Exit(1)
+	}
+}
diff --git a/examples/reuse/root b/examples/reuse/root
@@ -0,0 +1 @@
+choose
diff --git a/examples/reuse/root.vis b/examples/reuse/root.vis
@@ -0,0 +1,6 @@
+MOUT foo 0
+MOUT bar 1
+HALT
+INCMP foo 0
+INCMP bar 1
+INCMP ^ *
diff --git a/go.mod b/go.mod
@@ -7,11 +7,18 @@ require (
 	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
+	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,16 +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/vm/runner.go b/vm/runner.go
@@ -246,7 +246,7 @@ 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)
 		if err != nil {
@@ -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
@@ -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)