Accept one spelling per draw, and reject a path into a repeating level
This commit is contained in:
@@ -347,7 +347,19 @@ renders e.g. `Kungsgatan 35` / `176 99 Stockholm`, never a Stockholm postal code
|
|||||||
beside Tranås. Each row carries its own `weight`, so how often a place appears is data
|
beside Tranås. Each row carries its own `weight`, so how often a place appears is data
|
||||||
too. The rule holds both ways: two tails of one head come from the same row, and
|
too. The rule holds both ways: two tails of one head come from the same row, and
|
||||||
one path read twice reads one value (`{p.first} … {p.first}@…` gives one name).
|
one path read twice reads one value (`{p.first} … {p.first}@…` gives one name).
|
||||||
A bare `{place}` in such a format reads that same draw.
|
|
||||||
|
**One draw, one spelling.** A format may not both *render* a level and *read a
|
||||||
|
path into* it — `{p}` beside `{p.first}`, or `{place}` beside `{place.locality}`.
|
||||||
|
Rendering a level expands it afresh while a path reads the level's held draw, so
|
||||||
|
the two would disagree; naming the fields you want is the one spelling that
|
||||||
|
always agrees, and the other is a load error:
|
||||||
|
|
||||||
|
```
|
||||||
|
token {p} renders a level that {p.first} reads a path into; name the fields you want instead
|
||||||
|
```
|
||||||
|
|
||||||
|
For the same reason a path may not read into a level carrying a `repeat`: it
|
||||||
|
reads one draw, so the repeat could never apply.
|
||||||
|
|
||||||
A path is held at **every level it passes through**, not just the first, so the
|
A path is held at **every level it passes through**, not just the first, so the
|
||||||
facts can nest as deeply as they belong:
|
facts can nest as deeply as they belong:
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ type template struct {
|
|||||||
ops []op // format compiled once (see compileOps); what expand walks
|
ops []op // format compiled once (see compileOps); what expand walks
|
||||||
grow int // minimum output size, to size the render buffer
|
grow int // minimum output size, to size the render buffer
|
||||||
// bound is the fields the format addresses by dotted path, each drawn once per
|
// bound is the fields the format addresses by dotted path, each drawn once per
|
||||||
// expansion so two tokens read one row (see boundHeads and expand). nil when
|
// expansion so two tokens read one row (see compileOps and expand). nil when
|
||||||
// the format takes no path.
|
// the format takes no path.
|
||||||
bound map[string]bool
|
bound map[string]bool
|
||||||
}
|
}
|
||||||
@@ -161,6 +161,9 @@ func compileTemplate(m map[string]any) (node, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
t.ops, t.grow, t.bound = compileOps(format)
|
t.ops, t.grow, t.bound = compileOps(format)
|
||||||
|
if err := checkNoOverlap(t.ops, t.bound); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +177,9 @@ func checkPath(n node, tail []string) error {
|
|||||||
}
|
}
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case *template:
|
case *template:
|
||||||
|
if n.repeat > 1 {
|
||||||
|
return fmt.Errorf("it carries a repeat, which a path reading one draw of it cannot apply")
|
||||||
|
}
|
||||||
child, ok := n.fields[tail[0]]
|
child, ok := n.fields[tail[0]]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("no field %q", tail[0])
|
return fmt.Errorf("no field %q", tail[0])
|
||||||
|
|||||||
@@ -189,31 +189,41 @@ func resolve(s *session, arms []arm, t *template, bound *draws) string {
|
|||||||
n = drawn(s, t.fields[a.key])
|
n = drawn(s, t.fields[a.key])
|
||||||
bound.variant[a.key] = n
|
bound.variant[a.key] = n
|
||||||
}
|
}
|
||||||
// Walk the tail, holding the draw at every level the path passes through, so
|
// Hold the draw at every level passed through, so two paths sharing a prefix
|
||||||
// two paths sharing a prefix share every choice along it, not just the head.
|
// share it; the leaf needs no hold, as checkNoOverlap means no other token
|
||||||
// The last level is held too: another token may name it in full.
|
// can name it.
|
||||||
for i, seg := range a.tail {
|
for i, seg := range a.tail {
|
||||||
|
if i < len(a.steps) {
|
||||||
held, drew := bound.variant[a.steps[i]]
|
held, drew := bound.variant[a.steps[i]]
|
||||||
if !drew {
|
if !drew {
|
||||||
held = drawn(s, child(n, seg))
|
held = drawn(s, child(n, seg))
|
||||||
bound.variant[a.steps[i]] = held
|
bound.variant[a.steps[i]] = held
|
||||||
}
|
}
|
||||||
n = held
|
n = held
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
n = child(n, seg)
|
||||||
}
|
}
|
||||||
v := render(s, n)
|
v := render(s, n)
|
||||||
bound.value[a.name] = v
|
bound.value[a.name] = v
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
// child is the node one path segment names below an already-drawn node. checkPath
|
// child is the node one path segment names below an already-drawn node. It holds
|
||||||
// proved the segment exists and that drawn leaves a template here, so this cannot
|
// while checkPath and the set a choice shares (see sharedPaths) agree with this
|
||||||
// fail.
|
// walk: both prove the segment exists and that drawn leaves a template here. Each
|
||||||
|
// way that can break panics naming the segment, so a slip in that agreement
|
||||||
|
// reports where it happened rather than surfacing a nil node a level later.
|
||||||
func child(n node, seg string) node {
|
func child(n node, seg string) node {
|
||||||
t, ok := n.(*template)
|
t, ok := n.(*template)
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(fmt.Sprintf("fakes: %q under %T, which carries no fields", seg, n))
|
panic(fmt.Sprintf("fakes: %q under %T, which carries no fields", seg, n))
|
||||||
}
|
}
|
||||||
return t.fields[seg]
|
c, ok := t.fields[seg]
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("fakes: no field %q under a drawn level", seg))
|
||||||
|
}
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawn resolves a choice to one variant, so a bound head is a concrete node the
|
// drawn resolves a choice to one variant, so a bound head is a concrete node the
|
||||||
|
|||||||
+35
-8
@@ -2,6 +2,7 @@ package fakes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -178,15 +179,12 @@ func fieldTokens(format string) []string {
|
|||||||
// arm is one alternative of a {a|b} token, split into the key naming the node in
|
// arm is one alternative of a {a|b} token, split into the key naming the node in
|
||||||
// a template's fields (a sibling field, or the whole "..path" string a reference is
|
// a template's fields (a sibling field, or the whole "..path" string a reference is
|
||||||
// bound under) and the tail of a dotted path into it. A non-empty tail is what makes
|
// bound under) and the tail of a dotted path into it. A non-empty tail is what makes
|
||||||
// the arm a bound draw: its head is drawn once per expansion (see boundHeads).
|
// the arm a bound draw: its head is drawn once per expansion (see compileOps).
|
||||||
type arm struct {
|
type arm struct {
|
||||||
name string // as written, and the key a bound draw's value is held under
|
name string // as written, and the key a bound draw's value is held under
|
||||||
key string
|
key string
|
||||||
tail []string
|
tail []string
|
||||||
// steps holds the key for each segment the walk descends to, so every level a
|
steps []string // key per level passed through; the head and leaf hold their own
|
||||||
// path passes through is held — including its last, which another path may
|
|
||||||
// name in full ({p.addr} beside {p.addr.city}).
|
|
||||||
steps []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitArm splits one token alternative into key and tail. A reference keeps its
|
// splitArm splits one token alternative into key and tail. A reference keeps its
|
||||||
@@ -200,13 +198,42 @@ func splitArm(name string) arm {
|
|||||||
return arm{name: name, key: name}
|
return arm{name: name, key: name}
|
||||||
}
|
}
|
||||||
segs := strings.Split(tail, ".")
|
segs := strings.Split(tail, ".")
|
||||||
steps := make([]string, len(segs))
|
var steps []string
|
||||||
for i := range segs {
|
for i := 0; i < len(segs)-1; i++ { // every level except the leaf's own
|
||||||
steps[i] = head + "." + strings.Join(segs[:i+1], ".")
|
steps = append(steps, head+"."+strings.Join(segs[:i+1], "."))
|
||||||
}
|
}
|
||||||
return arm{name: name, key: head, tail: segs, steps: steps}
|
return arm{name: name, key: head, tail: segs, steps: steps}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkNoOverlap rejects a format that both renders a level and reads a path into
|
||||||
|
// it — {p} beside {p.first}, or {p.addr} beside {p.addr.city}. The two spell one
|
||||||
|
// draw two ways: the path reads the level's held draw, while rendering the level
|
||||||
|
// expands it afresh, so their values disagree. One spelling, so there is nothing
|
||||||
|
// to get wrong. Names are compared in sorted order, so which pair is reported
|
||||||
|
// does not depend on where the tokens sit.
|
||||||
|
func checkNoOverlap(ops []op, bound map[string]bool) error {
|
||||||
|
var names []string
|
||||||
|
for i := range ops {
|
||||||
|
if ops[i].kind != 'f' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, a := range ops[i].arms {
|
||||||
|
if bound[a.key] {
|
||||||
|
names = append(names, a.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
for i, level := range names {
|
||||||
|
for _, path := range names[i+1:] {
|
||||||
|
if strings.HasPrefix(path, level+".") {
|
||||||
|
return fmt.Errorf("token {%s} renders a level that {%s} reads a path into; name the fields you want instead", level, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// checkSegments rejects an unfinished path: "{a.}", "{.b}" and "{a..b}" each have
|
// checkSegments rejects an unfinished path: "{a.}", "{.b}" and "{a..b}" each have
|
||||||
// a segment naming nothing. A field really named "" would otherwise make them
|
// a segment naming nothing. A field really named "" would otherwise make them
|
||||||
// resolve, so a typo would read as a path that worked.
|
// resolve, so a typo would read as a path that worked.
|
||||||
|
|||||||
Reference in New Issue
Block a user