Skip to content

lookibed leftovers: goto in a block literal, AOT struct order, max_framed_call_depth - #4494

Merged
borisbat merged 4 commits into
masterfrom
bbatkin/lookibed-leftovers
Nov 8, 2026
Merged

borisbat merged 4 commits into
masterfrom
bbatkin/lookibed-leftovers

Conversation

@borisbat

@borisbat borisbat commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

Behavior change: a static goto inside a block literal now reaches the literal's own label on the interpreter; -aot orders 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_VERSION 5: a host built on the old CodeOfPolicies layout 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 return where -jit printed 3 (#4476). The struct sort's dependency walk followed a pointer field into its pointee, so Inner holding owner : Outer? read as depending on Outer, the sort saw a cycle and kept declaration order, and the generated C++ failed on field has incomplete type (#4109). options stack bounds the simulated stack only, so a recursion under a large stack died of SIGSEGV on the native stack with no diagnostic (#4110).

What changes.

  • A block literal holding labels simulates as SimNode_ClosureBlockWithLabels: the closure epilogue plus the labelled block's jump handling; the plain closure node and the invokeEx code0 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_depth mirror into Context::maxFramedCallDepth; a nonzero cap makes Function::makeSimNode emit SimNode_CallChecked<N> and SimNode_CallAndCopyOrMoveChecked<N> in place of the plain nodes, with a fused CallChecked family for the one- and two-argument shapes; every panic handler restores framedCallDepth beside fastCallDepth; Context::restart zeroes both.
  • The dasLLAMA general follow-up ledger gains row 289: the typed-aliasing opt-in for the CPU kernel generators and the hand-written [hot_path] loops.

Observable behavior.

Where to look. SimNode_ClosureBlockWithLabels::eval in src/simulate/simulate.cpp and its selection in the ExprBlock visit of src/ast/ast_simulate.cpp; collectStructDeps in src/ast/ast_program.cpp; enterCheckedFramedCall and the checked nodes in include/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-pr chain (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_sitemap not 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.
  • Negative controls: the pre-fix binary reproduces all three issues on the same programs (interpreter end of block without return, clang++ -fsyntax-only errors on the generated C++, SIGSEGV at depth 1000000); the fixed binary passes each.
  • The framed-depth test's recursers keep a live var local: the optimizer folds an if-return body into a single return, which IS fastcall, so the first draft of the fixtures was counted by the fastcall guard; options log_stack shows 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.
  • Hot path: the plain SimNode_ClosureBlock, SimNode_Call and SimNode_CallAndCopyOrMove evaluators 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 under ARCHITECTURE.md#sanctioned-hot-path-additions beside the fastcall counter's entry.
  • ABI: Context gains two trailing uint32_t members, CodeOfPolicies one trailing field with DAS_POLICIES_VERSION 4 -> 5 and the field in DAS_MODULE_CACHE_POLICY_FIELDS, so AstSerializer::getVersion() goes 253 -> 254 and a module-cache record an older build wrote is rejected as stale (probed: a 253 header reads deser: FALLBACK, the record rewrites, the tests pass); no symbol is removed, renamed or retyped, so external modules compile unchanged (skills/internal/abi_break_sweep.md sweep: additions only).
  • Codex round one (--base origin/master, six exec steps over the diff): one finding, rejected as unreachable - it read SimNode_ClosureBlockWithLabels as turning an outward jump from a non-void literal into end of block without return, but no jump leaves a literal: a static goto to an enclosing label is error[30819]: can't find label at compile, and a computed one panics invalid label index at 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.md regenerated from the descriptions (test-audit --verify matches).
  • Copilot round one: the stale enterCheckedCall name in the architecture paragraph fixed; the two "list the fixture in AOT_LANGUAGE_MODULE_FILES" remarks rejected - the module is options no_aot, interpreted under the AOT build like _fast_call_depth_recursers.das, and the tests-aot gate built and ran the test on this tip.
  • Copilot round two: the missing getVersion() bump for the new field on DAS_MODULE_CACHE_POLICY_FIELDS accepted (src/builtin/REVIEW.md, the module-cache record rule).

Claims - stated, not tested

  • The SimNodeDebug_ClosureBlockWithLabels and SimNodeDebug_ClosureBlock debugger twins are read, not run; the non-debug twin is the tested path.
  • The fused CallChecked two-argument shapes are exercised by rec_sum2; which fusion permutation a given call takes is not asserted.

Not done

  • Calls through function pointers, lambdas and class methods are not counted by either depth guard (as documented for the fastcall guard); the struct-constructor call built directly as SimNode_CallAndCopyOrMove<0> is not counted either.
  • The copy-or-move checked node has no fused twin; it takes the general node when the cap is set.

#nightly - src/ and include/daScript/simulate/ change, and tests/language under the full test_aot.

borisbat and others added 3 commits November 8, 2026 02:18
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>
Copilot AI balanced review requested due to automatic review settings November 8, 2026 11:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 goto within 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_depth plumbing 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.das to AOT_LANGUAGE_MODULE_FILES in tests/aot/CMakeLists.txt (the new framed-depth test depends on it).
  • Nit: In include/daScript/simulate/ARCHITECTURE.md, rename the referenced symbol from enterCheckedCall to the implemented Context::enterCheckedFramedCall for 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

Comment thread tests/language/framed_call_depth.das Resolved
@borisbat
borisbat force-pushed the bbatkin/lookibed-leftovers branch from e0d49d8 to 23d94e3 Compare November 8, 2026 11:32
Copilot AI balanced review requested due to automatic review settings November 8, 2026 11:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
@borisbat
borisbat force-pushed the bbatkin/lookibed-leftovers branch from 23d94e3 to cb69c0e Compare November 8, 2026 13:35
Copilot AI balanced review requested due to automatic review settings November 8, 2026 13:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@borisbat
borisbat merged commit 50ddda4 into master Nov 8, 2026
67 checks passed
@borisbat
borisbat deleted the bbatkin/lookibed-leftovers branch November 8, 2026 15:26
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.

2 participants