Skip to content

Match OpenSCAD's recursion-abort diagnostic wording exactly (file/line + TRACE lines) - #103

Open
particlesector wants to merge 4 commits into
mainfrom
claude/issue-list-review-461qfl
Open

Match OpenSCAD's recursion-abort diagnostic wording exactly (file/line + TRACE lines)#103
particlesector wants to merge 4 commits into
mainfrom
claude/issue-list-review-461qfl

Conversation

@particlesector

Copy link
Copy Markdown
Owner

Summary

  • checkRecursionAbort()'s diagnostic for detected infinite recursion now appends OpenSCAD's in file X, line Y location suffix (using the SourceLoc the interpreter already tracks via recursionAbortedLoc()).
  • Adds the TRACE: called by 'X' in file F, line L call-stack lines that follow OpenSCAD's ERROR: line, one per unwound call frame, innermost first:
    • Interpreter::m_callStack — pushed/popped around each named function/closure call (evaluate()'s FunctionCall case, callClosure()) — covers the function-call chain.
    • CsgEvaluator::m_ctxStack — pushed/popped around every evalModuleCall() invocation (echo, assert, every other builtin, every user module) — covers the enclosing module/builtin-call chain.
    • checkRecursionAbort() walks both, innermost to outermost, and bolts the resulting lines onto the existing Diagnostic's message (there's no separate DiagLevel for "trace"; any consumer that prints d.message verbatim — e.g. tests/tools/scad_dump.cpp — reproduces OpenSCAD's multi-line block as-is this way).

Closes #101.

Verification

Verified byte-for-byte against real OpenSCAD 2021.01's actual output for its own upstream test files (recursion-test-function.scad, issue3118-recur-limit.scad — fetched from openscad/openscad, now copied verbatim into tests/fixtures/eval_diag/), plus new tests covering nested module calls and ordinary (non-tail) recursion, which have no upstream oracle but exercise the same mechanism.

This sandbox had no vcpkg/binary-cache access, so I built the pinned Manifold 3.4.1 + Clipper2 2.0.1 versions from source (matching this repo's vcpkg manifest) plus apt packages for glm/spdlog/nlohmann-json/Catch2, and ran the real chiselcad_tests binary rather than relying on static review.

  • Full test suite passes: 646 test cases / 3604 assertions
  • New TRACE tests pass, including two byte-exact matches against real OpenSCAD output
  • No regressions in the rest of the suite

Test plan

  • ctest --test-dir build --output-on-failure (or ./build/chiselcad_tests) — all 646 cases pass.

Generated by Claude Code

claude added 2 commits August 5, 2026 00:16
The hard-abort diagnostic for detected infinite recursion only named
the function, e.g. "Recursion detected calling function 'crash'".
Real OpenSCAD appends the call site's file and line
("... in file recursion-test-function.scad, line 1"), which
checkRecursionAbort() can now append too since Interpreter already
tracks the abort's SourceLoc via recursionAbortedLoc() — it just
wasn't being surfaced in the message text yet.

The TRACE: call-stack lines from OpenSCAD's full diagnostic (a
separate call-stack-with-locations feature the interpreter doesn't
track today) are still not implemented; that's the larger remaining
half of #101.
)

Completes the diagnostic-wording parity started in the previous commit:
real OpenSCAD's recursion-detected error is followed by one
"TRACE: called by 'X' in file F, line L" line per unwound call frame,
innermost first. ChiselCAD's abort mechanism short-circuits instead of
unwinding via exceptions, so there was no call-stack to walk for this.

Adds that call stack in two halves that mirror where each frame is
actually entered:
- Interpreter::m_callStack, pushed/popped around each named function/
  closure call (evaluate()'s FunctionCall case, callClosure()) —
  covers the function-call chain.
- CsgEvaluator::m_ctxStack, pushed/popped around every evalModuleCall()
  invocation (echo, assert, every other builtin, every user module) —
  covers the enclosing module/builtin-call chain.

checkRecursionAbort() walks both, innermost to outermost, bolting the
resulting TRACE lines onto the existing Diagnostic's message (there's
no separate DiagLevel for "trace"; every consumer that prints
d.message verbatim reproduces OpenSCAD's multi-line block this way).

Verified byte-for-byte against real OpenSCAD 2021.01's actual output
for its own upstream test files (recursion-test-function.scad,
issue3118-recur-limit.scad — fetched from openscad/openscad, now
copied into tests/fixtures/eval_diag/ verbatim), plus new tests for
nested module calls and ordinary (non-tail) recursion, which have no
upstream oracle but follow the same mechanism. Full suite (646 cases)
still passes.

@particlesector particlesector left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the diagnostic-wording change. The mechanism is sound: m_callStack (function/closure calls) and m_ctxStack (module/builtin calls) are each maintained correctly — pushed/popped in matching pairs, with evalModuleCall()'s use of an RAII popper protecting against early returns — and checkRecursionAbort() walks both innermost-to-outermost to build the TRACE lines, matching OpenSCAD's format. The three m_recursionAbortedStack capture sites are consistent with whether a frame has already been pushed at that point (verified against the actual push/pop code), avoiding both duplication and off-by-one drops. Byte-exact verification against real OpenSCAD output on two upstream fixtures gives good confidence. Left one minor test-coverage nit inline; nothing blocking.


Generated by Claude Code

Comment thread src/lang/Interpreter.cpp
…site

Review feedback on PR #103: the existing TRACE tests all exercise
evaluate()'s FunctionCall path, never callClosure() — a separate
kMaxCallDepth check and m_callStack push site for recursive function
literals (as opposed to `function` defs). Adds a test that recurses
via a self-referencing closure to cover it too.

@particlesector particlesector left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up on the closure test added for the earlier nit.


Generated by Claude Code

Comment thread src/lang/Interpreter.cpp Outdated
…ll site

Review feedback on PR #103: callClosure() pushed
fnVal.closure->def->loc (the FunctionLit's own location, i.e. where
`function(...) ...` was written) for both m_recursionAbortedLoc and its
m_callStack frame, instead of the call site — unlike the sibling
FunctionDef path in evaluate(), which correctly uses node.loc (the
FunctionCall's own location). Every recursive call through a given
closure would report the same fixed line regardless of where each
call was actually made.

Threads a callLoc parameter through callClosure() from both of its
call sites (FunctionCall and CallExpr in evaluate(), which already had
the right node.loc available but weren't passing it), and uses that
for both fields instead.

The previous closure test's script had the recursive call and the
closure literal on the same line, so it couldn't tell def-site from
call-site apart — passed either way. Rewrote it against a fixture
where they're on different lines, asserting the TRACE lines land on
the call site and never on the definition line.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Low] Recursion-detected hard abort doesn't match OpenSCAD's exact diagnostic wording (ERROR/TRACE lines)

2 participants