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
This commit is contained in:
@@ -80,6 +80,7 @@ In [docs/decisions.md](docs/decisions.md):
|
|||||||
- `parent` names the link column and the table alike
|
- `parent` names the link column and the table alike
|
||||||
- After a row, a path names a column or a linked table
|
- 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
|
- 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 table selects by one key column, a code, never a free-form name
|
||||||
- A parent row with no child row is a load error
|
- 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
|
- The choice-of-rows fence guards a data file's root, and requires string fields
|
||||||
|
|||||||
@@ -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
|
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
|
per `..`: `{l..municipality..region.name}`. `.country` after a row still reads the link
|
||||||
inside the row stepped up to. `.country` after a row still reads the link column's cell,
|
column's cell, `NO`.
|
||||||
`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:
|
After a `..`, `.` steps down again, drawing afresh inside the row stepped up to, apart from
|
||||||
select rows before the `..`. `--list` advertises no path with a `..`. At the start of a
|
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)).
|
reference, `..` is the folder above ([References](#references)).
|
||||||
|
|
||||||
### Options and fields
|
### Options and fields
|
||||||
|
|||||||
@@ -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.
|
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
|
## 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
|
Decided 2026-09-27 by the maintainer, for goal 5's one spelling per result; valid while
|
||||||
|
|||||||
@@ -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
|
// 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,
|
// 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
|
// so the same read written twice reads one value; the frames a read opens for the name
|
||||||
// scopes around where it lands (enter).
|
// scopes around where it lands (enter); and the rows drawn below each step up stepped down from.
|
||||||
type drawMemo struct {
|
type drawMemo struct {
|
||||||
variant map[string]node
|
variant map[string]node
|
||||||
value map[string]readValue
|
value map[string]readValue
|
||||||
enteredFrames map[*nameScope]*pickFrame
|
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
|
// groupDraws is what one render's draw group has drawn for its reference paths: a memo, and the
|
||||||
|
|||||||
@@ -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))
|
return nil, fmt.Errorf("%s is not a table, so it has no row to select", selectorOf(seg))
|
||||||
case seg == "..":
|
case seg == "..":
|
||||||
if c, ok := n.(*tableColumn); ok {
|
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) {
|
switch n := n.(type) {
|
||||||
case *folder:
|
case *folder:
|
||||||
@@ -233,7 +233,7 @@ func (t *table) stepUp(rest []string) error {
|
|||||||
case len(rest) == 0 || rest[0] != t.parentT.segment:
|
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)
|
return fmt.Errorf(`".." steps up from %s to its parent table, so name that next: ..%s`, t.segment, t.parentT.segment)
|
||||||
case hasSelector(rest):
|
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
|
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.
|
// 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
|
// 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
|
// 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) {
|
func drawSteps(s *generatorState, n node, steps []pathStep, pins *pinSet, memo *drawMemo, levels []string) (node, *pinSet) {
|
||||||
var climbed *table
|
var climbed *table
|
||||||
for _, st := range steps {
|
for _, st := range steps {
|
||||||
@@ -445,7 +445,7 @@ func drawSteps(s *generatorState, n node, steps []pathStep, pins *pinSet, memo *
|
|||||||
case st.kind == stepParent:
|
case st.kind == stepParent:
|
||||||
climbed = t.parentT
|
climbed = t.parentT
|
||||||
case st.kind == stepChild && climbed != nil:
|
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)
|
n = t.drawStep(s, st, pins)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ func (r readRoute) spelled(ref string) string {
|
|||||||
type tableRead struct {
|
type tableRead struct {
|
||||||
headTable *table
|
headTable *table
|
||||||
pins pinSet
|
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
|
drawn map[*table]bool
|
||||||
sels []tableSel
|
sels []tableSel
|
||||||
landsRow bool
|
landsRow bool
|
||||||
@@ -253,27 +253,29 @@ func tableReadOf(head node, a arm, leaf node) *tableRead {
|
|||||||
r := &tableRead{headTable: t}
|
r := &tableRead{headTable: t}
|
||||||
r.leafPins = &r.pins
|
r.leafPins = &r.pins
|
||||||
_, r.landsRow = leaf.(*tableRow)
|
_, r.landsRow = leaf.(*tableRow)
|
||||||
climbed := false
|
var climbed *table
|
||||||
for _, st := range a.steps {
|
for _, st := range a.steps {
|
||||||
if climbed && st.kind != stepChild && st.kind != stepParent {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch st.kind {
|
switch st.kind {
|
||||||
case stepParent:
|
case stepParent:
|
||||||
t, climbed = t.parentT, true
|
t, climbed = t.parentT, t.parentT
|
||||||
r.leafPins = r.leafPins.above(t)
|
case stepChild:
|
||||||
|
if climbed != nil {
|
||||||
|
r.leafPins, climbed = r.leafPins.above(climbed), nil
|
||||||
|
}
|
||||||
|
t = t.descendant(st.name)
|
||||||
case stepSelect:
|
case stepSelect:
|
||||||
r.pins.pin(t, st.row)
|
r.pins.pin(t, st.row)
|
||||||
r.sels = append(r.sels, tableSel{t, joinSegments(append([]string{a.writtenHead}, a.tail[:st.at+1]...))})
|
r.sels = append(r.sels, tableSel{t, joinSegments(append([]string{a.writtenHead}, a.tail[:st.at+1]...))})
|
||||||
case stepDraw:
|
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 {
|
if r.drawn == nil {
|
||||||
r.drawn = map[*table]bool{}
|
r.drawn = map[*table]bool{}
|
||||||
}
|
}
|
||||||
for d, stop := t, r.pins.nearestPinned(t); d != stop; d = d.parentT {
|
for d, stop := t, r.pins.nearestPinned(t); d != stop; d = d.parentT {
|
||||||
r.drawn[d] = true
|
r.drawn[d] = true
|
||||||
}
|
}
|
||||||
case stepChild:
|
|
||||||
t = t.descendant(st.name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
|
|||||||
@@ -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,
|
// 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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
group := sc.groupDraws()
|
||||||
|
group.pins = pins
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case *table:
|
case *table:
|
||||||
n.drawIn(s, &sc.groupDraws().pins)
|
n.drawIn(s, &group.pins)
|
||||||
return n, nil
|
return n, nil
|
||||||
case *tableRow:
|
case *tableRow:
|
||||||
return n.t, nil
|
return n.t, nil
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func (f *Generator) Fake(path string) (string, error) {
|
|||||||
f.loadShippedAt(segments)
|
f.loadShippedAt(segments)
|
||||||
var draws renderDraws
|
var draws renderDraws
|
||||||
sc := renderScope{draws: &draws}
|
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 {
|
if err != nil {
|
||||||
return "", fmt.Errorf("fejkdata: %s: %w", path, err)
|
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)
|
return "", fmt.Errorf("fejkdata: %s names a folder, not a value", path)
|
||||||
}
|
}
|
||||||
group := sc.groupDraws()
|
group := sc.groupDraws()
|
||||||
|
group.pins = pins
|
||||||
sc, _ = sc.at(n, &group.pins).enter(n, &group.memo)
|
sc, _ = sc.at(n, &group.pins).enter(n, &group.memo)
|
||||||
return render(f.rand, n, sc), nil
|
return render(f.rand, n, sc), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// descend walks a caller's path to the node it names, pinning in sc the rows it
|
// descend walks a caller's path to the node it names, returning the rows its leaf renders in.
|
||||||
// selects or draws.
|
func descend(s *generatorState, root node, segments []string) (node, pinSet, error) {
|
||||||
func descend(s *generatorState, root node, segments []string, sc renderScope) (node, error) {
|
|
||||||
// docs/decisions.md#a-path-is-walked-once-without-drawing-before-it-is-walked-for-real
|
// docs/decisions.md#a-path-is-walked-once-without-drawing-before-it-is-walked-for-real
|
||||||
var buf [16]pathStep
|
var buf [16]pathStep
|
||||||
steps, err := probePath(root, segments, buf[:0])
|
steps, err := probePath(root, segments, buf[:0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, pinSet{}, err
|
||||||
}
|
}
|
||||||
group := sc.groupDraws()
|
var pins pinSet
|
||||||
n, pins := drawSteps(s, root, steps, &group.pins, nil, nil)
|
n, leafPins := drawSteps(s, root, steps, &pins, nil, nil)
|
||||||
// A caller's path is its render's first draw, so the rows a step down after a step up drew
|
return n, *leafPins, nil
|
||||||
// are all the render pins.
|
|
||||||
group.pins = *pins
|
|
||||||
return n, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderOnce renders n as one render, over draws of its own.
|
// renderOnce renders n as one render, over draws of its own.
|
||||||
|
|||||||
+15
-6
@@ -237,11 +237,13 @@ func TestTableDescendsToALinkedTable(t *testing.T) {
|
|||||||
|
|
||||||
func TestTableStepsUpToTheRowItLinksTo(t *testing.T) {
|
func TestTableStepsUpToTheRowItLinksTo(t *testing.T) {
|
||||||
files := with(geo(), map[string]string{
|
files := with(geo(), map[string]string{
|
||||||
"drawn.json": `"{/locality.code}|{/locality..municipality.code}|{/locality..municipality..region.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}"`,
|
"named.json": `"{/locality as l}{l.code}|{l..municipality.code}|{l..municipality..region.code}"`,
|
||||||
"down.json": `"{/locality.code}|{/locality..municipality.locality.code}"`,
|
"down.json": `"{/locality.code}|{/locality..municipality.locality.code}"`,
|
||||||
"named-down.json": `"{/locality as l}{l.code}|{l..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}"}`,
|
"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))
|
f := newGenerator(t, writeFiles(t, files), WithSeed(4))
|
||||||
for path, want := range map[string]string{
|
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" {
|
if got := fakeTemplate(t, f, "{/locality[L4]..municipality.name}"); got != "Lund" {
|
||||||
t.Errorf("{/locality[L4]..municipality.name} = %q, want Lund", got)
|
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{}
|
afresh := map[string]bool{}
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
for _, path := range []string{"drawn", "named"} {
|
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)
|
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")
|
down, err := f.FakeRecord("locality[L4]..municipality.locality")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -313,7 +322,7 @@ func TestTableStepUpRefusals(t *testing.T) {
|
|||||||
"locality[L4]..region.name": "..municipality",
|
"locality[L4]..region.name": "..municipality",
|
||||||
"region[12]..municipality.name": "no parent",
|
"region[12]..municipality.name": "no parent",
|
||||||
"locality[L4]..": "..municipality",
|
"locality[L4]..": "..municipality",
|
||||||
"locality[L4]..municipality[0180]": "selects its rows before",
|
"locality[L4]..municipality[0180]": "municipality[0180]",
|
||||||
"locality[L4].name..municipality": "column",
|
"locality[L4].name..municipality": "column",
|
||||||
"x..y": "not a table",
|
"x..y": "not a table",
|
||||||
} {
|
} {
|
||||||
|
|||||||
Reference in New Issue
Block a user