go-vise

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

commit 503e6d0eabf40c53b27c24281ee933fa3fb99074
parent ac6d8342ee5f47b4e58c71c2737b2ab54e76426b
Author: lash <dev@holbrook.no>
Date:   Wed, 26 Apr 2023 07:38:27 +0100

Implement menu as multi-page sink

Diffstat:
MREADME.md | 3+--
Masm/asm_test.go | 1+
Mcache/cache.go | 5++++-
Mrender/menu.go | 30++++++++++++++++++++++++++----
Mrender/page.go | 37++++++++++++++++++++++++++++++++-----
Mrender/page_test.go | 3++-
Mrender/size_test.go | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mvm/debug.go | 11+++--------
Mvm/debug_test.go | 5+++--
Mvm/opcodes.go | 6+++---
Mvm/runner.go | 10+++++-----
Mvm/vm.go | 19++++++++++---------
12 files changed, 180 insertions(+), 40 deletions(-)

diff --git a/README.md b/README.md @@ -55,8 +55,7 @@ The VM defines the following opcode symbols, alphabetically: * `MOUT <choice> <display>` - Add menu display entry. Each entry should have a matching `INCMP` whose `arg` matches `choice`. `display` is a descriptive text of the menu item. * `MOVE <symbol>` - Create a new execution frame, invalidating all previous `MAP` calls. * `MPREV <choice> <display>` - Define how to display the choice for returning when browsing menu. -* `MSEP` - **Not yet implemented**. Marker for menu page separation. Incompatible with browseable nodes. -* `MSIZE <max> <min>` - **Not yet implemented**. Set min and max display size of menu part to `num` bytes. +* `MSINK` - If set, the menu is defined as the browseable content sink. Cannot be used with an active `MAP` of a symbol with `LOAD` size `0`. * `RELOAD <symbol>` - Execute a code symbol already loaded by `LOAD` and cache the data, constrained to the previously given `size` for the same symbol. See "External code" below. diff --git a/asm/asm_test.go b/asm/asm_test.go @@ -253,6 +253,7 @@ func TestParserCapQuote(t *testing.T) { b := vm.NewLine(nil, vm.MOUT, []string{"a", "foo"}, nil, nil) b = vm.NewLine(b, vm.MOUT, []string{"b", "Bar"}, nil, nil) b = vm.NewLine(b, vm.MOUT, []string{"c", "baz"}, nil, nil) + b = vm.NewLine(b, vm.MSINK, nil, nil, nil) s, err := vm.ToString(b) log.Printf("parsing:\n%s", s) diff --git a/cache/cache.go b/cache/cache.go @@ -116,9 +116,12 @@ func(ca *Cache) Update(key string, value string) error { // Fails if key has not been loaded. func(ca *Cache) Get(key string) (string, error) { i := ca.frameOf(key) + if i == -1 { + return "", fmt.Errorf("key '%s' not found in any frame", key) + } r, ok := ca.Cache[i][key] if !ok { - return "", fmt.Errorf("unknown key: %s", key) + return "", fmt.Errorf("unknown key '%s'", key) } return r, nil } diff --git a/render/menu.go b/render/menu.go @@ -46,11 +46,14 @@ type Menu struct { canPrevious bool // availability flag for the "previous" browse option. //outputSize uint16 // maximum size constraint for the menu. sink bool + keep bool } // NewMenu creates a new Menu with an explicit page count. func NewMenu() *Menu { - return &Menu{} + return &Menu{ + keep: true, + } } // WithBrowseConfig defines the criteria for page browsing. @@ -66,6 +69,20 @@ func(m *Menu) WithPages() *Menu { return m } +func(m *Menu) WithSink() *Menu { + m.sink = true + return m +} + +func(m *Menu) WithDispose() *Menu { + m.keep = false + return m +} + +func(m Menu) IsSink() bool { + return m.sink +} + // WithSize defines the maximum byte size of the rendered menu. //func(m *Menu) WithOutputSize(outputSize uint16) *Menu { // m.outputSize = outputSize @@ -129,8 +146,10 @@ func(m *Menu) Sizes() ([4]uint32, error) { // After this has been executed, the state of the menu will be empty. func(m *Menu) Render(idx uint16) (string, error) { var menuCopy [][2]string - for _, v := range m.menu { - menuCopy = append(menuCopy, v) + if m.keep { + for _, v := range m.menu { + menuCopy = append(menuCopy, v) + } } err := m.applyPage(idx) @@ -150,7 +169,9 @@ func(m *Menu) Render(idx uint16) (string, error) { } r += fmt.Sprintf("%s:%s", choice, title) } - m.menu = menuCopy + if m.keep { + m.menu = menuCopy + } return r, nil } @@ -213,5 +234,6 @@ func(m *Menu) reset() { func(m *Menu) Reset() { m.menu = [][2]string{} + m.sink = false m.reset() } diff --git a/render/page.go b/render/page.go @@ -20,6 +20,7 @@ type Page struct { sink *string // Content symbol rendered by dynamic size. sizer *Sizer // Process size constraints. err error // Error state to prepend to output. + extra string // Extra content to append to received template } // NewPage creates a new Page object. @@ -150,6 +151,7 @@ func(pg *Page) RenderTemplate(ctx context.Context, sym string, values map[string if err != nil { return "", err } + tpl += pg.extra if pg.err != nil { derr := pg.Error() Logg.DebugCtxf(ctx, "prepending error", "err", pg.err, "display", derr) @@ -174,7 +176,6 @@ func(pg *Page) RenderTemplate(ctx context.Context, sym string, values map[string return "", err } - b := bytes.NewBuffer([]byte{}) err = tp.Execute(b, values) if err != nil { @@ -200,6 +201,7 @@ func(pg *Page) Render(ctx context.Context, sym string, idx uint16) (string, erro // It clears mappings and removes the sink definition. func(pg *Page) Reset() { pg.sink = nil + pg.extra = "" pg.cacheMap = make(map[string]string) if pg.menu != nil { pg.menu.Reset() @@ -256,7 +258,7 @@ func(pg *Page) joinSink(sinkValues []string, remaining uint32, menuSizes [4]uint for i, v := range sinkValues { l += len(v) - Logg.Tracef("processing sink", "idx", i, "value", v) + Logg.Tracef("processing sink", "idx", i, "value", v, "netremaining", netRemaining, "l", l) if uint32(l) > netRemaining - 1 { if tb.Len() == 0 { return "", 0, fmt.Errorf("capacity insufficient for sink field %v", i) @@ -289,6 +291,15 @@ func(pg *Page) joinSink(sinkValues []string, remaining uint32, menuSizes [4]uint return r, count, nil } +func(pg *Page) applyMenuSink(ctx context.Context) ([]string, error) { + s, err := pg.menu.WithDispose().WithPages().Render(0) + if err != nil { + return nil, err + } + values := strings.Split(s, "\n") + return values, nil +} + // render menu and all syms except sink, split sink into display chunks func(pg *Page) prepare(ctx context.Context, sym string, values map[string]string, idx uint16) (map[string]string, error) { if pg.sizer == nil { @@ -297,10 +308,25 @@ func(pg *Page) prepare(ctx context.Context, sym string, values map[string]string // extract sink values noSinkValues, sink, sinkValues, err := pg.split(sym, values) + if err != nil { + return nil, err + } // check if menu is sink aswell, fail if it is. if pg.menu != nil { - // if pg.menu.ReservedSize() + if pg.menu.IsSink() { + if sink != "" { + return values, fmt.Errorf("cannot use menu as sink when sink already mapped") + } + sinkValues, err = pg.applyMenuSink(ctx) + if err != nil { + return nil, err + } + sink = "_menu" + pg.extra = "\n{{._menu}}" + pg.sizer.sink = sink + noSinkValues[sink] = "" + } } // pre-render template without sink @@ -363,8 +389,9 @@ func(pg *Page) render(ctx context.Context, sym string, values map[string]string, if err != nil { return "", err } - Logg.Debugf("rendered menu", "bytes", len(s)) - if len(s) > 0 { + l := len(s) + Logg.Debugf("rendered menu", "bytes", l) + if l > 0 { r += "\n" + s } } diff --git a/render/page_test.go b/render/page_test.go @@ -124,7 +124,7 @@ func TestWithError(t *testing.T) { pg := NewPage(ca, rs) ca.Push() - mn := NewMenu() //.WithOutputSize(32) + mn := NewMenu() err := mn.Put("0", "aiee") if err != nil { t.Fatal(err) @@ -144,3 +144,4 @@ bar t.Fatalf("expected:\n\t%s\ngot:\n\t%s", expect, r) } } + diff --git a/render/size_test.go b/render/size_test.go @@ -297,3 +297,93 @@ func TestManySizesMenued(t *testing.T) { } } } + +func TestMenuCollideSink(t *testing.T) { + ca := cache.NewCache() + rs := resource.NewMemResource() + rs.AddTemplate("foo", "bar") + szr := NewSizer(30) + pg := NewPage(ca, rs).WithSizer(szr) + ca.Push() + + ca.Add("inky", "pinky", 5) + ca.Add("blinky", "clyde", 0) + pg.Map("inky") + + mn := NewMenu().WithSink() + pg = pg.WithMenu(mn) + + var err error + ctx := context.TODO() + _, err = pg.Render(ctx, "foo", 0) + if err != nil { + t.Fatal(err) + } + + mn = NewMenu().WithSink() + pg = pg.WithMenu(mn) + pg.Map("blinky") + _, err = pg.Render(ctx, "foo", 0) + if err == nil { + t.Fatalf("expected error") + } +} + +func TestMenuSink(t *testing.T) { + ca := cache.NewCache() + rs := resource.NewMemResource() + rs.AddTemplate("foo", "bar {{.baz}}") + szr := NewSizer(30) + + mn := NewMenu().WithSink() + mn.Put("0", "inky") + mn.Put("1", "pinky") + mn.Put("22", "blinky") + mn.Put("3", "clyde") + mn.Put("44", "tinkywinky") + + pg := NewPage(ca, rs).WithSizer(szr).WithMenu(mn) + ca.Push() + + ca.Add("baz", "xyzzy", 5) + pg.Map("baz") + + var err error + ctx := context.TODO() + r, err := pg.Render(ctx, "foo", 0) + if err != nil { + t.Fatal(err) + } + expect := `bar xyzzy +0:inky +1:pinky` + if r != expect { + t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) + } + + mn = NewMenu().WithSink() + mn.Put("0", "inky") + mn.Put("1", "pinky") + mn.Put("22", "blinky") + mn.Put("3", "clyde") + mn.Put("44", "tinkywinky") + + pg = NewPage(ca, rs).WithSizer(szr).WithMenu(mn) + ca.Push() + + ca.Add("baz", "xyzzy", 5) + pg.Map("baz") + + r, err = pg.Render(ctx, "foo", 1) + if err != nil { + t.Fatal(err) + } + expect = `bar xyzzy +22:blinky +3:clyde` + if r != expect { + t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) + } +} + + diff --git a/vm/debug.go b/vm/debug.go @@ -112,14 +112,9 @@ func ParseAll(b []byte, w io.Writer) (int, error) { case HALT: b, err = ParseHalt(b) rs = "HALT\n" - case MSIZE: - r, v, bb, err := ParseMSize(b) - b = bb - if err == nil { - if w != nil { - rs = fmt.Sprintf("%s %v %v\n", s, r, v) - } - } + case MSINK: + b, err = ParseMSink(b) + rs = "MSINK\n" case MOUT: r, v, bb, err := ParseMOut(b) b = bb diff --git a/vm/debug_test.go b/vm/debug_test.go @@ -121,12 +121,13 @@ func TestToString(t *testing.T) { t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r) } - b = NewLine(nil, MSIZE, nil, nil, []uint8{0x42, 0x2a}) + b = NewLine(nil, MSINK, nil, nil, nil) //[]uint8{0x42, 0x2a}) r, err = ToString(b) if err != nil { t.Fatal(err) } - expect = "MSIZE 66 42\n" + //expect = "MSIZE 66 42\n" + expect = "MSINK\n" if r != expect { t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r) } diff --git a/vm/opcodes.go b/vm/opcodes.go @@ -15,7 +15,7 @@ const ( MOVE = 6 HALT = 7 INCMP = 8 - MSIZE = 9 + MSINK = 9 MOUT = 10 MNEXT = 11 MPREV = 12 @@ -33,7 +33,7 @@ var ( MOVE: "MOVE", HALT: "HALT", INCMP: "INCMP", - MSIZE: "MSIZE", + MSINK: "MSINK", MOUT: "MOUT", MNEXT: "MNEXT", MPREV: "MPREV", @@ -49,7 +49,7 @@ var ( "MOVE": MOVE, "HALT": HALT, "INCMP": INCMP, - "MSIZE": MSIZE, + "MSINK": MSINK, "MOUT": MOUT, "MNEXT": MNEXT, "MPREV": MPREV, diff --git a/vm/runner.go b/vm/runner.go @@ -127,8 +127,8 @@ func(vm *Vm) Run(ctx context.Context, b []byte) ([]byte, error) { b, err = vm.runMove(ctx, b) case INCMP: b, err = vm.runInCmp(ctx, b) - case MSIZE: - b, err = vm.runMSize(ctx, b) + case MSINK: + b, err = vm.runMSink(ctx, b) case MOUT: b, err = vm.runMOut(ctx, b) case MNEXT: @@ -404,9 +404,9 @@ func(vm *Vm) runHalt(ctx context.Context, b []byte) ([]byte, error) { } // executes the MSIZE opcode -func(vm *Vm) runMSize(ctx context.Context, b []byte) ([]byte, error) { - Logg.WarnCtxf(ctx, "MSIZE not yet implemented") - _, _, b, err := ParseMSize(b) +func(vm *Vm) runMSink(ctx context.Context, b []byte) ([]byte, error) { + b, err := ParseMSink(b) + vm.mn = vm.mn.WithSink() return b, err } diff --git a/vm/vm.go b/vm/vm.go @@ -85,15 +85,16 @@ func ParseMNext(b []byte) (string, string, []byte, error) { return parseTwoSym(b) } -// ParseMSize parses and extracts the expected argument portion of a MSIZE instruction -func ParseMSize(b []byte) (uint32, uint32, []byte, error) { - if len(b) < 2 { - return 0, 0, b, fmt.Errorf("argument too short") - } - r := uint32(b[0]) - rr := uint32(b[1]) - b = b[2:] - return r, rr, b, nil +// ParseMSink parses and extracts the expected argument portion of a MSINK instruction +func ParseMSink(b []byte) ([]byte, error) { + return parseNoArg(b) +// if len(b) < 2 { +// return 0, 0, b, fmt.Errorf("argument too short") +// } +// r := uint32(b[0]) +// rr := uint32(b[1]) +// b = b[2:] +// return b, nil } // ParseMOut parses and extracts the expected argument portion of a MOUT instruction