From 1b5e928d4f17e33c8c923de17756bae652f1cd94 Mon Sep 17 00:00:00 2001 From: M Date: Mon, 31 Aug 2026 13:42:41 +0200 Subject: [PATCH] Fail loudly where calc says it cannot happen calcVar.eval returned NaN on a path indexVars makes unreachable, and a name missing from the index map read operand 0. Both would have printed a wrong number rather than reporting anything. They panic now, like child() does for the same class of slip, so drift between calcVars and indexVars surfaces as a stack trace naming the operand. Co-Authored-By: Claude Opus 5 --- calc.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/calc.go b/calc.go index 578607c..6f63b51 100644 --- a/calc.go +++ b/calc.go @@ -44,8 +44,12 @@ func (n calcIdx) eval(operands []string) float64 { } // eval on an unplaced name cannot happen: calcPrep runs indexVars over every -// expression it compiles, so only a calcIdx reaches a render. -func (n calcVar) eval([]string) float64 { return math.NaN() } +// expression it compiles, so only a calcIdx reaches a render. It panics rather +// than returning NaN, so a node kind indexVars forgets is a stack trace and not a +// silently wrong number. +func (n calcVar) eval([]string) float64 { + panic(fmt.Sprintf("fakes: calc operand %q was never placed", string(n))) +} func (n calcNeg) eval(operands []string) float64 { return -n.x.eval(operands) } @@ -112,7 +116,11 @@ func calcPrep(args []string) callFn { func indexVars(n calcNode, at map[string]int) calcNode { switch n := n.(type) { case calcVar: - return calcIdx(at[string(n)]) + i, placed := at[string(n)] + if !placed { // calcVars named every operand, so a miss means the two disagree + panic(fmt.Sprintf("fakes: calc operand %q is not among the names read for it", string(n))) + } + return calcIdx(i) case calcNeg: return calcNeg{indexVars(n.x, at)} case calcBin: