Answer stability round 1 and product-owner round 1: List advertises no path into a level carrying a repeat, a refusal that says to read the level whole, and plainer walk-position docs

This commit is contained in:
2026-10-05 21:55:46 +02:00
committed by Lillem4n
parent a9c59ba6b7
commit 964f04c7c8
7 changed files with 52 additions and 43 deletions
+4 -3
View File
@@ -12,9 +12,10 @@ replacement, and each removed path, column or flag.
`{/city[Oslo]..country.name}` renders `Norway`, and `.` after it steps down again, `{/city[Oslo]..country.name}` renders `Norway`, and `.` after it steps down again,
drawing afresh. `New` refuses a `..` naming anything but the parent table, and a drawing afresh. `New` refuses a `..` naming anything but the parent table, and a
selector after a `..`. selector after a `..`.
- A path given to `Fake` or the CLI may not read into a level carrying a - A path, given to `Fake` or the CLI or referenced in a template, may not read into a
`repeat`, as a reference may not: `Fake("x.a")` where `x` carries `"repeat": 3` fails, level carrying a `repeat`, and `List` advertises none that does: where `x` carries
naming the level `x`. `"repeat": 3`, `Fake("x.a")` fails naming the level `x`, and `Fake("x")` renders it
whole.
- Every `{…}` draws afresh, and a name is the one way to keep a pick: `{p.a} {p.b}`, - Every `{…}` draws afresh, and a name is the one way to keep a pick: `{p.a} {p.b}`,
`{w} {uppercase(w)}` and `{/person.first} {/person.last}` are two draws each, which `{w} {uppercase(w)}` and `{/person.first} {/person.last}` are two draws each, which
`{p as q}{q.a} {q.b}` and `{/person as n}{n.first} {n.last}` make one. `{p as q}{q.a} {q.b}` and `{/person as n}{n.first} {n.last}` make one.
+2 -1
View File
@@ -795,7 +795,8 @@ token {p.postal-code}: not every variant of this 2-way choice carries "postal-co
``` ```
The sub-fields stay addressable — `Fake("address.place.locality")` renders, and The sub-fields stay addressable — `Fake("address.place.locality")` renders, and
`List` advertises it. A path may not read into a level carrying a `repeat`. `List` advertises it. A path may not read into a level carrying a `repeat`, and `List`
advertises none that does; read that level whole.
### Performance ### Performance
+35 -31
View File
@@ -113,67 +113,71 @@ func (c config) load() (folder, error) {
} }
// List returns the sorted dotted paths Fake renders: each category and every field, // List returns the sorted dotted paths Fake renders: each category and every field,
// column and linked table below it, a direct descent at a time. A choice consumes no // column and linked table below it, a direct descent at a time, stopping at a level
// segment, so a path continues through one only where every variant carries it. // carrying a repeat. A choice consumes no segment, so a path continues through one
// only where every variant carries it.
func (f *Generator) List() []string { func (f *Generator) List() []string {
f.mu.Lock() f.mu.Lock()
defer f.mu.Unlock() defer f.mu.Unlock()
out := paths(&f.root) out := paths(&f.root, false)
sort.Strings(out) sort.Strings(out)
return out return out
} }
// paths lists the dot paths addressable from n, relative to it, where "" is n // paths lists the dot paths addressable from n, relative to it, where "" is n
// itself. A folder has no value of its own, so it contributes only its children's. // itself. A folder has no value of its own, so it contributes only its children's.
func paths(n node) []string { // intoRepeats lists the fields under a level carrying a repeat too, which a path
// reaches only to be refused.
func paths(n node, intoRepeats bool) []string {
switch n := n.(type) { switch n := n.(type) {
case *folder: case *folder:
var out []string var out []string
for _, name := range sortedNames(n.children) { for _, name := range sortedNames(n.children) {
for _, p := range paths(n.children[name]) { out = appendUnder(out, name, paths(n.children[name], intoRepeats))
out = append(out, join(name, p))
}
} }
for _, name := range sortedNames(n.unloaded) { for _, name := range sortedNames(n.unloaded) {
for _, p := range n.unloaded[name].paths { out = appendUnder(out, name, n.unloaded[name].paths)
out = append(out, join(name, p))
}
} }
return out return out
case *template: case *template:
out := []string{""} out := []string{""}
if n.repeat > 1 && !intoRepeats {
return out
}
for _, name := range sortedNames(n.fields) { for _, name := range sortedNames(n.fields) {
for _, p := range paths(n.fields[name]) { out = appendUnder(out, name, paths(n.fields[name], intoRepeats))
out = append(out, join(name, p))
}
} }
return out return out
case *nullItem:
return []string{""}
case *table: case *table:
return tablePaths(n) return tablePaths(n, intoRepeats)
case *tableColumn, *tableRow: case *tableColumn, *tableRow, *nullItem:
return []string{""} return []string{""}
case *choice: case *choice:
out := []string{""} shared := n.shared
for p := range n.shared { if !intoRepeats {
out = append(out, p) shared = sharedPaths(n.items, false)
} }
return out return append([]string{""}, sortedNames(shared)...)
default: default:
panic(invariant.Broken("paths has no case for node %T", n)) panic(invariant.Broken("paths has no case for node %T", n))
} }
} }
// appendUnder appends each of ps to out under name.
func appendUnder(out []string, name string, ps []string) []string {
for _, p := range ps {
out = append(out, join(name, p))
}
return out
}
// tablePaths is a table's columns, then each table linked to it under its name: the // tablePaths is a table's columns, then each table linked to it under its name: the
// direct descents, a step at a time. // direct descents, a step at a time.
func tablePaths(t *table) []string { func tablePaths(t *table, intoRepeats bool) []string {
out := append([]string{""}, t.rows.Header()...) out := append([]string{""}, t.rows.Header()...)
sort.Strings(out[1:]) sort.Strings(out[1:])
for _, c := range t.rows.Children() { for _, c := range t.rows.Children() {
for _, p := range paths(c.Owner()) { out = appendUnder(out, c.Segment(), paths(c.Owner(), intoRepeats))
out = append(out, join(c.Segment(), p))
}
} }
return out return out
} }
@@ -181,13 +185,13 @@ func tablePaths(t *table) []string {
// sharedPaths is the sub-paths every item carries — the only ones a path may step // sharedPaths is the sub-paths every item carries — the only ones a path may step
// through a choice to reach. It intersects, bailing as soon as the set // through a choice to reach. It intersects, bailing as soon as the set
// is empty, which is immediate for a choice of plain strings. // is empty, which is immediate for a choice of plain strings.
func sharedPaths(items []node) map[string]bool { func sharedPaths(items []node, intoRepeats bool) map[string]bool {
shared := subPaths(items[0]) shared := subPaths(items[0], intoRepeats)
for _, it := range items[1:] { for _, it := range items[1:] {
if len(shared) == 0 { if len(shared) == 0 {
return nil return nil
} }
next := subPaths(it) next := subPaths(it, intoRepeats)
for p := range shared { for p := range shared {
if !next[p] { if !next[p] {
delete(shared, p) delete(shared, p)
@@ -197,10 +201,10 @@ func sharedPaths(items []node) map[string]bool {
return shared return shared
} }
// subPaths is paths(n) as a set, without the empty path that means n itself. // subPaths is paths(n, intoRepeats) as a set, without the empty path that means n itself.
func subPaths(n node) map[string]bool { func subPaths(n node, intoRepeats bool) map[string]bool {
out := map[string]bool{} out := map[string]bool{}
for _, p := range paths(n) { for _, p := range paths(n, intoRepeats) {
if p != "" { if p != "" {
out[p] = true out[p] = true
} }
+1 -1
View File
@@ -266,7 +266,7 @@ func compileChoice(items []any, pos position) (node, error) {
} }
c.cum = cum c.cum = cum
} }
c.shared = sharedPaths(c.items) c.shared = sharedPaths(c.items, true)
return c, nil return c, nil
} }
+8 -5
View File
@@ -142,9 +142,12 @@ func (w *pathCheck) walk(n node, tail []string) (node, error) {
return at.n, nil return at.n, nil
} }
func repeatLevelError(level string) error {
return fmt.Errorf("the level %q carries a repeat, so a path cannot read one draw of it; read %q whole", level, level)
}
// pathPos is where a walk stands: the node reached, the tail left to walk from it, and whether // pathPos is where a walk stands: the node reached, the tail left to walk from it, and whether
// the step into it came from a row of an ancestor table, so a table there reads a row even // it descended there, as route reads it.
// where the tail is empty.
type pathPos struct { type pathPos struct {
n node n node
tail []string tail []string
@@ -154,8 +157,8 @@ type pathPos struct {
func (p pathPos) more() bool { return len(p.tail) > 0 || p.descended } func (p pathPos) more() bool { return len(p.tail) > 0 || p.descended }
// takeStep moves at past the step its tail starts with, from its node, which is no choice, and // takeStep moves at past the step its tail starts with, from its node, which is no choice, and
// appends what it takes to steps: a field, or the route a table passes. whole is the path at's // appends what it takes to steps: a field, or the route a table passes. whole is the full path,
// tail ends, and level names its head in errors. // at.tail its unwalked end, and level names its head in errors.
func takeStep(at *pathPos, whole []string, level string, steps []pathStep, pins *pinSet) ([]pathStep, error) { func takeStep(at *pathPos, whole []string, level string, steps []pathStep, pins *pinSet) ([]pathStep, error) {
i := len(whole) - len(at.tail) i := len(whole) - len(at.tail)
switch x := at.n.(type) { switch x := at.n.(type) {
@@ -168,7 +171,7 @@ func takeStep(at *pathPos, whole []string, level string, steps []pathStep, pins
return routeSteps(steps, pins, x, r, i) return routeSteps(steps, pins, x, r, i)
case *template: case *template:
if x.repeat > 1 && !grammar.IsSelector(at.tail[0]) { if x.repeat > 1 && !grammar.IsSelector(at.tail[0]) {
return steps, fmt.Errorf("the level %q carries a repeat, which a path reading one draw of it cannot apply", join(level, strings.Join(whole[:i], "."))) return steps, repeatLevelError(join(level, strings.Join(whole[:i], ".")))
} }
} }
next, err := stepInto(at.n, at.tail[0]) next, err := stepInto(at.n, at.tail[0])
+1 -1
View File
@@ -109,7 +109,7 @@ func TestNodeSwitchesHandleEveryKind(t *testing.T) {
for name, call := range map[string]func(node){ for name, call := range map[string]func(node){
"columnItems": func(n node) { columnItems(n) }, "columnItems": func(n node) { columnItems(n) },
"contained": func(n node) { contained(n) }, "contained": func(n node) { contained(n) },
"paths": func(n node) { paths(n) }, "paths": func(n node) { paths(n, false) },
"prove": func(n node) { (&valueProof{}).prove(n) }, "prove": func(n node) { (&valueProof{}).prove(n) },
"render": func(n node) { "render": func(n node) {
var frames frameStack var frames frameStack
+1 -1
View File
@@ -72,7 +72,7 @@ func shippedIndexSource(whole *Generator) ([]byte, error) {
if t, isTable := s.n.(*table); isTable && t.rows.Options().Parent != "" { if t, isTable := s.n.(*table); isTable && t.rows.Options().Parent != "" {
fmt.Fprintf(&b, "parent: %q, ", t.rows.Options().Parent) fmt.Fprintf(&b, "parent: %q, ", t.rows.Options().Parent)
} }
ps := paths(s.n) ps := paths(s.n, false)
sort.Strings(ps) sort.Strings(ps)
fmt.Fprintf(&b, "paths: %#v},\n", ps) fmt.Fprintf(&b, "paths: %#v},\n", ps)
} }