Hold a calc operand's draw for the expansion

A field the format rendered and a calc read was drawn twice, so the operand
shown could differ from the operand computed — the same disagreement the
dotted-path rule already fences, in its plainest spelling. The README
carried it as a caveat, which is what a rule like this exists to remove.

A calc operand now joins the names an expansion holds, so it is drawn once
and every later read of it — the calc, and any bare token spelling it —
sees that draw. The hold stays per expansion: each repeat iteration and each
nested template draws its own.

expand reads a calc's operands before the call and hands over their values,
so a builtin takes (emitted, operands) rather than the sibling fields, and
the evaluator indexes that slice instead of walking the node tree. That is
what keeps the draws a local. Handing them to a builtin instead lets them
escape through an indirect call, which put two maps on the heap for every
held format, calc or not: BenchmarkBound went 56 B/5 allocs -> 744 B/10.

Measured against main: Bound 359 -> 349 ns at 56 B/5 allocs, unchanged;
Calc 416 -> 577 ns and one more alloc, which is what the correlation costs.

Validation is unchanged: t.bound still carries only the levels a dotted
token reads, so the overlap fences fence exactly what they did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
M
2026-08-30 23:09:58 +02:00
committed by lilleman-tw
parent 4f87a4dd9e
commit 9ed0d3bf55
5 changed files with 155 additions and 86 deletions
+6 -3
View File
@@ -47,9 +47,12 @@ type template struct {
ops []op // format compiled once (see compileOps); what expand walks
grow int // minimum output size, to size the render buffer
// bound maps each field the format addresses by dotted path to one path token
// reading it: drawn once per expansion (see compileOps and expand), and the
// token names the other half of an overlap. nil when the format takes no path.
// reading it, which is the half of an overlap the fences name. nil when the
// format takes no path.
bound map[string]string
// held is every name drawn once per expansion: the bound levels above, plus the
// siblings a {calc()} reads. nil when the format holds nothing (see expand).
held map[string]bool
}
func (*template) isNode() {}
@@ -160,7 +163,7 @@ func compileTemplate(m map[string]any) (node, error) {
if err := checkTokens(format, t.fields); err != nil {
return nil, err
}
t.ops, t.grow, t.bound = compileOps(format)
t.ops, t.grow, t.bound, t.held = compileOps(format)
if err := checkNoOverlap(format, t.bound); err != nil {
return nil, err
}