Fix dependent for-loop/list-comp ranges (later dim sees earlier binding) - #70
Conversation
A later for-clause/list-comprehension-for dimension's own range/RHS expression could never see an earlier dimension's current binding -- `for (i=[0:2], j=[0:i])` (a standard triangular-loop idiom) or `[for (p=[1:N], pt=f(p)) pt]` (a common BOSL/BOSL2 pattern, e.g. snappy-reprap's wiring.scad fillet_path) both warned "Ignoring unknown variable" and silently produced undef/wrong results, cascading into large amounts of spurious downstream "undefined operation" warnings and partial/wrong geometry in real scripts. Root cause: every dimension's own RHS expression was evaluated in one flat pass, upfront, against the ORIGINAL (pre-loop) ctx/scope, before ANY variable was bound -- present in six independent copies of the same shape: evalFor/evalListElement's ListCompFor case/resolveIntersectionFor (interpreter) and compileForLoop/compileIntersectionForLoop/ compileListElement's ListCompFor case (VM compiler, including the intersection_for compiler added earlier this week, which inherited the bug from compileForLoop's own scaffold it reused). Verified wrong against real OpenSCAD.app directly for all three constructs, not just an internal inconsistency -- confirmed by a real-world repro (snappy-reprap's full_assembly.scad): 6024 spurious warnings and a visibly incomplete model before this fix, 2 (a real, unrelated, pre-existing warning) after. Fix: each dimension's own RHS now evaluates INSIDE the recursion/nested- loop-emission (interpreter) or is emitted structurally INSIDE the enclosing dimension's own loop body (compiler), so it re-evaluates once per outer-dimension binding with every earlier dimension already bound -- exactly mirroring ordinary nested-loop semantics, matching real OpenSCAD. Op::IterReset becomes unreachable as a result (materializing now naturally resets the iterator's own index as a side effect on every re-entry) -- left defined rather than removed in this same change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
prepareChildrenForward's dyn-variable copy loop, meant only to forward
$fn/$fa/$fs/$t-style overrides from the intermediate wrapper's scope,
also swept up $children/$parent_modules and overwrote the correctly
inherited target values with the wrapper module's own bookkeeping.
This silently broke the common BOSL/BOSL2 guarded-forwarding idiom
`module left(x) { if ($children > N) children(N); }`: the guard read
the wrapper's own 1-child count instead of the real caller's, so it
evaluated false and dropped geometry with no warning. Found via a
real user project (snappy-reprap) where this pattern accounts for a
large fraction of the rendered model.
Bump to 0.13.9.
|
Pushed a second, more severe fix onto this branch after further real-world testing against the same snappy-reprap project surfaced a deeper bug. Root cause: Combined with the dependent-for-loop-range fix already on this branch, snappy-reprap's Added a regression test ( |
Summary
A real, pre-existing correctness bug (predates all my work this week — reproduces on the already-published v0.13.4) found while investigating a user-reported "partial model + spurious warnings" report on a real-world script.
for-clause/list-comprehension-fordimension's own range/RHS expression could never see an earlier dimension's current binding —for (i=[0:2], j=[0:i])(a standard triangular-loop idiom) or[for (p=[1:N], pt=f(p)) pt](a common BOSL/BOSL2 pattern) both warnedIgnoring unknown variableand silently producedundef/wrong results, cascading into large amounts of spurious downstream warnings and incomplete geometry.evalFor/evalListElement'sListCompForcase/resolveIntersectionFor(interpreter) andcompileForLoop/compileIntersectionForLoop/compileListElement'sListCompForcase (VM compiler) — including theintersection_forcompiler added earlier this week (Extend Op::PushCsgWrap to intersection_for -- the true last native-reentry gap #69), which inherited the bug fromcompileForLoop's own scaffold it reused.snappy-reprap/full_assembly.scad): 6024 spurious warnings and a visibly incomplete model before this fix, 2 (a real, unrelated, pre-existing warning already in the source) after. STL output grew from 4.0MB to 5.7MB (the "partial model" the user reported).Op::IterResetbecomes unreachable as a result (left defined, not removed, in this change).Test plan
OSCAD_BYTECODE_VMon/off, up from 739 (6 new tests: dependent-range correctness for all 3 constructs × interpreter/compiled, each pinned against real OpenSCAD.app's own verified output).fillet_path-shaped) now matches real OpenSCAD.app's echo output exactly, both VM on and off.snappy-reprap/full_assembly.scad) warning count: 6024 → 2.🤖 Generated with Claude Code