Repository navigation
lookibed leftovers: goto in a block literal, AOT struct order, max_framed_call_depth - #4494
Conversation
Row 289 of followup_general.md: the kernel generator templates take [hint(strict_aliasing)] beside their noalias hints, the hand-written [hot_path] loops get it one by one, each kept only where the CPU rail measures a gain - expected small or zero. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A block literal's body always became SimNode_ClosureBlock, which never received a label table and read the jump flag as the block ending, so a static goto inside the literal left it with 'end of block without return' while -jit ran it. A literal holding labels now simulates as SimNode_ClosureBlockWithLabels: the closure epilogue plus the labelled block's jump handling, a jump to a label it owns taken in place and any other handed outward. The plain closure node is unchanged, so the invokeEx code0 path and every literal without labels compile as before. Fixes #4476 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The struct sort's dependency walk returned early only when the struct type itself was marked pointer, but a field such as 'owner : Outer?' is a pointer type whose firstType is the struct, and the container recursion walked into it, so a struct held by value that pointed back at its holder read as a cycle and the sort kept declaration order: the holder's C++ definition came first with the member's type incomplete. The walk now stops at any pointer, whose pointee rides the forward declaration. Fixes #4109 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
Note
Copilot was unable to complete this review at the selected tier due to model policy or plan restrictions.
🟡 Changes recommended
Blocking issues remain because the new underscore-prefixed helper module isn’t added to the AOT module list (likely breaking the AOT subset build/CI) and there’s also a documentation symbol mismatch that should be corrected.
3 open issues
What changed in this PR
This pull request addresses three “lookibed leftovers” by fixing interpreter goto behavior inside block literals, correcting -aot struct definition ordering in the presence of back-pointers, and adding an interpreter framed-recursion safety cap via options max_framed_call_depth / CodeOfPolicies (with associated ABI stamp bump, docs, and tests).
Changes:
- Interpreter: introduce/select a label-aware closure block sim node so
gotowithin a block literal targets the literal’s own labels correctly. - AOT: stop struct dependency traversal at pointer types to avoid false cycles and ensure proper definition order for by-value embedded structs.
- Runtime/Policies: add
max_framed_call_depthplumbing and checked call nodes; restore framed-depth counters across panic/recover paths; update docs/tests/changelog.
Required follow-ups from review:
- Critical: Ensure the underscore-prefixed test helper module is included in the AOT subset build by adding
tests/language/_framed_call_depth_recursers.dastoAOT_LANGUAGE_MODULE_FILESintests/aot/CMakeLists.txt(the new framed-depth test depends on it). - Nit: In
include/daScript/simulate/ARCHITECTURE.md, rename the referenced symbol fromenterCheckedCallto the implementedContext::enterCheckedFramedCallfor accurate grepability and doc/code alignment.
| File | Notes |
|---|---|
| tests/README.md | Test index regenerated to reflect new/updated language tests. |
| tests/language/structs_and_classes.das | Adds coverage for AOT struct ordering with back-pointers. |
| tests/language/framed_call_depth.das | Adds tests for max_framed_call_depth behavior and diagnostics. |
| tests/language/control_flow.das | Adds regression coverage for goto inside block literals. |
| tests/language/_framed_call_depth_recursers.das | New helper module; must be included in AOT subset module list per critical comment. |
| src/simulate/simulate.cpp | Implements label-aware closure-block evaluation node. |
| src/simulate/simulate_visit.cpp | Visitor support updates for new sim node families. |
| src/simulate/simulate_fusion_call2.cpp | Adds/updates fused checked-call support (2-arg shapes). |
| src/simulate/simulate_fusion_call1.cpp | Adds/updates fused checked-call support (1-arg shapes). |
| src/simulate/simulate_exceptions.cpp | Restores framed-depth counter across panic/recover paths. |
| src/runtime/context.cpp | Plumbs option/policy into Context::maxFramedCallDepth. |
| src/builtin/module_builtin_rtti.cpp | Exposes new policy field via RTTI. |
| src/builtin/module_builtin_ast_serialize.cpp | Includes new policy field in serialization/match fields. |
| src/builtin/jit_runtime.cpp | Ensures framed-depth restoration in JIT recover paths. |
| src/ast/ast_simulate.cpp | Selects label-aware closure node and checked-call nodes when enabled. |
| src/ast/ast_program.cpp | Fixes struct dependency collection to stop at pointer types. |
| modules/dasLLAMA/followup_general.md | Adds follow-up ledger entry (row 289). |
| include/daScript/simulate/simulate.h | Adds framed-call depth tracking and entry helper API. |
| include/daScript/simulate/simulate_nodes.h | Introduces checked call-node families. |
| include/daScript/simulate/code_of_policies.h | Adds policy field and bumps DAS_POLICIES_VERSION. |
| include/daScript/simulate/ARCHITECTURE.md | Docs update; nit: fix symbol name reference to Context::enterCheckedFramedCall. |
| doc/source/stdlib/handmade/structure_annotation-rtti-CodeOfPolicies.rst | Documents the new CodeOfPolicies field. |
| doc/source/reference/language/options.rst | Documents options max_framed_call_depth. |
| CHANGELIST.md | Changelog entries for the fixes and new option. |
This pull request requires changes.
Review effort: Balanced
e0d49d8 to
23d94e3
Compare
There was a problem hiding this comment.
Note
Copilot was unable to complete this review at the selected tier due to model policy or plan restrictions.
🟡 Changes recommended
A critical issue remains because the module-cache serialization layout was changed by adding a new field in DAS_MODULE_CACHE_POLICY_FIELDS without the required AstSerializer::getVersion() bump to invalidate older cached records, risking deserialization with an incompatible schema.
1 open issue
3 resolved since last review
Review effort: Balanced
options stack bounds the simulated stack alone, so a recursion of framed calls under a large stack ran into the native stack's end and died with SIGSEGV where no diagnostic could fire. The framed guard is the fastcall guard's twin: CodeOfPolicies::max_framed_call_depth (also the option) mirrors into Context::maxFramedCallDepth, a nonzero cap makes Function::makeSimNode emit SimNode_CallChecked and SimNode_CallAndCopyOrMoveChecked in place of the plain nodes, with a fused CallChecked family for the one- and two-argument shapes, and every panic handler restores framedCallDepth beside fastCallDepth. A program with the cap at zero emits only the unchecked nodes, so the guard costs the unprotected path nothing. The test's recursers keep a live local so the optimizer cannot fold them into fastcalls, and measure the depth the cap leaves under the harness's own frames instead of assuming it. Fixes #4110 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
23d94e3 to
cb69c0e
Compare
There was a problem hiding this comment.
Note
Copilot was unable to complete this review at the selected tier due to model policy or plan restrictions.
🔵 Needs a closer look
The change reaches into core interpreter control-flow and call/exception mechanics (adding a call-depth guard, longjmp recovery, and a new SimNode variant), so a human review is needed to judge cross-platform runtime/performance impacts and ensure no call paths were missed.
No open issues 🎉
1 resolved since last review
Review effort: Balanced


Behavior change: a static
gotoinside a block literal now reaches the literal's own label on the interpreter;-aotorders a by-value member's struct before its holder whatever points back;options max_framed_call_depth(new, default 0) caps framed recursion with a recoverable panic.DAS_POLICIES_VERSION5: a host built on the oldCodeOfPolicieslayout fails the ABI stamp and must rebuild.Why. Three lookibed leftovers. A block literal's body always became
SimNode_ClosureBlock, which has no label table and read the jump flag as the block ending:EXCEPTION: end of block without returnwhere-jitprinted3(#4476). The struct sort's dependency walk followed a pointer field into its pointee, soInnerholdingowner : Outer?read as depending onOuter, the sort saw a cycle and kept declaration order, and the generated C++ failed onfield has incomplete type(#4109).options stackbounds the simulated stack only, so a recursion under a largestackdied of SIGSEGV on the native stack with no diagnostic (#4110).What changes.
SimNode_ClosureBlockWithLabels: the closure epilogue plus the labelled block's jump handling; the plain closure node and theinvokeExcode0 path are unchanged.collectStructDeps(src/ast/ast_program.cpp) stops at any pointer type: the pointee rides the forward declaration.options max_framed_call_depth/CodeOfPolicies::max_framed_call_depthmirror intoContext::maxFramedCallDepth; a nonzero cap makesFunction::makeSimNodeemitSimNode_CallChecked<N>andSimNode_CallAndCopyOrMoveChecked<N>in place of the plain nodes, with a fusedCallCheckedfamily for the one- and two-argument shapes; every panic handler restoresframedCallDepthbesidefastCallDepth;Context::restartzeroes both.[hot_path]loops.Observable behavior.
EXCEPTION: end of block without return->3; forward and backward gotos inside a literal both land.-aot:error: field 'embedded' has incomplete type-> compiles,Innerdefined beforeOuter.options max_framed_call_depth = 50000on the A recursion deeper than the native stack kills the interpreter with SIGSEGV: options stack bounds only the simulated stack and no diagnostic fires #4110 program at depth 1000000: SIGSEGV ->stack overflow, max_framed_call_depth 50000 exceeded while calling ...(itscount_downbody folds to a fastcall, so theremax_fast_call_depthis the cap that fires).Where to look.
SimNode_ClosureBlockWithLabels::evalinsrc/simulate/simulate.cppand its selection in theExprBlockvisit ofsrc/ast/ast_simulate.cpp;collectStructDepsinsrc/ast/ast_program.cpp;enterCheckedFramedCalland the checked nodes ininclude/daScript/simulate/simulate.h/simulate_nodes.h;tests/language/control_flow.das,structs_and_classes.das,framed_call_depth.das.Validation, claims, ledger
Validation
make-prchain (preflight --full) on the tip: every code gate green - tests-cpp, tests-interp, tests-jit, tests-aot (436 s), utils-tests, the full 242-file cpp-syntax sweep the changed headers trigger, compile-sweep, lint, format, review-md; the two docs reds are this box (sphinx_sitemapnot installed, the local digest generator's drift), as on JIT: const container parameters are not readonly, parameter hints are checked, the pointer model is written #4488 and jit: typed aliasing by class, off where memory is reinterpreted #4491.end of block without return,clang++ -fsyntax-onlyerrors on the generated C++, SIGSEGV at depth 1000000); the fixed binary passes each.varlocal: the optimizer folds an if-return body into a singlereturn, which IS fastcall, so the first draft of the fixtures was counted by the fastcall guard;options log_stackshows the final ones framed (48-byte frames). The cells measure the depth the cap leaves under the harness's own interpreted frames with a probe recursion (rec_count) instead of assuming it, since the framed counter counts every interpreted framed call including the harness, which the fastcall counter never did.SimNode_ClosureBlock,SimNode_CallandSimNode_CallAndCopyOrMoveevaluators are byte-identical; the checked variants are new nodes chosen only when the cap is set. The only addition on an always-on path is the second counter's save and restore in the panic handlers (one 32-bit load and one store each), recorded underARCHITECTURE.md#sanctioned-hot-path-additionsbeside the fastcall counter's entry.Contextgains two trailinguint32_tmembers,CodeOfPoliciesone trailing field withDAS_POLICIES_VERSION4 -> 5 and the field inDAS_MODULE_CACHE_POLICY_FIELDS, soAstSerializer::getVersion()goes 253 -> 254 and a module-cache record an older build wrote is rejected as stale (probed: a 253 header readsdeser: FALLBACK, the record rewrites, the tests pass); no symbol is removed, renamed or retyped, so external modules compile unchanged (skills/internal/abi_break_sweep.mdsweep: additions only).--base origin/master, six exec steps over the diff): one finding, rejected as unreachable - it readSimNode_ClosureBlockWithLabelsas turning an outward jump from a non-void literal intoend of block without return, but no jump leaves a literal: a staticgototo an enclosing label iserror[30819]: can't find labelat compile, and a computed one panicsinvalid label indexat the goto node (the block's label scope is its own since goto: a labelled block resolves only its own labels; a computed goto checks its target #4475), both probed on the interpreter and the JIT.tests/README.mdregenerated from the descriptions (test-audit --verifymatches).enterCheckedCallname in the architecture paragraph fixed; the two "list the fixture inAOT_LANGUAGE_MODULE_FILES" remarks rejected - the module isoptions no_aot, interpreted under the AOT build like_fast_call_depth_recursers.das, and thetests-aotgate built and ran the test on this tip.getVersion()bump for the new field onDAS_MODULE_CACHE_POLICY_FIELDSaccepted (src/builtin/REVIEW.md, the module-cache record rule).Claims - stated, not tested
SimNodeDebug_ClosureBlockWithLabelsandSimNodeDebug_ClosureBlockdebugger twins are read, not run; the non-debug twin is the tested path.CallCheckedtwo-argument shapes are exercised byrec_sum2; which fusion permutation a given call takes is not asserted.Not done
SimNode_CallAndCopyOrMove<0>is not counted either.#nightly -
src/andinclude/daScript/simulate/change, andtests/languageunder the fulltest_aot.