Skip to content

fix: preserve loop lexical TDZ and retained closure cells - #10235

Closed
proggeramlug wants to merge 1 commit into
mainfrom
codex/10051-loop-lexical-tdz
Closed

proggeramlug wants to merge 1 commit into
mainfrom
codex/10051-loop-lexical-tdz

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Forward-captured block let/const bindings now get a fresh TDZ cell on each block entry. The reported loop prints tdz,undefined,0,0,tdz,undefined,1,1, matching Node 26.5.1, and retained callbacks keep their original iteration's values.

HIR previously included nested lexical bindings in function-entry preallocation. Allocate those cells at their own block entry instead, including one shared switch-case environment after discriminant evaluation. Keep function-scoped var cells shared and allocate TDZ cells before hoisted block-function closures. Codegen also emits the allocation in every copy of a TDZ statement while reusing its stack slot; otherwise the normal finally path could miss an allocation emitted only on the exception path.

Fixes #10051.

Validation on macOS arm64:

  • Coherent perry-dev compiler, runtime-static and stdlib-static build, with PERRY_RUNTIME_DIR pinned to its archives. The synchronous fixture links the native runtime without application dependencies.
  • The exact reported failure reproduced before the fix. Both new HIR regressions failed before and passed after. The new LLVM regression caught the missing normal-path finally allocation before the codegen fix; the shared-cell control stayed green.
  • HIR: 411 library tests plus 2 new integration tests passed (1 existing ignored test). Codegen: 1,533 tests passed (1 existing ignored test).
  • Byte-for-byte Node/native comparisons passed all 12 combinations: commonjs/module × O0/Os/Oz × default/compact GC. Compact configuration: PERRY_RS4GC=0 PERRY_SHADOW_STACK=1 PERRY_INLINE_SHADOW_SLOT=0 PERRY_FULL_OUTLINE_IC=1. Each compilation disables caching and auto-optimization; each execution has a 15-second timeout.
  • cargo test --profile perry-dev -p perry --test loop_lexical_tdz passed, exercising the committed integration entry point and all 12 native comparisons.
  • Ten neighboring TDZ, forward-capture, generator, class-capture and finally regressions passed against Node.
  • GC-root audit on traced LLVM after the canonical RS4GC rewrite: 484 statepoints, 352 live bundles, 1,444 relocates and zero unrooted/stale hazards.
  • Additional protected from-space runs with seed 1, rate 1 and allocation threshold 0 passed: default/O0 recorded 19 copying minors and 153 moved objects; compact/Oz recorded 19 copying minors and 155 moved objects. Both recorded 13 loop polls and protected retired from-space.
  • scripts/pre-tag-check.sh --quick: all checks passed except the existing public benchmark evidence-freshness failure. Its artifact, validators and every source/harness fingerprint input are unchanged from base d8bfa28a38. Node pin consistency and git diff --check passed.

No version bump.

Summary by CodeRabbit

  • Bug Fixes

    • Corrected lexical let/const behavior in loops, switches, and finally blocks.
    • Preserved proper Temporal Dead Zone errors and fresh bindings for each block entry.
    • Maintained shared behavior for function-scoped var declarations.
  • Tests

    • Added regression coverage across loop types, switch fallthrough, closures, block functions, and abrupt control-flow paths.
    • Added validation against native Node.js behavior across supported compilation modes.

CI update: the current head is red on the same unchanged areas seen on the earlier independent PRs: the perex_owner.rs raw-TLS policy check, public benchmark freshness, two unused WebAssembly helpers, generated API-doc drift, and Linux native_stack::tests::stack_top_respects_custom_thread_stack_sizes (3,765 runtime tests pass, one fails, four are ignored). These files are outside this diff. This comparison identifies shared failures; it is not an exact-base Linux A/B rerun. The scoped native loop-TDZ regression (133.83 seconds) and both HIR regressions have now passed in Linux CI. The shared failures above still prevent an overall green gate.

@proggeramlug
proggeramlug force-pushed the codex/10051-loop-lexical-tdz branch from cc6f53f to c00ca31 Compare September 13, 2026 23:23
@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: bed15526-5702-420d-a8b4-b845d40b52e7

📥 Commits

Reviewing files that changed from the base of the PR and between d8bfa28 and c00ca31.

📒 Files selected for processing (13)
  • changelog.d/10235-loop-lexical-tdz.md
  • crates/perry-codegen/src/stmt/mod.rs
  • crates/perry-codegen/src/stmt/prealloc_tdz_path_tests.rs
  • crates/perry-hir/src/ir/stmt.rs
  • crates/perry-hir/src/lower/expr_function.rs
  • crates/perry-hir/src/lower/lowering_context.rs
  • crates/perry-hir/src/lower/stmt.rs
  • crates/perry-hir/src/lower_decl/block.rs
  • crates/perry-hir/src/lower_decl/body_stmt.rs
  • crates/perry-hir/tests/loop_lexical_tdz.rs
  • crates/perry/tests/loop_lexical_tdz.rs
  • scripts/test-loop-lexical-tdz.mjs
  • test-files/test_gap_10051_loop_lexical_tdz.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The change allocates fresh TDZ cells for captured lexical bindings at each block entry. It preserves shared var cells, handles switch and finally paths, and adds HIR, codegen, and Node/native regression coverage.

Changes

Captured lexical TDZ handling

Layer / File(s) Summary
Nested scope TDZ lowering
crates/perry-hir/src/lower/..., crates/perry-hir/src/ir/stmt.rs
Nested forward-captured let and const bindings now return TDZ box IDs and emit PreallocateTdzBoxes at block entry. Function-level preallocation excludes these IDs. Strict block-function paths use the same scope-local allocation.
Shared switch environment lowering
crates/perry-hir/src/lower/stmt.rs, crates/perry-hir/src/lower_decl/body_stmt.rs
Switch lowering collects case TDZ boxes, saves the discriminant in __switch_discriminant, preallocates the boxes, and reads the saved discriminant.
TDZ storage allocation
crates/perry-codegen/src/stmt/mod.rs, crates/perry-codegen/src/stmt/prealloc_tdz_path_tests.rs
Code generation re-emits TDZ heap-cell allocation when a stack slot already exists, while ordinary preallocation continues to reuse the existing cell. Tests cover normal and exceptional finally paths.
Regression validation
crates/perry-hir/tests/loop_lexical_tdz.rs, crates/perry/tests/loop_lexical_tdz.rs, scripts/test-loop-lexical-tdz.mjs, test-files/test_gap_10051_loop_lexical_tdz.ts, changelog.d/10235-loop-lexical-tdZ.md
Tests cover loop entries, closures, shared var, switch fallthrough, finally, block functions, function expressions, abrupt exits, GC modes, package types, and optimization levels.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant TypeScriptSource
  participant HIRLowering
  participant Codegen
  participant PerryBinary
  TypeScriptSource->>HIRLowering: lower loop and nested lexical bindings
  HIRLowering->>Codegen: emit PreallocateTdzBoxes
  Codegen->>PerryBinary: allocate fresh TDZ cells
  PerryBinary->>PerryBinary: execute loop entries and closures
Loading

Possibly related PRs

  • PerryTS/perry#6044: Introduced the PreallocateTdzBoxes mechanism and related TDZ HIR and codegen plumbing used by this change.

Merge Risk: ⚪ Minimal · up to c00ca

The lexical-binding change preserves per-entry TDZ cells while retaining shared var behavior, with regression coverage across lowering, code generation, runtime modes, and optimization levels. It is ready to merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.31% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 26 functions across 12 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #10051 requires fresh per-iteration lexical cells, repeated TDZ setup, preserved shared var cells, and undefined after an uninitialized declaration. The HIR changes allocate nested forward-c…
Out of Scope Changes check ✅ Passed The changed source files implement issue #10051 lexical binding and TDZ behavior. The HIR and LLVM tests, standalone fixture, comparison script, integration test, and changelog directly support the im…
Title check ✅ Passed The title clearly summarizes the main change: preserving loop lexical TDZ behavior and retained closure cells.
Description check ✅ Passed The description covers the change, related issue, implementation details, extensive validation results, CI status, and the absence of a version bump. It does not reproduce the template checklist, but …
Full details: Docstring Coverage

Explanation

Docstring coverage is 42.31% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 26 functions across 12 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/10051-loop-lexical-tdz

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Landed via merge train 186r (#10247) at eb13fa1 on main.

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.

codegen: captured let loses its TDZ on the second loop iteration

1 participant