Hold what a calc operand's render settled, up to a reference

The fence pinned only the operand node, so a reference reaching into it drew
that value again: {net.v} was a load error while {..cat.net.v} loaded and
disagreed in 20 of 40 renders. The hold now covers what rendering the operand
settles inside itself, so both spellings of that shape are rejected.

The walk stops at a {..path} edge, where the operand's own value ends and a
shared source begins. cover stops there too, through named, so both halves of
the fence end at the same boundary — and two names drawing from one referenced
category stay two draws.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
M
2026-08-31 21:20:15 +02:00
committed by lilleman-tw
parent 8d03ce34bb
commit 21e7af6c1f
2 changed files with 25 additions and 8 deletions
+6 -4
View File
@@ -365,10 +365,12 @@ 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 what the error, with no path token anywhere. A calc renders its operand whole, so the hold
hold pins is that one field — another route conflicts only by naming it. Two pins every value that render settled: naming the operand and reaching into it
names that merely draw from one source are two draws, as `{word} {word}` is, so both conflict, and `{net.v}` and `{..cat.net.v}` are rejected alike. The hold
two dice over one `{..die}` are fine. ends at a `{..path}`, where the operand's own value ends and a shared source
begins — two names drawing from one referenced category are two draws, as
`{word} {word}` is, so two dice over 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
+19 -4
View File
@@ -103,16 +103,31 @@ func cover(n node, into map[node]bool) {
} }
} }
// operandDraw collects what one held draw of a {calc()} operand answers for: that // operandDraw collects what one held draw of a {calc()} operand answers for: the
// one node, since a calc renders its operand whole. Neither wider set works — // operand and what rendering it settles inside itself. A calc renders its operand
// containment reaches a sibling the operand never renders, and the render closure // whole, so that draw fixes every value the render produced, and a second route to
// reaches a source two operands share, and neither of those can disagree with it. // any of them disagrees with it.
//
// 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
// 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 —
// it reaches a sibling the operand never renders, which is no part of its value.
// A literal is left out for the 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
} }
if into[n] {
return
}
into[n] = true into[n] = true
for _, e := range renderEdges(n) {
if isRef(e.label) {
continue
}
operandDraw(e.to, into)
}
} }
// renders reports whether rendering n can reach anything in want, following the // renders reports whether rendering n can reach anything in want, following the