Hold a bound level against every route that renders it, and let a matching string be
This commit is contained in:
@@ -352,7 +352,9 @@ one path read twice reads one value (`{p.first} … {p.first}@…` gives one nam
|
|||||||
path into* it — `{p}` beside `{p.first}`, or `{place}` beside `{place.locality}`.
|
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
|
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
|
the two would disagree; naming the fields you want is the one spelling that
|
||||||
always agrees, and the other is a load error:
|
always agrees, and the other is a load error. This covers every way a level can
|
||||||
|
be rendered: a token, a `{calc()}` operand, and a `{..path}` reference — wherever
|
||||||
|
the reference sits, including in a field the format renders.
|
||||||
|
|
||||||
```
|
```
|
||||||
token {p} renders a level that {p.first} reads a path into; name the fields you want instead
|
token {p} renders a level that {p.first} reads a path into; name the fields you want instead
|
||||||
@@ -373,9 +375,8 @@ region it sits in all come from one draw. Two paths part company exactly where
|
|||||||
they diverge: `{p.a.v}` and `{p.b.v}` share the row and nothing below it.
|
they diverge: `{p.a.v}` and `{p.b.v}` share the row and nothing below it.
|
||||||
|
|
||||||
The binding lasts for one expansion, so each `repeat` iteration draws again and a
|
The binding lasts for one expansion, so each `repeat` iteration draws again and a
|
||||||
nested template keeps its own. A field no dotted token addresses is unaffected —
|
nested template keeps its own. A field no dotted token addresses is unaffected:
|
||||||
`{word} {word}` still draws twice — and so are `{calc()}` operands, which read
|
`{word} {word}` still draws twice.
|
||||||
their fields directly.
|
|
||||||
|
|
||||||
`New` checks a path the way `Fake` resolves one: every variant of a multi-variant
|
`New` checks a path the way `Fake` resolves one: every variant of a multi-variant
|
||||||
choice must carry the whole path, so a row missing a field is named at load:
|
choice must carry the whole path, so a row missing a field is named at load:
|
||||||
|
|||||||
@@ -117,6 +117,23 @@ func TestReferenceNamingABoundLevelIsRejected(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestASharedNodeIsWalkedOnce(t *testing.T) {
|
||||||
|
// Two fields reaching one node make the render graph a diamond, not a tree.
|
||||||
|
// The search past a bound level must take that in its stride rather than walk
|
||||||
|
// the shared node once per route.
|
||||||
|
f, err := New([]string{writeData(t, map[string]string{
|
||||||
|
"cat": `{"format":"{p.first}|{q}","p":[{"format":"{first}","first":["Anna","Bo"]}],` +
|
||||||
|
`"q":{"format":"{a}{b}","a":{"format":"{..shared}"},"b":{"format":"{..shared}"}}}`,
|
||||||
|
"shared": `["x"]`,
|
||||||
|
})}, WithSeed(1))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New = %v, want a shared node accepted", err)
|
||||||
|
}
|
||||||
|
if got, err := f.Fake("cat"); err != nil || !strings.HasSuffix(got, "|xx") {
|
||||||
|
t.Fatalf("Fake(cat) = %q, %v, want it to end in xx", got, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestReferenceToAMatchingStringIsAccepted(t *testing.T) {
|
func TestReferenceToAMatchingStringIsAccepted(t *testing.T) {
|
||||||
// A literal renders one fixed string, so no draw of it can disagree with a
|
// A literal renders one fixed string, so no draw of it can disagree with a
|
||||||
// held one. Two unrelated literals that merely spell the same text must not
|
// held one. Two unrelated literals that merely spell the same text must not
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ func loadData(paths []string) (map[string]node, error) {
|
|||||||
if err := checkNoCycles(root); err != nil {
|
if err := checkNoCycles(root); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := checkBoundLevelsHeld(root); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return root, nil
|
return root, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,11 +46,9 @@ type template struct {
|
|||||||
separator string
|
separator string
|
||||||
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 maps each field the format addresses by dotted path to one path token
|
||||||
// expansion so two tokens read one row (see compileOps and expand). nil when
|
// reading it: drawn once per expansion (see compileOps and expand), and the
|
||||||
// the format takes no path.
|
// token names the other half of an overlap. nil when the format takes no path.
|
||||||
// Each maps the field to one path token reading it, for naming the other half
|
|
||||||
// of an overlap.
|
|
||||||
bound map[string]string
|
bound map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,10 +167,12 @@ func compileTemplate(m map[string]any) (node, error) {
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkPath is the static twin of descend: it reports whether a dotted tail can
|
// checkPath reports whether a token's dotted tail can address a node whichever way
|
||||||
// address a node whichever way the draw goes. A multi-variant choice must carry the
|
// the draw goes, by the reachability rule descend applies — a multi-variant choice
|
||||||
// whole remaining path in the set every variant shares, so a path that validates
|
// must carry the whole remaining path in the set every variant shares — plus the
|
||||||
// here resolves on every render, and a typo is a New-time error.
|
// rules a held draw adds, which descend has no need of: a level a path reads may
|
||||||
|
// not carry a repeat, and each variant answers for that itself. So a path that
|
||||||
|
// validates here resolves on every render, and a typo is a New-time error.
|
||||||
func checkPath(n node, tail []string, level string) error {
|
func checkPath(n node, tail []string, level string) error {
|
||||||
if len(tail) == 0 {
|
if len(tail) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+57
-26
@@ -32,43 +32,74 @@ func linkRefs(root map[string]node) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
||||||
}
|
}
|
||||||
if err := checkNotBound(t, name, target); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", path, err)
|
|
||||||
}
|
|
||||||
t.fields[name] = target
|
t.fields[name] = target
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkNotBound rejects a reference that lands on a level this format binds, or
|
// checkBoundLevelsHeld rejects every route to a bound level except the paths that
|
||||||
// anywhere under one. Rendering the target draws it afresh beside the path that
|
// read it. A path holds one draw of the level; anything else that renders it draws
|
||||||
// reads its held draw, which is the overlap checkNoOverlap rejects between tokens
|
// again, and the two disagree. checkNoOverlap settles the spellings within one
|
||||||
// — a reference is one more spelling of it, and the only one that can reach a
|
// format (a token, a calc operand); this settles the rest — a reference, whether it
|
||||||
// level from outside the format. Heads are checked in sorted order so which
|
// sits in that format or in anything the format renders, however deep.
|
||||||
// overlap is reported does not vary.
|
//
|
||||||
func checkNotBound(t *template, ref string, target node) error {
|
// It runs after checkNoCycles, whose guarantee is what lets the walk terminate.
|
||||||
heads := make([]string, 0, len(t.bound))
|
func checkBoundLevelsHeld(root map[string]node) error {
|
||||||
for head := range t.bound {
|
return walkNodes(root, func(path string, n node) error {
|
||||||
heads = append(heads, head)
|
t, ok := n.(*template)
|
||||||
}
|
if !ok || len(t.bound) == 0 {
|
||||||
sort.Strings(heads)
|
return nil
|
||||||
for _, head := range heads {
|
|
||||||
if reaches(t.fields[head], target) {
|
|
||||||
return fmt.Errorf("reference {%s} renders a level that {%s} reads a path into; name the fields you want instead", ref, t.bound[head])
|
|
||||||
}
|
}
|
||||||
}
|
heads := make([]string, 0, len(t.bound))
|
||||||
return nil
|
for head := range t.bound {
|
||||||
|
heads = append(heads, head)
|
||||||
|
}
|
||||||
|
sort.Strings(heads) // so which overlap is reported does not vary
|
||||||
|
for _, head := range heads {
|
||||||
|
held := map[node]bool{}
|
||||||
|
cover(t.fields[head], held)
|
||||||
|
for _, e := range renderEdges(t) {
|
||||||
|
if splitArm(e.label).key == head {
|
||||||
|
continue // a path token reading this level, which is the one route allowed
|
||||||
|
}
|
||||||
|
if renders(e.to, held, map[node]bool{}) {
|
||||||
|
return fmt.Errorf("%s: {%s} renders a level that {%s} reads a path into; name the fields you want instead", path, e.label, t.bound[head])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// reaches reports whether want is n or a node contained in it. Containment is the
|
// cover collects what one held draw of a level answers for: the level and
|
||||||
// JSON structure, so the walk is over a tree and always terminates.
|
// everything contained in it, since a path may read any of it. Literals are left
|
||||||
func reaches(n, want node) bool {
|
// out — one fixed string cannot disagree with itself, and a literal is a value, so
|
||||||
if n == want {
|
// two that spell the same text are indistinguishable.
|
||||||
|
func cover(n node, into map[node]bool) {
|
||||||
|
if _, fixed := n.(literal); fixed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
into[n] = true
|
||||||
|
for _, c := range contained(n) {
|
||||||
|
cover(c.node, into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// renders reports whether rendering n can reach anything in want, following the
|
||||||
|
// same edges expand does. seen keeps a node shared by several routes from being
|
||||||
|
// walked twice; checkNoCycles has already proved the graph is a DAG, so the walk
|
||||||
|
// ends.
|
||||||
|
func renders(n node, want, seen map[node]bool) bool {
|
||||||
|
if want[n] {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
for _, c := range contained(n) {
|
if seen[n] {
|
||||||
if reaches(c.node, want) {
|
return false
|
||||||
|
}
|
||||||
|
seen[n] = true
|
||||||
|
for _, e := range renderEdges(n) {
|
||||||
|
if renders(e.to, want, seen) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -240,8 +240,7 @@ func checkNoOverlap(ops []op, bound map[string]string, format string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// reader is one way a format reaches a bound field: the name it reads, and how to
|
// reader is one way a format reaches a bound field, and how to name that spelling.
|
||||||
// name that spelling in an error.
|
|
||||||
type reader struct{ name, label string }
|
type reader struct{ name, label string }
|
||||||
|
|
||||||
// checkSegments rejects an unfinished path: "{a.}", "{.b}" and "{a..b}" each have
|
// checkSegments rejects an unfinished path: "{a.}", "{.b}" and "{a..b}" each have
|
||||||
|
|||||||
Reference in New Issue
Block a user