Cover a calc operand by what it renders

The fence now asks the right question for each kind of hold. A path head keeps
containment, since a path may read into anything the level carries. An operand
head takes the render closure, because a calc renders its field and that draw
fixes only the value the render produces.

That stops rejecting a reference to a sibling the operand never renders, which
had loaded fine before the fence widened, and starts rejecting a reference
that reaches what the operand renders through. A head whose cover is empty —
a fixed string — skips the walk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
M
2026-08-31 14:02:00 +02:00
committed by lilleman-tw
parent 779b89a804
commit 2e187117f8
+33 -2
View File
@@ -60,8 +60,19 @@ func checkBoundLevelsHeld(root map[string]node) error {
}
sort.Strings(heads) // so which overlap is reported does not vary
for _, head := range heads {
// What one draw answers for depends on how the draw is read. A path may
// read into anything the level contains; a calc renders its operand, so
// that draw fixes exactly the value the render produces.
held := map[node]bool{}
reader, isPath := t.bound[head]
if isPath {
cover(t.fields[head], held)
} else {
coverRendered(t.fields[head], held)
}
if len(held) == 0 {
continue // a fixed string, which cannot disagree with itself
}
// 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.
seen := map[node]bool{}
@@ -70,10 +81,10 @@ func checkBoundLevelsHeld(root map[string]node) error {
continue // a token or operand reading this draw, the routes allowed
}
if renders(e.to, held, seen) {
if reader, isPath := t.bound[head]; isPath {
if isPath {
return fmt.Errorf("%s: {%s} renders %q, which {%s} reads a path into; name the fields you want instead", path, e.label, head, reader)
}
return fmt.Errorf("%s: {%s} renders %q, which a {calc()} also reads; spell it {%s} so both read one draw", path, e.label, head, head)
return fmt.Errorf("%s: {%s} renders %q, which a {calc()} also reads; reach it one way so it is drawn once", path, e.label, head)
}
}
}
@@ -95,6 +106,26 @@ func cover(n node, into map[node]bool) {
}
}
// coverRendered collects what one held draw of a calc operand answers for: what
// rendering it reaches, following the same edges expand does. An operand is only
// ever rendered — checkNoOverlap rejects a path into one — so its draw fixes that
// value and nothing else. That is narrower than containment where a sibling the
// operand's format never renders cannot disagree with it, and wider where the
// operand renders through a reference, which is part of the value it produces.
// Literals are left out for the reason cover leaves them out.
func coverRendered(n node, into map[node]bool) {
if _, fixed := n.(literal); fixed {
return
}
if into[n] {
return
}
into[n] = true
for _, e := range renderEdges(n) {
coverRendered(e.to, 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