From 1ec2bee098edc4258a0957259915bccaf10f3f24 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 5 Oct 2026 00:05:58 +0200 Subject: [PATCH] Answer architecture round 1: share a step down's draw per draw group and pick, own descend's pins, name the spelling to write, and record the .. decision --- AGENTS.md | 1 + README.md | 16 +++++++++++----- docs/decisions.md | 16 ++++++++++++++++ draws.go | 22 ++++++++++++++++++++-- path.go | 10 +++++----- reads.go | 20 +++++++++++--------- record.go | 8 +++++--- render.go | 19 ++++++++----------- table_test.go | 21 +++++++++++++++------ 9 files changed, 92 insertions(+), 41 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f9974c9..9df76c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,6 +80,7 @@ In [docs/decisions.md](docs/decisions.md): - `parent` names the link column and the table alike - After a row, a path names a column or a linked table - A selected row is fixed, not drawn, and an unnamed read beside it is refused +- `..` steps up to the parent row, and a step down after it draws once per draw group or pick - A table selects by one key column, a code, never a free-form name - A parent row with no child row is a load error - The choice-of-rows fence guards a data file's root, and requires string fields diff --git a/README.md b/README.md index cd53345..a961750 100644 --- a/README.md +++ b/README.md @@ -585,11 +585,17 @@ fejkdata -d ./mydata '{/city as c}{c}, {c..country.name}' # Oslo, Norway ``` It steps up from a selected row, a drawn row or a [name](#names) bound to a row, one level -per `..`: `{l..municipality..region.name}`. After it, `.` steps down again, drawing -inside the row stepped up to. `.country` after a row still reads the link column's cell, -`NO`. A `..` that names anything but the parent table is an error naming the parent, and -so is a selector after a `..`, which could name a row outside the one stepped up to: -select rows before the `..`. `--list` advertises no path with a `..`. At the start of a +per `..`: `{l..municipality..region.name}`. `.country` after a row still reads the link +column's cell, `NO`. + +After a `..`, `.` steps down again, drawing afresh inside the row stepped up to, apart from +the row the path stepped up from. Paths stepping down from one `..` read one draw, as one +reference path does: `{/city..country.city.name}` and `{/city..country.city.population}` +describe one city, within a render and [draw group](#draw-group), or within a name's pick. + +A `..` that names anything but the parent table is an error naming the parent. A selector +after a `..` is an error naming the table to select from, since it could name a row outside +the one stepped up to. `--list` advertises no path with a `..`. At the start of a reference, `..` is the folder above ([References](#references)). ### Options and fields diff --git a/docs/decisions.md b/docs/decisions.md index dbe5b36..058a33f 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -511,6 +511,22 @@ case but the last, where two expected a render and all three accepted the refusa Goal 5.2 overrides its shared draw beneath a selection and its refusal of an unnamed read; `todo.md` item 4 replaces them. +## `..` steps up to the parent row, and a step down after it draws once per draw group or pick + +2026-10-05, larv-review on `todo.md` item 2. Goal 5.4 has `..` go up one level, and goal 5.2 +has each `{…}` make a new pick, which `todo.md` item 4 brings to every path. + +- `..` names the parent table next, `city..country`, as a folder path names the folder it + enters. `.country` after a row stays the link column's cell. +- A step down after a `..` draws afresh inside the row stepped up to (`drawSteps`), and the + draw fences leave it out (`tableReadOf`), since it pins none of the render's rows. Paths + stepping down from one `..` share that draw through their memo (`drawMemo.stepDownPins`), + as reference paths share a draw today, so two reads of one step down describe one row. +- A route through another row is another path to the same data, not a second spelling: + `city[Oslo]..country.name` and `country[NO].name` both load. + +Valid until item 4 makes every `{…}` draw afresh, which revises the shared step down. + ## A table selects by one key column, a code, never a free-form name Decided 2026-09-27 by the maintainer, for goal 5's one spelling per result; valid while diff --git a/draws.go b/draws.go index bfa6c2b..1dd17cc 100644 --- a/draws.go +++ b/draws.go @@ -4,12 +4,30 @@ import "strings" // drawMemo is what a hold, a draw group or a named pick has drawn: the variant each level was // drawn as, so every path under it reads one variant; the value each read produced, by its path, -// so the same read written twice reads one value; and the frames a read opens for the name -// scopes around where it lands (enter). +// so the same read written twice reads one value; the frames a read opens for the name +// scopes around where it lands (enter); and the rows drawn below each step up stepped down from. type drawMemo struct { variant map[string]node value map[string]readValue enteredFrames map[*nameScope]*pickFrame + steppedDown map[string]*pinSet +} + +// stepDownPins is the pins a path steps down into after stepping up to t, at the level at: kept +// in m where there is one, so every path stepping down there reads one draw. +func (m *drawMemo) stepDownPins(pins *pinSet, t *table, levels []string, at int) *pinSet { + if m == nil { + return pins.above(t) + } + p, ok := m.steppedDown[levels[at]] + if !ok { + p = pins.above(t) + if m.steppedDown == nil { + m.steppedDown = map[string]*pinSet{} + } + m.steppedDown[levels[at]] = p + } + return p } // groupDraws is what one render's draw group has drawn for its reference paths: a memo, and the diff --git a/path.go b/path.go index 399ec84..6247c82 100644 --- a/path.go +++ b/path.go @@ -155,9 +155,9 @@ func stepInto(n node, seg string) (node, error) { return nil, fmt.Errorf("%s is not a table, so it has no row to select", selectorOf(seg)) case seg == "..": if c, ok := n.(*tableColumn); ok { - return nil, fmt.Errorf(`".." steps up from a table's row, and %q is a column`, c.t.header[c.i]) + return nil, fmt.Errorf(`".." steps up from a table's row, and %q is a column; put the ".." right after the row`, c.t.header[c.i]) } - return nil, fmt.Errorf(`".." steps up from a table's row to the row it links to, and what it follows is not a table`) + return nil, fmt.Errorf(`".." steps up from a table's row to the row it links to, and what it follows is not a table; step up only from a table`) } switch n := n.(type) { case *folder: @@ -233,7 +233,7 @@ func (t *table) stepUp(rest []string) error { case len(rest) == 0 || rest[0] != t.parentT.segment: return fmt.Errorf(`".." steps up from %s to its parent table, so name that next: ..%s`, t.segment, t.parentT.segment) case hasSelector(rest): - return fmt.Errorf(`a path selects its rows before a "..", since a row selected after it could lie outside the row it steps up to; start a path at the table to select from it`) + return fmt.Errorf(`a path selects its rows before a "..", since a row selected after it could lie outside the row it steps up to; select from the table instead: %s`, joinSegments(append([]string{t.parentT.path}, rest[1:]...))) } return nil } @@ -423,7 +423,7 @@ func probePath(n node, tail []string, steps []pathStep) ([]pathStep, error) { // pins is nil for a sibling path, which never crosses a table, since a table is only a category. // For a memoized read, memo keeps the variant drawn at each of levels, so paths sharing a prefix // share it. It returns the leaf and the pins its row is in: a step down after a step up draws -// afresh, into pins of its own. +// afresh, into pins of its own, which memo shares with every path stepping down there. func drawSteps(s *generatorState, n node, steps []pathStep, pins *pinSet, memo *drawMemo, levels []string) (node, *pinSet) { var climbed *table for _, st := range steps { @@ -445,7 +445,7 @@ func drawSteps(s *generatorState, n node, steps []pathStep, pins *pinSet, memo * case st.kind == stepParent: climbed = t.parentT case st.kind == stepChild && climbed != nil: - pins, climbed = pins.above(climbed), nil + pins, climbed = memo.stepDownPins(pins, climbed, levels, st.at), nil } n = t.drawStep(s, st, pins) } diff --git a/reads.go b/reads.go index 0c050be..4acf8fe 100644 --- a/reads.go +++ b/reads.go @@ -230,7 +230,7 @@ func (r readRoute) spelled(ref string) string { type tableRead struct { headTable *table pins pinSet - leafPins *pinSet // the rows the leaf renders in: pins, short of the rows below a step up + leafPins *pinSet // the rows the leaf renders in: pins, short of the rows a step down after a step up draws afresh drawn map[*table]bool sels []tableSel landsRow bool @@ -253,27 +253,29 @@ func tableReadOf(head node, a arm, leaf node) *tableRead { r := &tableRead{headTable: t} r.leafPins = &r.pins _, r.landsRow = leaf.(*tableRow) - climbed := false + var climbed *table for _, st := range a.steps { - if climbed && st.kind != stepChild && st.kind != stepParent { - continue - } switch st.kind { case stepParent: - t, climbed = t.parentT, true - r.leafPins = r.leafPins.above(t) + t, climbed = t.parentT, t.parentT + case stepChild: + if climbed != nil { + r.leafPins, climbed = r.leafPins.above(climbed), nil + } + t = t.descendant(st.name) case stepSelect: r.pins.pin(t, st.row) r.sels = append(r.sels, tableSel{t, joinSegments(append([]string{a.writtenHead}, a.tail[:st.at+1]...))}) case stepDraw: + if r.leafPins != &r.pins { + continue // a step down after a step up draws apart from the render's pins + } if r.drawn == nil { r.drawn = map[*table]bool{} } for d, stop := t, r.pins.nearestPinned(t); d != stop; d = d.parentT { r.drawn[d] = true } - case stepChild: - t = t.descendant(st.name) } } return r diff --git a/record.go b/record.go index b459301..27186a3 100644 --- a/record.go +++ b/record.go @@ -152,15 +152,17 @@ func (f *Generator) FakeRecord(path string) (*Record, error) { } // tableRecord walks a path's tail from a table to the table whose row is the record, -// pinning the rows it selects or draws. +// pinning in sc the rows it selects or draws. func tableRecord(s *generatorState, t *table, tail []string, sc renderScope) (node, error) { - n, err := descend(s, t, tail, sc) + n, pins, err := descend(s, t, tail) if err != nil { return nil, err } + group := sc.groupDraws() + group.pins = pins switch n := n.(type) { case *table: - n.drawIn(s, &sc.groupDraws().pins) + n.drawIn(s, &group.pins) return n, nil case *tableRow: return n.t, nil diff --git a/render.go b/render.go index 889617c..d862e96 100644 --- a/render.go +++ b/render.go @@ -27,7 +27,7 @@ func (f *Generator) Fake(path string) (string, error) { f.loadShippedAt(segments) var draws renderDraws sc := renderScope{draws: &draws} - n, err := descend(f.rand, &f.root, segments, sc) + n, pins, err := descend(f.rand, &f.root, segments) if err != nil { return "", fmt.Errorf("fejkdata: %s: %w", path, err) } @@ -35,25 +35,22 @@ func (f *Generator) Fake(path string) (string, error) { return "", fmt.Errorf("fejkdata: %s names a folder, not a value", path) } group := sc.groupDraws() + group.pins = pins sc, _ = sc.at(n, &group.pins).enter(n, &group.memo) return render(f.rand, n, sc), nil } -// descend walks a caller's path to the node it names, pinning in sc the rows it -// selects or draws. -func descend(s *generatorState, root node, segments []string, sc renderScope) (node, error) { +// descend walks a caller's path to the node it names, returning the rows its leaf renders in. +func descend(s *generatorState, root node, segments []string) (node, pinSet, error) { // docs/decisions.md#a-path-is-walked-once-without-drawing-before-it-is-walked-for-real var buf [16]pathStep steps, err := probePath(root, segments, buf[:0]) if err != nil { - return nil, err + return nil, pinSet{}, err } - group := sc.groupDraws() - n, pins := drawSteps(s, root, steps, &group.pins, nil, nil) - // A caller's path is its render's first draw, so the rows a step down after a step up drew - // are all the render pins. - group.pins = *pins - return n, nil + var pins pinSet + n, leafPins := drawSteps(s, root, steps, &pins, nil, nil) + return n, *leafPins, nil } // renderOnce renders n as one render, over draws of its own. diff --git a/table_test.go b/table_test.go index 2e852bd..68d0748 100644 --- a/table_test.go +++ b/table_test.go @@ -237,11 +237,13 @@ func TestTableDescendsToALinkedTable(t *testing.T) { func TestTableStepsUpToTheRowItLinksTo(t *testing.T) { files := with(geo(), map[string]string{ - "drawn.json": `"{/locality.code}|{/locality..municipality.code}|{/locality..municipality..region.code}"`, - "named.json": `"{/locality as l}{l.code}|{l..municipality.code}|{l..municipality..region.code}"`, - "down.json": `"{/locality.code}|{/locality..municipality.locality.code}"`, - "named-down.json": `"{/locality as l}{l.code}|{l..municipality.locality.code}"`, - "record.json": `{"format":"","l":"{/locality.code}","m":"{/locality..municipality.code}"}`, + "drawn.json": `"{/locality.code}|{/locality..municipality.code}|{/locality..municipality..region.code}"`, + "named.json": `"{/locality as l}{l.code}|{l..municipality.code}|{l..municipality..region.code}"`, + "down.json": `"{/locality.code}|{/locality..municipality.locality.code}"`, + "named-down.json": `"{/locality as l}{l.code}|{l..municipality.locality.code}"`, + "twice.json": `"{/locality..municipality.locality.code}|{/locality..municipality.locality.name}"`, + "named-twice.json": `"{/locality as l}{l..municipality.locality.code}|{l..municipality.locality.name}"`, + "record.json": `{"format":"","l":"{/locality.code}","m":"{/locality..municipality.code}"}`, }) f := newGenerator(t, writeFiles(t, files), WithSeed(4)) for path, want := range map[string]string{ @@ -259,6 +261,7 @@ func TestTableStepsUpToTheRowItLinksTo(t *testing.T) { if got := fakeTemplate(t, f, "{/locality[L4]..municipality.name}"); got != "Lund" { t.Errorf("{/locality[L4]..municipality.name} = %q, want Lund", got) } + names := map[string]string{"L1": "Stockholm", "L2": "Solna", "L3": "Malmö", "L4": "Lund", "L5": "Göteborg", "L6": "Hisingen", "L7": "Sandby", "L8": "Sandby"} afresh := map[string]bool{} for i := 0; i < 200; i++ { for _, path := range []string{"drawn", "named"} { @@ -281,6 +284,12 @@ func TestTableStepsUpToTheRowItLinksTo(t *testing.T) { t.Fatalf("%s = %v, want a locality of the first one's municipality", path, parts) } } + for _, path := range []string{"twice", "named-twice"} { + parts := strings.Split(fake(t, f, path), "|") + if names[parts[0]] != parts[1] { + t.Fatalf("%s = %v, want both reads of one step down to read one locality", path, parts) + } + } down, err := f.FakeRecord("locality[L4]..municipality.locality") if err != nil { t.Fatal(err) @@ -313,7 +322,7 @@ func TestTableStepUpRefusals(t *testing.T) { "locality[L4]..region.name": "..municipality", "region[12]..municipality.name": "no parent", "locality[L4]..": "..municipality", - "locality[L4]..municipality[0180]": "selects its rows before", + "locality[L4]..municipality[0180]": "municipality[0180]", "locality[L4].name..municipality": "column", "x..y": "not a table", } {