Pin the per-head walk, and say where the hold stops
Sharing renders' seen set across heads left the suite green while silently
under-rejecting: the first head's walk marks the only route to the second
head's draw. This PR widens heads from the bound levels to every held name,
so two heads in one template is now the ordinary case; a rejected case whose
violation sits on the later head pins the scoping.
An operand rendering one field twice covers the revisit guard, taking
coverage to 97.8% against main's 97.4%.
The README said the two spellings are rejected alike without saying what the
hold follows, which is not true where the operand reaches its own field
through a reference. It now names the plain {field} tokens the walk follows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -366,11 +366,11 @@ the reference sits, including in a field the format renders.
|
|||||||
A `{calc()}` operand is held on its own terms too, so the same fence guards it:
|
A `{calc()}` operand is held on its own terms too, so the same fence guards it:
|
||||||
`{..cat.net} x 2 = {calc(net * 2, 2)}` names one field two ways and is a load
|
`{..cat.net} x 2 = {calc(net * 2, 2)}` names one field two ways and is a load
|
||||||
error, with no path token anywhere. A calc renders its operand whole, so the hold
|
error, with no path token anywhere. A calc renders its operand whole, so the hold
|
||||||
pins every value that render settled: naming the operand and reaching into it
|
pins what that render settled, following the operand's plain `{field}` tokens —
|
||||||
both conflict, and `{net.v}` and `{..cat.net.v}` are rejected alike. The hold
|
`{net.v}` and `{..cat.net.v}` are rejected alike. It stops at a `{..path}`, where
|
||||||
ends at a `{..path}`, where the operand's own value ends and a shared source
|
the operand's own value ends and a shared source begins: two names drawing from
|
||||||
begins — two names drawing from one referenced category are two draws, as
|
one referenced category are two draws, as `{word} {word}` is, so two dice over
|
||||||
`{word} {word}` is, so two dice over one `{..die}` are fine.
|
one `{..die}` are fine.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
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
|
||||||
|
|||||||
@@ -200,6 +200,16 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
},
|
},
|
||||||
`{q} renders "net"`,
|
`{q} renders "net"`,
|
||||||
},
|
},
|
||||||
|
// Each head is walked against its own seen set. Sharing one across heads
|
||||||
|
// would let the first head's walk mark the only route to the second's draw,
|
||||||
|
// and this violation sits on the later head in sorted order.
|
||||||
|
"a violation on the second of two held heads": {
|
||||||
|
map[string]string{
|
||||||
|
"cat": `{"format":"{calc(a + b, 0)} {w}","a":{"format":"{x}","x":["1","2"]},` +
|
||||||
|
`"b":{"format":"{y}","y":["3","4"]},"w":{"format":"{..cat.b}"}}`,
|
||||||
|
},
|
||||||
|
`{w} renders "b"`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for name, c := range rejected {
|
for name, c := range rejected {
|
||||||
_, err := New([]string{writeData(t, c.files)})
|
_, err := New([]string{writeData(t, c.files)})
|
||||||
@@ -242,6 +252,11 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
"a literal operand named twice": {
|
"a literal operand named twice": {
|
||||||
"cat": `{"format":"{calc(n * 2, 0)} {..cat.n}","n":"5"}`,
|
"cat": `{"format":"{calc(n * 2, 0)} {..cat.n}","n":"5"}`,
|
||||||
},
|
},
|
||||||
|
// One node reached twice while walking the operand: the walk must not
|
||||||
|
// revisit it, and the repeat is not a second route to anything.
|
||||||
|
"an operand that renders one field twice": {
|
||||||
|
"cat": `{"format":"{calc(v * 2, 0)}","v":{"format":"{a}{a}","a":["1","2"]}}`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for name, files := range accepted {
|
for name, files := range accepted {
|
||||||
if _, err := New([]string{writeData(t, files)}); err != nil {
|
if _, err := New([]string{writeData(t, files)}); err != nil {
|
||||||
|
|||||||
+3
-4
@@ -68,7 +68,7 @@ func checkBoundLevelsHeld(root map[string]node) error {
|
|||||||
operandDraw(t.fields[head], held)
|
operandDraw(t.fields[head], held)
|
||||||
}
|
}
|
||||||
if len(held) == 0 {
|
if len(held) == 0 {
|
||||||
continue // a fixed string, which cannot disagree with itself
|
continue // an early out: a literal head holds nothing to reach
|
||||||
}
|
}
|
||||||
// One seen set across the edges: a node that cannot reach the level
|
// One seen set across the edges: a node that cannot reach the level
|
||||||
// cannot reach it by another route either, so it is walked once here.
|
// cannot reach it by another route either, so it is walked once here.
|
||||||
@@ -111,9 +111,8 @@ func cover(n node, into map[node]bool) {
|
|||||||
// The walk stops at a {..path} edge, which is where the operand's own value ends
|
// The walk stops at a {..path} edge, which is where the operand's own value ends
|
||||||
// and a shared source begins: two names referencing one category are two draws, the
|
// and a shared source begins: two names referencing one category are two draws, the
|
||||||
// same rule {word} {word} follows. cover stops there too, by way of named, so both
|
// same rule {word} {word} follows. cover stops there too, by way of named, so both
|
||||||
// halves of the fence end at the same boundary. Containment would be wrong here —
|
// halves of the fence end at the same boundary. A literal is left out for the
|
||||||
// it reaches a sibling the operand never renders, which is no part of its value.
|
// reason cover leaves one out.
|
||||||
// A literal is left out for the reason cover leaves one out.
|
|
||||||
func operandDraw(n node, into map[node]bool) {
|
func operandDraw(n node, into map[node]bool) {
|
||||||
if _, fixed := n.(literal); fixed {
|
if _, fixed := n.(literal); fixed {
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user