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