Make type check expr loop not recursive#9872
Draft
jaredramirez wants to merge 41 commits into
Draft
Conversation
1eca417 to
9cc8cc5
Compare
9cc8cc5 to
c266da9
Compare
cadfaa7 to
2862473
Compare
Reify the checkExpr stack frame into an explicit Self-owned work stack (allocated at Check init), splitting each node into enter/resume/exit around the shared prologue/epilogue. Incremental escape-hatch migration (Approach B), e_block first with a manual review gate before scaling out. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Foundation (frame stack + passthrough driver + depth guard), required two-pass differential harness, deep-nesting stress test, e_tuple_access warm-up migration, then full e_block + checkBlockStatements flattening. Stops at the manual review gate before scaling to other expr kinds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate all leaf expression kinds (literals, lookups, crash, ellipsis, break, anno_only, hosted_lambda, runtime_error, zero_argument_tag) to the iterative checkExprIter work-stack driver. Each leaf runs its recursive arm body verbatim in the .exit step (prologue in .enter sets step=.exit with no children scheduled), reusing the shared epilogue+finish tail. Recursive arms kept intact as source of truth. Adds focused differential tests covering each kind. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrates e_binop, e_closure, e_dbg, e_expect, e_expect_err, e_field_access, e_method_eq, e_nominal, e_nominal_external, e_return, e_structural_eq, e_structural_hash, e_unary_minus, e_unary_not to checkExprIter. Direct-child kinds schedule their children as work-stack frames; helper-delegating and per-child-state kinds run their recursive body verbatim under the driver. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… iterative driver Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cumulators) Convert the e_call apply/record_builder/range path from the escape-hatch recursive body to a true interleaving migration on the work stack: .enter schedules the func child, a call_after_func resume step instantiates a generalized func var and schedules all arg children, and .exit runs the post-child body verbatim. Adds a per-frame call_arg flag so each func/arg child re-asserts checking_call_arg at its own prologue. did_err and the effectful-func does_fx accumulators are reconstructed in the resume/exit steps. Adds a focused differential test exercising e_call. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert the recursive e_interpolation arm to the iterative work-stack driver using the e_block interleave template: add interp_after_first, interp_after_part_value, and interp_after_part_segment resume steps plus an interpolation CheckKindState carrying str_var/item_var/first_var/ did_err/part_cursor. Preserves the exact unify order and the per-child checking_call_arg=true set before each scheduled child. Adds a focused differential test exercising string interpolation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inline checkIteratorForLoop into the iterative work-stack driver as an interleaving kind: .enter marks the hoist observable effect, checks the pattern inline, and schedules the iterable child; the for_after_iterable resume step builds the iterator/next dispatch constraints and records the for-loop dispatch plan, then schedules the body child; .exit unifies the loop expr with empty_record. The recursive arm is kept as the source of truth. Adds a focused differential test for a for-loop over a list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Flatten checkIfElseExpr's recursion spine: schedule each branch cond and body (and the final else) as work-stack frames with resume steps (if_after_cond / if_after_body), preserving per-branch hoist-selection suppression, the branch_acc accumulator, exact unifyInContext contexts and branch indices, warn_unused_branches warnings, and the pairwise error-recovery break semantics. The recursive checkIfElseExpr arm stays intact as the escape-hatch source of truth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Treat checkMatchExpr as a single unit (like checkBlockStatements/ checkMethodCallExpr): add e_match to isMigratedKind, advance to .exit in the driver's .enter switch, and run the recursive arm's body verbatim in .exit. checkMatchExpr's child checkExpr calls re-enter the iterative driver (each its own frame_base), so no native recursion through the statement-nesting spine is added. Inference results unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert the recursive .e_lambda arm to the iterative work-stack driver via enterLambdaExpr/.exit helpers: arg-pattern checks + annotation rigid/per-arg unifies run in .enter before scheduling the single body child; the post-body closeAbsentConstructedPayloadVars + annotation-return unify + return-constraint processing + function-type construction run in exitLambdaExpr. The empirical_exhaustiveness_depth guard is saved/zeroed in .enter and restored in .exit (the recursive defer). The lambda's own does_fx is reset to false (a lambda value performs no effects; the body's effectfulness is captured in the function type via mkFuncEffectful). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Also carries post-main-merge housekeeping that couldn't be cleanly split without interactive hunk tooling: dev-history comment cleanup across the check module, and two differential guards for the is_immediate_callee feature (immediately-invoked vs argument lambda). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the e_str migration, applying fixes from a code review of the non-recursive checker branch: - e_list: interleave each element's elem_var unify (new list_after_elem resume step) instead of deferring all unifies to .exit, so element-mismatch diagnostics are emitted in element order, matching the recursive arm (mirrors the existing e_str/e_record interleaving). - Restore the per-expression tracy span (one per checkExprIter call). - Fetch each node's CIR.Expr once per visit (thread it into checkEnterPrologue; reuse prologue.expr in .exit) instead of three times. - Extract checkLeafExpr: the literal/leaf bodies now live once and both drivers delegate to it, removing ~430 lines of verbatim duplication and the cross-driver drift risk. - Hoist the call-arg/immediate-callee flag re-assertion to one shared site. - Remove the dead MAX_CHECK_RECURSION_DEPTH guard: it never ran in production (every kind is migrated), could not return cleanly mid-iteration (the driver's .exit state restores are skipped on an error unwind), and only covered the checker while parse/canon recurse unguarded. Replaced with a note documenting the deep-nesting limit. Tests: differential coverage for list mismatch + nested error; a bounded-depth binop stress test.
2862473 to
95e26ac
Compare
…logue-epilogue # Conflicts: # src/check/Check.zig
Scope return constraints to owning lambda
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Converts
checkExprfrom native recursion to an explicit iterative work-stack driver (checkExprIter), so the common deeply-nested spine can no longer overflow the native stack. EveryCIR.Exprkind is now migrated —checkExprRecursiveis retained only as the differential-harness baseline (selected byforce_recursive).How it works
checkEnterPrologue/checkExitEpilogueextract the per-expression prologue/epilogue into one shared seam (ExprPrologue), used by both drivers.checkExprframe onto acheck_frame_stackwith.enter→ resume-step →.exitphases. The blockfinal_exprspine — the common deep case — is walked on this stack with O(1) native depth.e_binop, the call family,e_match, …) and interleaving kinds (e_call,e_if,e_for,e_lambda,e_record,e_str,e_interpolation,e_list) carry their cross-child state in per-kind resume steps /CheckKindState.Migration (bottom → top)
Foundation (driver skeleton, differential harness, deep-nesting stress test, prologue/epilogue extraction) →
e_tuple_access→e_block(flatten the block spine) → 22 leaf kinds → single-child kinds → multi-child kinds (list/tuple/tag/call-family) →e_call→e_interpolation→e_for→e_if→e_match→e_lambda→e_record→e_str, then the code-review fixes below.Latest commit — code-review fixes
e_listnow interleaves its per-elementelem_varunifies (list_after_elemresume step) so element-mismatch diagnostics are emitted in element order, matching the recursive arm.checkExprItercall).CIR.Expronce per visit (threaded intocheckEnterPrologue, reused in.exit) instead of three times.checkLeafExpr: the literal/leaf bodies live once and both drivers delegate to it (~430 lines of verbatim duplication removed).MAX_CHECK_RECURSION_DEPTHguard (see Known limitation).Known limitation — deep O(depth) spines
The block
final_exprspine is O(1), but the remaining O(depth) spines — helper-delegating kinds and statement-nested blocks — re-entercheckExpr → checkExprIterper level and overflow the native stack at pathological depth (empirically low hundreds of levels). There is intentionally no checker depth guard: a guard can't return cleanly mid-iteration (the driver's.exitstate restores are skipped on an error unwind), and parse + canonicalization recurse and SIGSEGV on the same shapes unguarded anyway. Real crash-safety on deep input needs an iterative front-end too; until then this is a documented limit, consistent across the front-end. Seedeep_nesting_test.zigand the note at the top ofCheck.zig.Verification
zig build run-test-zig-module-check(incl. the differential harness) → green.run-snapshot-tool) → zero diffs.run-check-zig-format→ clean.🤖 Generated with Claude Code