go-vise

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

commit f7bcf8896b3ee4ca75071300bd1e8563219d862d
parent b0a3324409eee2a8c52093cb397a6f02790f90ef
Author: lash <dev@holbrook.no>
Date:   Fri, 31 Mar 2023 22:35:13 +0100

Remove unused input from EntryFunc, add docs

Diffstat:
Mgo/resource/resource.go | 6++++--
Mgo/router/router.go | 29+++++++++++++++++++++++++++++
Mgo/state/state.go | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mgo/state/state_test.go | 2++
Mgo/vm/vm.go | 49+++++++++++++++++++++++++++++++++++--------------
Mgo/vm/vm_test.go | 8++++----
6 files changed, 167 insertions(+), 51 deletions(-)

diff --git a/go/resource/resource.go b/go/resource/resource.go @@ -4,9 +4,11 @@ import ( "context" ) -type EntryFunc func(input []byte, ctx context.Context) (string, error) +// EntryFunc is a function signature for retrieving value for a key +type EntryFunc func(ctx context.Context) (string, error) -type Fetcher interface { +// Resource implementation are responsible for retrieving values and templates for symbols, and can render templates from value dictionaries. +type Resource interface { Get(sym string) (string, error) Render(sym string, values map[string]string) (string, error) FuncFor(sym string) (EntryFunc, error) diff --git a/go/router/router.go b/go/router/router.go @@ -4,23 +4,33 @@ import ( "fmt" ) +// Router contains and parses the routing section of the bytecode for a node. type Router struct { selectors []string symbols map[string]string } +// NewRouter creates a new Router object. func NewRouter() Router { return Router{ symbols: make(map[string]string), } } +// NewStaticRouter creates a new Router object with a single destination. +// +// Used for routes that consume input value instead of navigation choices. func NewStaticRouter(symbol string) Router { return Router{ symbols: map[string]string{"_": symbol}, } } +// Add associates a selector with a destination symbol. +// +// Fails if: +// - selector or symbol value is invalid +// - selector already exists func(r *Router) Add(selector string, symbol string) error { if r.symbols[selector] != "" { return fmt.Errorf("selector %v already set to symbol %v", selector, symbol) @@ -41,14 +51,27 @@ func(r *Router) Add(selector string, symbol string) error { return nil } +// Get retrieve symbol for selector. +// +// Returns an empty string if selector does not exist. +// +// Will always return an empty string if the router is static. func(r *Router) Get(selector string) string { return r.symbols[selector] } +// Get the statically defined symbol destination. +// +// Returns an empty string if not a static router. func(r *Router) Default() string { return r.symbols["_"] } +// Next removes one selector from the list of registered selectors. +// +// It returns it together with it associated value in bytecode form. +// +// Returns an empty byte array if no more selectors remain. func(r *Router) Next() []byte { if len(r.selectors) == 0 { return []byte{} @@ -70,6 +93,9 @@ func(r *Router) Next() []byte { return b } +// ToBytes consume all selectors and values and returns them in sequence in bytecode form. +// +// This is identical to concatenating all returned values from non-empty Next() results. func(r *Router) ToBytes() []byte { b := []byte{} for true { @@ -82,6 +108,9 @@ func(r *Router) ToBytes() []byte { return b } +// Restore a Router from bytecode. +// +// FromBytes(ToBytes()) creates an identical object. func FromBytes(b []byte) Router { rb := NewRouter() navigable := true diff --git a/go/state/state.go b/go/state/state.go @@ -5,19 +5,33 @@ import ( "log" ) +// State holds the command stack, error condition of a unique execution session. +// +// It also holds cached values for all results of executed symbols. +// +// Cached values are linked to the command stack level it which they were loaded. When they go out of scope they are freed. +// +// Values must be mapped to a level in order to be available for retrieval and count towards size +// +// It can hold a single argument, which is freed once it is read +// +// Symbols are loaded with individual size limitations. The limitations apply if a load symbol is updated. Symbols may be added with a 0-value for limits, called a "sink." If mapped, the sink will consume all net remaining size allowance unused by other symbols. Only one sink may be mapped per level. +// +// Symbol keys do not count towards cache size limitations. type State struct { - Flags []byte - CacheSize uint32 - CacheUseSize uint32 - Cache []map[string]string - CacheMap map[string]string - ExecPath []string - Arg *string - sizes map[string]uint16 - sink *string + Flags []byte // Error state + CacheSize uint32 // Total allowed cumulative size of values in cache + CacheUseSize uint32 // Currently used bytes by all values in cache + Cache []map[string]string // All loaded cache items + CacheMap map[string]string // Mapped + execPath []string // Command symbols stack + arg *string // Optional argument. Nil if not set. + sizes map[string]uint16 // Size limits for all loaded symbols. + sink *string // //sizeIdx uint16 } +// NewState creates a new State object with bitSize number of error condition states. func NewState(bitSize uint64) State { if bitSize == 0 { panic("bitsize cannot be 0") @@ -36,40 +50,61 @@ func NewState(bitSize uint64) State { return st } -func(st State) Where() string { - if len(st.ExecPath) == 0 { - return "" - } - l := len(st.ExecPath) - return st.ExecPath[l-1] -} - +// WithCacheSize applies a cumulative cache size limitation for all cached items. func(st State) WithCacheSize(cacheSize uint32) State { st.CacheSize = cacheSize return st } +// Where returns the current active rendering symbol. +func(st State) Where() string { + if len(st.execPath) == 0 { + return "" + } + l := len(st.execPath) + return st.execPath[l-1] +} + +// PutArg adds the optional argument. +// +// Fails if arg already set. func(st *State) PutArg(input string) error { - st.Arg = &input + st.arg = &input + if st.arg != nil { + return fmt.Errorf("arg already set to %s", *st.arg) + } return nil } +// PopArg retrieves the optional argument. Will be freed upon retrieval. +// +// Fails if arg not set (or already freed). func(st *State) PopArg() (string, error) { - if st.Arg == nil { + if st.arg == nil { return "", fmt.Errorf("arg is not set") } - return *st.Arg, nil + return *st.arg, nil } +// Down adds the given symbol to the command stack. +// +// Clears mapping and sink. func(st *State) Down(input string) { m := make(map[string]string) st.Cache = append(st.Cache, m) st.sizes = make(map[string]uint16) - st.ExecPath = append(st.ExecPath, input) + st.execPath = append(st.execPath, input) st.resetCurrent() } +// Up removes the latest symbol to the command stack, and make the previous symbol current. +// +// Frees all symbols and associated values loaded at the previous stack level. Cache capacity is increased by the corresponding amount. +// +// Clears mapping and sink. +// +// Fails if called at top frame. func(st *State) Up() error { l := len(st.Cache) if l == 0 { @@ -83,16 +118,24 @@ func(st *State) Up() error { log.Printf("free frame %v key %v value size %v", l, k, sz) } st.Cache = st.Cache[:l] - st.ExecPath = st.ExecPath[:l] + st.execPath = st.execPath[:l] st.resetCurrent() return nil } -func(st *State) Add(key string, value string, sizeHint uint16) error { - if sizeHint > 0 { +// Add adds a cache value under a cache symbol key. +// +// Also stores the size limitation of for key for later updates. +// +// Fails if: +// - key already defined +// - value is longer than size limit +// - adding value exceeds cumulative cache capacity +func(st *State) Add(key string, value string, sizeLimit uint16) error { + if sizeLimit > 0 { l := uint16(len(value)) - if l > sizeHint { - return fmt.Errorf("value length %v exceeds value size limit %v", l, sizeHint) + if l > sizeLimit { + return fmt.Errorf("value length %v exceeds value size limit %v", l, sizeLimit) } } checkFrame := st.frameOf(key) @@ -106,16 +149,24 @@ func(st *State) Add(key string, value string, sizeHint uint16) error { log.Printf("add key %s value size %v", key, sz) st.Cache[len(st.Cache)-1][key] = value st.CacheUseSize += sz - st.sizes[key] = sizeHint + st.sizes[key] = sizeLimit return nil } +// Update sets a new value for an existing key. +// +// Uses the size limitation from when the key was added. +// +// Fails if: +// - key not defined +// - value is longer than size limit +// - replacing value exceeds cumulative cache capacity func(st *State) Update(key string, value string) error { - sizeHint := st.sizes[key] + sizeLimit := st.sizes[key] if st.sizes[key] > 0 { l := uint16(len(value)) - if l > sizeHint { - return fmt.Errorf("update value length %v exceeds value size limit %v", l, sizeHint) + if l > sizeLimit { + return fmt.Errorf("update value length %v exceeds value size limit %v", l, sizeLimit) } } checkFrame := st.frameOf(key) @@ -139,6 +190,11 @@ func(st *State) Update(key string, value string) error { return nil } +// Map marks the given key for retrieval. +// +// After this, Val() will return the value for the key, and Size() will include the value size and limitations in its calculations. +// +// Only one symbol with no size limitation may be mapped at the current level. func(st *State) Map(key string) error { m, err := st.Get() if err != nil { @@ -155,10 +211,12 @@ func(st *State) Map(key string) error { return nil } +// Depth returns the current call stack depth. func(st *State) Depth() uint8 { return uint8(len(st.Cache)) } +// Get returns the full key-value mapping for all mapped keys at the current cache level. func(st *State) Get() (map[string]string, error) { if len(st.Cache) == 0 { return nil, fmt.Errorf("get at top frame") @@ -166,6 +224,9 @@ func(st *State) Get() (map[string]string, error) { return st.Cache[len(st.Cache)-1], nil } +// Val returns value for key +// +// Fails if key is not mapped. func(st *State) Val(key string) (string, error) { r := st.CacheMap[key] if len(r) == 0 { @@ -174,7 +235,7 @@ func(st *State) Val(key string) (string, error) { return r, nil } - +// Reset flushes all state contents below the top level, and returns to the top level. func(st *State) Reset() { if len(st.Cache) == 0 { return @@ -184,6 +245,7 @@ func(st *State) Reset() { return } +// Check returns true if a key already exists in the cache. func(st *State) Check(key string) bool { return st.frameOf(key) == -1 } diff --git a/go/state/state_test.go b/go/state/state_test.go @@ -4,6 +4,7 @@ import ( "testing" ) +// Check creation and testing of state flags func TestNewStateFlags(t *testing.T) { st := NewState(5) if len(st.Flags) != 1 { @@ -20,6 +21,7 @@ func TestNewStateFlags(t *testing.T) { } } +// func TestNewStateCache(t *testing.T) { st := NewState(17) if st.CacheSize != 0 { diff --git a/go/vm/vm.go b/go/vm/vm.go @@ -11,7 +11,7 @@ import ( "git.defalsify.org/festive/state" ) -//type Runner func(instruction []byte, st state.State, rs resource.Fetcher, ctx context.Context) (state.State, []byte, error) +//type Runner func(instruction []byte, st state.State, rs resource.Resource, ctx context.Context) (state.State, []byte, error) func argFromBytes(input []byte) (string, []byte, error) { if len(input) == 0 { @@ -22,7 +22,14 @@ func argFromBytes(input []byte) (string, []byte, error) { return string(out), input[1+sz:], nil } -func Apply(input []byte, instruction []byte, st state.State, rs resource.Fetcher, ctx context.Context) (state.State, []byte, error) { +// Apply applies input to router bytecode to resolve the node symbol to execute. +// +// The execution byte code is initialized with the appropriate MOVE +// +// If the router indicates an argument input, the optional argument is set on the state. +// +// TODO: the bytecode load is a separate step so Run should be run separately. +func Apply(input []byte, instruction []byte, st state.State, rs resource.Resource, ctx context.Context) (state.State, []byte, error) { var err error arg, input, err := argFromBytes(input) @@ -52,7 +59,12 @@ func Apply(input []byte, instruction []byte, st state.State, rs resource.Fetcher return st, instruction, nil } -func Run(instruction []byte, st state.State, rs resource.Fetcher, ctx context.Context) (state.State, []byte, error) { +// Run extracts individual op codes and arguments and executes them. +// +// Each step may update the state. +// +// On error, the remaining instructions will be returned. State will not be rolled back. +func Run(instruction []byte, st state.State, rs resource.Resource, ctx context.Context) (state.State, []byte, error) { var err error for len(instruction) > 0 { log.Printf("instruction is now %v", instruction) @@ -92,7 +104,8 @@ func Run(instruction []byte, st state.State, rs resource.Fetcher, ctx context.Co return st, instruction, nil } -func RunMap(instruction []byte, st state.State, rs resource.Fetcher, ctx context.Context) (state.State, []byte, error) { +// RunMap executes the MAP opcode +func RunMap(instruction []byte, st state.State, rs resource.Resource, ctx context.Context) (state.State, []byte, error) { head, tail, err := instructionSplit(instruction) if err != nil { return st, instruction, err @@ -101,7 +114,8 @@ func RunMap(instruction []byte, st state.State, rs resource.Fetcher, ctx context return st, tail, err } -func RunCatch(instruction []byte, st state.State, rs resource.Fetcher, ctx context.Context) (state.State, []byte, error) { +// RunMap executes the CATCH opcode +func RunCatch(instruction []byte, st state.State, rs resource.Resource, ctx context.Context) (state.State, []byte, error) { head, tail, err := instructionSplit(instruction) if err != nil { return st, instruction, err @@ -115,7 +129,8 @@ func RunCatch(instruction []byte, st state.State, rs resource.Fetcher, ctx conte return st, []byte{}, nil } -func RunCroak(instruction []byte, st state.State, rs resource.Fetcher, ctx context.Context) (state.State, []byte, error) { +// RunMap executes the CROAK opcode +func RunCroak(instruction []byte, st state.State, rs resource.Resource, ctx context.Context) (state.State, []byte, error) { head, tail, err := instructionSplit(instruction) if err != nil { return st, instruction, err @@ -126,7 +141,8 @@ func RunCroak(instruction []byte, st state.State, rs resource.Fetcher, ctx conte return st, []byte{}, nil } -func RunLoad(instruction []byte, st state.State, rs resource.Fetcher, ctx context.Context) (state.State, []byte, error) { +// RunLoad executes the LOAD opcode +func RunLoad(instruction []byte, st state.State, rs resource.Resource, ctx context.Context) (state.State, []byte, error) { head, tail, err := instructionSplit(instruction) if err != nil { return st, instruction, err @@ -137,7 +153,7 @@ func RunLoad(instruction []byte, st state.State, rs resource.Fetcher, ctx contex sz := uint16(tail[0]) tail = tail[1:] - r, err := refresh(head, tail, rs, ctx) + r, err := refresh(head, rs, ctx) if err != nil { return st, tail, err } @@ -145,12 +161,13 @@ func RunLoad(instruction []byte, st state.State, rs resource.Fetcher, ctx contex return st, tail, err } -func RunReload(instruction []byte, st state.State, rs resource.Fetcher, ctx context.Context) (state.State, []byte, error) { +// RunLoad executes the RELOAD opcode +func RunReload(instruction []byte, st state.State, rs resource.Resource, ctx context.Context) (state.State, []byte, error) { head, tail, err := instructionSplit(instruction) if err != nil { return st, instruction, err } - r, err := refresh(head, tail, rs, ctx) + r, err := refresh(head, rs, ctx) if err != nil { return st, tail, err } @@ -158,7 +175,8 @@ func RunReload(instruction []byte, st state.State, rs resource.Fetcher, ctx cont return st, tail, nil } -func RunMove(instruction []byte, st state.State, rs resource.Fetcher, ctx context.Context) (state.State, []byte, error) { +// RunLoad executes the MOVE opcode +func RunMove(instruction []byte, st state.State, rs resource.Resource, ctx context.Context) (state.State, []byte, error) { head, tail, err := instructionSplit(instruction) if err != nil { return st, instruction, err @@ -167,19 +185,22 @@ func RunMove(instruction []byte, st state.State, rs resource.Fetcher, ctx contex return st, tail, nil } -func RunBack(instruction []byte, st state.State, rs resource.Fetcher, ctx context.Context) (state.State, []byte, error) { +// RunLoad executes the BACK opcode +func RunBack(instruction []byte, st state.State, rs resource.Resource, ctx context.Context) (state.State, []byte, error) { st.Up() return st, instruction, nil } -func refresh(key string, sym []byte, rs resource.Fetcher, ctx context.Context) (string, error) { +// retrieve data for key +func refresh(key string, rs resource.Resource, ctx context.Context) (string, error) { fn, err := rs.FuncFor(key) if err != nil { return "", err } - return fn(sym, ctx) + return fn(ctx) } +// split instruction into symbol and arguments func instructionSplit(b []byte) (string, []byte, error) { if len(b) == 0 { return "", nil, fmt.Errorf("argument is empty") diff --git a/go/vm/vm_test.go b/go/vm/vm_test.go @@ -19,15 +19,15 @@ type TestResource struct { state *state.State } -func getOne(input []byte, ctx context.Context) (string, error) { +func getOne(ctx context.Context) (string, error) { return "one", nil } -func getTwo(input []byte, ctx context.Context) (string, error) { +func getTwo(ctx context.Context) (string, error) { return "two", nil } -func getDyn(input []byte, ctx context.Context) (string, error) { +func getDyn(ctx context.Context) (string, error) { return dynVal, nil } @@ -36,7 +36,7 @@ type TestStatefulResolver struct { } -func (r *TestResource) getEachArg(input []byte, ctx context.Context) (string, error) { +func (r *TestResource) getEachArg(ctx context.Context) (string, error) { return r.state.PopArg() }