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,
drawing afresh. `New` refuses a `..` naming anything but the parent table, and a
selector after a `..`.
- A path given to `Fake` or the CLI may not read into a level carrying a
`repeat`, as a reference may not: `Fake("x.a")` where `x` carries `"repeat": 3` fails,
naming the level `x`.
- A path, given to `Fake` or the CLI or referenced in a template, may not read into a
level carrying a `repeat`, and `List` advertises none that does: where `x` carries
`"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}`,
`{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.
+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
`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
+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,
// column and linked table below it, a direct descent at a time. A choice consumes no
// segment, so a path continues through one only where every variant carries it.
// column and linked table below it, a direct descent at a time, stopping at a level
// 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 {
f.mu.Lock()
defer f.mu.Unlock()
out := paths(&f.root)
out := paths(&f.root, false)
sort.Strings(out)
return out
}
// 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.
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) {
case *folder:
var out []string
for _, name := range sortedNames(n.children) {
for _, p := range paths(n.children[name]) {
out = append(out, join(name, p))
}
out = appendUnder(out, name, paths(n.children[name], intoRepeats))
}
for _, name := range sortedNames(n.unloaded) {
for _, p := range n.unloaded[name].paths {
out = append(out, join(name, p))
}
out = appendUnder(out, name, n.unloaded[name].paths)
}
return out
case *template:
out := []string{""}
if n.repeat > 1 && !intoRepeats {
return out
}
for _, name := range sortedNames(n.fields) {
for _, p := range paths(n.fields[name]) {
out = append(out, join(name, p))
}
out = appendUnder(out, name, paths(n.fields[name], intoRepeats))
}
return out
case *nullItem:
return []string{""}
case *table:
return tablePaths(n)
case *tableColumn, *tableRow:
return tablePaths(n, intoRepeats)
case *tableColumn, *tableRow, *nullItem:
return []string{""}
case *choice:
out := []string{""}
for p := range n.shared {
out = append(out, p)
shared := n.shared
if !intoRepeats {
shared = sharedPaths(n.items, false)
}
return out
return append([]string{""}, sortedNames(shared)...)
default:
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
// 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()...)
sort.Strings(out[1:])
for _, c := range t.rows.Children() {
for _, p := range paths(c.Owner()) {
out = append(out, join(c.Segment(), p))
}
out = appendUnder(out, c.Segment(), paths(c.Owner(), intoRepeats))
}
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
// through a choice to reach. It intersects, bailing as soon as the set
// is empty, which is immediate for a choice of plain strings.
func sharedPaths(items []node) map[string]bool {
shared := subPaths(items[0])
func sharedPaths(items []node, intoRepeats bool) map[string]bool {
shared := subPaths(items[0], intoRepeats)
for _, it := range items[1:] {
if len(shared) == 0 {
return nil
}
next := subPaths(it)
next := subPaths(it, intoRepeats)
for p := range shared {
if !next[p] {
delete(shared, p)
@@ -197,10 +201,10 @@ func sharedPaths(items []node) map[string]bool {
return shared
}
// subPaths is paths(n) as a set, without the empty path that means n itself.
func subPaths(n node) map[string]bool {
// subPaths is paths(n, intoRepeats) as a set, without the empty path that means n itself.
func subPaths(n node, intoRepeats bool) map[string]bool {
out := map[string]bool{}
for _, p := range paths(n) {
for _, p := range paths(n, intoRepeats) {
if p != "" {
out[p] = true
}
+1 -1
View File
@@ -266,7 +266,7 @@ func compileChoice(items []any, pos position) (node, error) {
}
c.cum = cum
}
c.shared = sharedPaths(c.items)
c.shared = sharedPaths(c.items, true)
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
}
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
// the step into it came from a row of an ancestor table, so a table there reads a row even
// where the tail is empty.
// it descended there, as route reads it.
type pathPos struct {
n node
tail []string
@@ -154,8 +157,8 @@ type pathPos struct {
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
// appends what it takes to steps: a field, or the route a table passes. whole is the path at's
// tail ends, and level names its head in errors.
// appends what it takes to steps: a field, or the route a table passes. whole is the full path,
// 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) {
i := len(whole) - len(at.tail)
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)
case *template:
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])
+1 -1
View File
@@ -109,7 +109,7 @@ func TestNodeSwitchesHandleEveryKind(t *testing.T) {
for name, call := range map[string]func(node){
"columnItems": func(n node) { columnItems(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) },
"render": func(n node) {
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 != "" {
fmt.Fprintf(&b, "parent: %q, ", t.rows.Options().Parent)
}
ps := paths(s.n)
ps := paths(s.n, false)
sort.Strings(ps)
fmt.Fprintf(&b, "paths: %#v},\n", ps)
}