From 5a398c4908658761e779b22774716cf3955c5823 Mon Sep 17 00:00:00 2001 From: Perry Dev Date: Sun, 5 Jul 2026 20:26:53 +0000 Subject: [PATCH] feat(runtime): Temporal Dead Zone for forward-referenced lexical let/const MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a real TDZ mechanism. A lexical `let`/`const` binding that is forward-referenced by an earlier closure or hoisted function (before its declaration, at function or block scope) is now seeded with a distinct uninitialized sentinel `TAG_TDZ` at scope entry. Reading, `typeof`-ing, or assigning it while still in the dead zone throws a spec `ReferenceError` ("Cannot access 'x' before initialization"); the declaration's own initializer (or `let x;` -> undefined) ends the dead zone. Mechanism: - `TAG_TDZ` sentinel (value/tags.rs), one past the array-hole sentinel in the reserved 0x7FFC NaN-box singleton band, a bit pattern no legitimate value ever holds. - `js_box_get_bits` throws on `TAG_TDZ` — the single choke point every boxed read routes through (direct, closure-captured, compound). Zero-cost for any already-initialized box; GC tracing reads the raw cell so marking an uninitialized box is unaffected. - `js_box_set_bits_tdz_checked` throws on a write to a still-uninitialized binding; user LocalSet writes to TDZ boxes route through it while the declaration's own initializer write stays on plain `js_box_set_bits`. - New HIR `Stmt::PreallocateTdzBoxes` variant seeds boxes with the sentinel; only genuine lexical forward-decls (tracked in `tdz_forward_ids`) are TDZ-seeded, so `var` / hoisted-function / compiler-internal boxes keep the historical undefined-seed and never spuriously throw. Scope: function-local and block-local forward-referenced lexical bindings. Module-top-level TDZ and direct self-reference-in-initializer are deferred (they need name-resolution routing orthogonal to the sentinel). --- crates/perry-codegen-js/src/emit/stmts.rs | 2 +- .../src/emit/js_fallback.rs | 2 +- crates/perry-codegen-wasm/src/emit/stmt.rs | 2 +- .../src/emit/string_collection.rs | 2 +- crates/perry-codegen/src/boxed_vars.rs | 2 +- crates/perry-codegen/src/codegen/closure.rs | 1 + crates/perry-codegen/src/codegen/entry.rs | 2 + crates/perry-codegen/src/codegen/function.rs | 1 + crates/perry-codegen/src/codegen/method.rs | 2 + .../src/collectors/escape_arrays.rs | 3 +- .../src/collectors/escape_news.rs | 2 +- .../src/collectors/escape_objects.rs | 3 +- .../perry-codegen/src/collectors/hir_facts.rs | 6 +- .../src/collectors/pointer_locals.rs | 6 +- .../src/collectors/scalar_methods.rs | 3 +- .../src/collectors/shadow_slots.rs | 3 +- .../src/collectors/this_as_value.rs | 2 +- crates/perry-codegen/src/expr/mod.rs | 7 + .../src/lower_call/closure_analysis.rs | 2 +- .../src/lower_call/new_helpers.rs | 3 +- crates/perry-codegen/src/nanbox.rs | 5 + crates/perry-codegen/src/stmt/let_stmt.rs | 30 +++ crates/perry-codegen/src/stmt/loops.rs | 14 +- crates/perry-codegen/src/stmt/mod.rs | 185 ++++++++++-------- crates/perry-hir/src/analysis.rs | 7 +- crates/perry-hir/src/analysis/value_types.rs | 3 +- crates/perry-hir/src/audit.rs | 2 +- crates/perry-hir/src/capability.rs | 2 +- crates/perry-hir/src/dynamic_import.rs | 15 +- .../perry-hir/src/dynamic_import/visitors.rs | 12 +- crates/perry-hir/src/egress.rs | 2 +- crates/perry-hir/src/ir/stmt.rs | 8 + .../src/js_transform/cross_module_natives.rs | 2 +- crates/perry-hir/src/js_transform/imports.rs | 2 +- .../src/js_transform/local_natives.rs | 4 +- crates/perry-hir/src/lockdown.rs | 2 +- crates/perry-hir/src/lower/context.rs | 1 + crates/perry-hir/src/lower/expr_function.rs | 25 ++- .../perry-hir/src/lower/lowering_context.rs | 8 + crates/perry-hir/src/lower_decl/block.rs | 35 +++- .../src/lower_decl/class_captures.rs | 3 +- crates/perry-hir/src/monomorph/defaults.rs | 2 +- crates/perry-hir/src/monomorph/driver.rs | 2 +- .../src/monomorph/substitute_expr.rs | 1 + .../src/monomorph/update_call_sites.rs | 2 +- crates/perry-hir/src/stable_hash/stmts.rs | 4 + crates/perry-runtime/src/box.rs | 16 +- crates/perry-runtime/src/error.rs | 25 +++ crates/perry-runtime/src/value/mod.rs | 2 +- crates/perry-runtime/src/value/tags.rs | 10 + crates/perry-transform/src/deforest/scan.rs | 6 +- crates/perry-transform/src/deforest/walk.rs | 2 +- .../perry-transform/src/generator/id_scan.rs | 2 +- .../src/generator/iter_result_rewrite.rs | 3 +- .../src/generator/lower/call_this.rs | 3 +- crates/perry-transform/src/inline/analysis.rs | 2 +- .../src/inline/closure_analysis.rs | 4 +- .../src/inline/cross_module.rs | 6 +- .../src/inline/exact_receivers.rs | 5 +- crates/perry-transform/src/inline/mod.rs | 2 +- .../perry-transform/src/inline/substitute.rs | 2 +- .../src/inline/super_detect.rs | 3 +- .../src/unroll/escape_analysis.rs | 3 +- crates/perry-transform/src/unroll/mod.rs | 6 +- .../compile/collect_modules/crypto_ns.rs | 3 +- 65 files changed, 377 insertions(+), 162 deletions(-) diff --git a/crates/perry-codegen-js/src/emit/stmts.rs b/crates/perry-codegen-js/src/emit/stmts.rs index 59205e5c72..dca48c6e4b 100644 --- a/crates/perry-codegen-js/src/emit/stmts.rs +++ b/crates/perry-codegen-js/src/emit/stmts.rs @@ -236,7 +236,7 @@ impl JsEmitter { // (alloca slot+box for hoisted FnDecl ids). The JS backend has no // equivalent — JS hoisting handles this for free in the V8 / JSC // runtime. Emit nothing. - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } } diff --git a/crates/perry-codegen-wasm/src/emit/js_fallback.rs b/crates/perry-codegen-wasm/src/emit/js_fallback.rs index 73131d0f4e..ac79b96867 100644 --- a/crates/perry-codegen-wasm/src/emit/js_fallback.rs +++ b/crates/perry-codegen-wasm/src/emit/js_fallback.rs @@ -206,7 +206,7 @@ impl WasmModuleEmitter { // Issue #569: PreallocateBoxes is a perry-codegen-only directive // — JS hoisting handles forward refs natively, so the wasm/JS // backend has no equivalent to emit. - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-codegen-wasm/src/emit/stmt.rs b/crates/perry-codegen-wasm/src/emit/stmt.rs index 36fcbeda44..22d195c5e6 100644 --- a/crates/perry-codegen-wasm/src/emit/stmt.rs +++ b/crates/perry-codegen-wasm/src/emit/stmt.rs @@ -425,7 +425,7 @@ impl<'a> FuncEmitCtx<'a> { // Issue #569: Wasm backend has no equivalent of LLVM's // alloca'd box slot — JS hoisting in the host runtime handles // forward refs natively. Emit nothing. - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-codegen-wasm/src/emit/string_collection.rs b/crates/perry-codegen-wasm/src/emit/string_collection.rs index f501d80936..68d4bd9fc6 100644 --- a/crates/perry-codegen-wasm/src/emit/string_collection.rs +++ b/crates/perry-codegen-wasm/src/emit/string_collection.rs @@ -418,7 +418,7 @@ impl WasmModuleEmitter { } } Stmt::Break | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => {} - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-codegen/src/boxed_vars.rs b/crates/perry-codegen/src/boxed_vars.rs index 128e707721..30055fecc1 100644 --- a/crates/perry-codegen/src/boxed_vars.rs +++ b/crates/perry-codegen/src/boxed_vars.rs @@ -92,7 +92,7 @@ fn collect_prealloc_box_ids_in_stmts(stmts: &[perry_hir::Stmt], out: &mut HashSe use perry_hir::Stmt; for s in stmts { match s { - Stmt::PreallocateBoxes(ids) => { + Stmt::PreallocateBoxes(ids) | Stmt::PreallocateTdzBoxes(ids) => { for id in ids { out.insert(*id); } diff --git a/crates/perry-codegen/src/codegen/closure.rs b/crates/perry-codegen/src/codegen/closure.rs index 1905d1af05..375c8b3a5c 100644 --- a/crates/perry-codegen/src/codegen/closure.rs +++ b/crates/perry-codegen/src/codegen/closure.rs @@ -768,6 +768,7 @@ pub(super) fn compile_closure( func_returns_class: &cross_module.func_returns_class, boxed_vars: closure_boxed_vars, prealloc_boxes: std::collections::HashSet::new(), + tdz_boxes: std::collections::HashSet::new(), compiler_private_async_i32_control_locals: &cross_module .compiler_private_async_i32_control_locals, compiler_private_async_i1_control_locals: &cross_module diff --git a/crates/perry-codegen/src/codegen/entry.rs b/crates/perry-codegen/src/codegen/entry.rs index e22b2d63f7..224e45b1e5 100644 --- a/crates/perry-codegen/src/codegen/entry.rs +++ b/crates/perry-codegen/src/codegen/entry.rs @@ -601,6 +601,7 @@ pub(super) fn compile_module_entry( func_returns_class: &cross_module.func_returns_class, boxed_vars: main_boxed_vars, prealloc_boxes: std::collections::HashSet::new(), + tdz_boxes: std::collections::HashSet::new(), compiler_private_async_i32_control_locals: &cross_module .compiler_private_async_i32_control_locals, compiler_private_async_i1_control_locals: &cross_module @@ -1134,6 +1135,7 @@ pub(super) fn compile_module_entry( func_returns_class: &cross_module.func_returns_class, boxed_vars: init_boxed_vars, prealloc_boxes: std::collections::HashSet::new(), + tdz_boxes: std::collections::HashSet::new(), compiler_private_async_i32_control_locals: &cross_module .compiler_private_async_i32_control_locals, compiler_private_async_i1_control_locals: &cross_module diff --git a/crates/perry-codegen/src/codegen/function.rs b/crates/perry-codegen/src/codegen/function.rs index 94987492a7..332abb7bfe 100644 --- a/crates/perry-codegen/src/codegen/function.rs +++ b/crates/perry-codegen/src/codegen/function.rs @@ -504,6 +504,7 @@ pub(super) fn compile_function( func_returns_class: &cross_module.func_returns_class, boxed_vars, prealloc_boxes: std::collections::HashSet::new(), + tdz_boxes: std::collections::HashSet::new(), compiler_private_async_i32_control_locals: &cross_module .compiler_private_async_i32_control_locals, compiler_private_async_i1_control_locals: &cross_module diff --git a/crates/perry-codegen/src/codegen/method.rs b/crates/perry-codegen/src/codegen/method.rs index fa2f67b938..72d3fd9d5e 100644 --- a/crates/perry-codegen/src/codegen/method.rs +++ b/crates/perry-codegen/src/codegen/method.rs @@ -388,6 +388,7 @@ pub(super) fn compile_method( func_returns_class: &cross_module.func_returns_class, boxed_vars: method_boxed_vars, prealloc_boxes: std::collections::HashSet::new(), + tdz_boxes: std::collections::HashSet::new(), compiler_private_async_i32_control_locals: &cross_module .compiler_private_async_i32_control_locals, compiler_private_async_i1_control_locals: &cross_module @@ -1262,6 +1263,7 @@ pub(super) fn compile_static_method( func_returns_class: &cross_module.func_returns_class, boxed_vars: static_boxed_vars, prealloc_boxes: std::collections::HashSet::new(), + tdz_boxes: std::collections::HashSet::new(), compiler_private_async_i32_control_locals: &cross_module .compiler_private_async_i32_control_locals, compiler_private_async_i1_control_locals: &cross_module diff --git a/crates/perry-codegen/src/collectors/escape_arrays.rs b/crates/perry-codegen/src/collectors/escape_arrays.rs index dfaa4825bb..cd4ba3af89 100644 --- a/crates/perry-codegen/src/collectors/escape_arrays.rs +++ b/crates/perry-codegen/src/collectors/escape_arrays.rs @@ -130,7 +130,8 @@ fn collect_used_array_indices_in_stmts( | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } } diff --git a/crates/perry-codegen/src/collectors/escape_news.rs b/crates/perry-codegen/src/collectors/escape_news.rs index ceff2f0b9f..bef63bab49 100644 --- a/crates/perry-codegen/src/collectors/escape_news.rs +++ b/crates/perry-codegen/src/collectors/escape_news.rs @@ -166,7 +166,7 @@ fn collect_used_new_fields_in_stmts( ); } Stmt::Break | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => {} - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } } diff --git a/crates/perry-codegen/src/collectors/escape_objects.rs b/crates/perry-codegen/src/collectors/escape_objects.rs index 5d71221926..eda4a83d9a 100644 --- a/crates/perry-codegen/src/collectors/escape_objects.rs +++ b/crates/perry-codegen/src/collectors/escape_objects.rs @@ -163,7 +163,8 @@ fn collect_used_object_fields_in_stmts( | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } } diff --git a/crates/perry-codegen/src/collectors/hir_facts.rs b/crates/perry-codegen/src/collectors/hir_facts.rs index 3c16ecffbb..ed32b05e86 100644 --- a/crates/perry-codegen/src/collectors/hir_facts.rs +++ b/crates/perry-codegen/src/collectors/hir_facts.rs @@ -522,7 +522,8 @@ fn collect_owned_buffer_lets( | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) | Stmt::Throw(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } } @@ -704,7 +705,8 @@ impl ArrayFactCollector { | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-codegen/src/collectors/pointer_locals.rs b/crates/perry-codegen/src/collectors/pointer_locals.rs index 7bddff576f..4c18880ad8 100644 --- a/crates/perry-codegen/src/collectors/pointer_locals.rs +++ b/crates/perry-codegen/src/collectors/pointer_locals.rs @@ -345,7 +345,8 @@ pub fn collect_pointer_typed_locals( | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} Stmt::If { condition, then_branch, @@ -475,7 +476,8 @@ pub fn collect_pointer_typed_locals( | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} Stmt::If { condition, then_branch, diff --git a/crates/perry-codegen/src/collectors/scalar_methods.rs b/crates/perry-codegen/src/collectors/scalar_methods.rs index 2ad6bd943d..78939ad8d5 100644 --- a/crates/perry-codegen/src/collectors/scalar_methods.rs +++ b/crates/perry-codegen/src/collectors/scalar_methods.rs @@ -440,7 +440,8 @@ fn stmt_writes_this_property(stmt: &Stmt, property: &str) -> bool { | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => false, + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => false, } } diff --git a/crates/perry-codegen/src/collectors/shadow_slots.rs b/crates/perry-codegen/src/collectors/shadow_slots.rs index 8344566d9c..a3567dbb68 100644 --- a/crates/perry-codegen/src/collectors/shadow_slots.rs +++ b/crates/perry-codegen/src/collectors/shadow_slots.rs @@ -134,6 +134,7 @@ pub fn collect_declared_shadow_locals_in_stmt( | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) | Stmt::Throw(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-codegen/src/collectors/this_as_value.rs b/crates/perry-codegen/src/collectors/this_as_value.rs index fb3beeae2d..e886ca0bb3 100644 --- a/crates/perry-codegen/src/collectors/this_as_value.rs +++ b/crates/perry-codegen/src/collectors/this_as_value.rs @@ -200,7 +200,7 @@ pub fn stmts_use_this_as_value(stmts: &[perry_hir::Stmt], fields: &HashSet { false } - Stmt::PreallocateBoxes(_) => false, + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => false, }; if bad { return true; diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 937925fdcf..b1c528edaf 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -400,6 +400,13 @@ pub(crate) struct FnCtx<'a> { /// pre-allocated box. The id is added to `boxed_vars` automatically /// so subsequent `LocalGet`/`LocalSet`/`Update` go through the box. pub prealloc_boxes: std::collections::HashSet, + /// LocalIds whose pre-allocated box was seeded with the TAG_TDZ sentinel + /// (Temporal Dead Zone) via `Stmt::PreallocateTdzBoxes` rather than + /// `undefined`. A read of one of these boxes before its `Stmt::Let` runs + /// throws a spec ReferenceError (enforced in the runtime `js_box_get_bits` + /// choke point). The `Stmt::Let` arm consults this set so a no-init + /// declaration (`let x;`) still clears the sentinel to `undefined`. + pub tdz_boxes: std::collections::HashSet, /// Compiler-private async/generator control locals whose closure-shared /// storage is a primitive heap cell instead of a generic JSValue box. /// These ids are emitted by Perry's generator transform, not user source: diff --git a/crates/perry-codegen/src/lower_call/closure_analysis.rs b/crates/perry-codegen/src/lower_call/closure_analysis.rs index 5096a8ccc3..6098da4b43 100644 --- a/crates/perry-codegen/src/lower_call/closure_analysis.rs +++ b/crates/perry-codegen/src/lower_call/closure_analysis.rs @@ -181,7 +181,7 @@ pub fn find_outer_writes_stmt( } } Stmt::Labeled { body, .. } => find_outer_writes_stmt(body, inner_ids, out), - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-codegen/src/lower_call/new_helpers.rs b/crates/perry-codegen/src/lower_call/new_helpers.rs index 514b73db2b..9a536ab609 100644 --- a/crates/perry-codegen/src/lower_call/new_helpers.rs +++ b/crates/perry-codegen/src/lower_call/new_helpers.rs @@ -113,7 +113,8 @@ fn stmt_any( | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => false, + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => false, } } diff --git a/crates/perry-codegen/src/nanbox.rs b/crates/perry-codegen/src/nanbox.rs index e3949987e8..272f8443e5 100644 --- a/crates/perry-codegen/src/nanbox.rs +++ b/crates/perry-codegen/src/nanbox.rs @@ -19,6 +19,10 @@ pub const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; /// invariant. Inline `IndexGet` paths emitted by the codegen must select- /// rewrite this back to `TAG_UNDEFINED` so user code never sees the sentinel. pub const TAG_HOLE: u64 = 0x7FFC_0000_0000_0010; +/// TDZ (Temporal Dead Zone) sentinel — see `perry-runtime::value::TAG_TDZ`. +/// A lexical `let`/`const`/`class` box is seeded with this at scope entry; +/// reading it before the declaration runs throws a spec ReferenceError. +pub const TAG_TDZ: u64 = 0x7FFC_0000_0000_0011; pub const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; pub const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; pub const INT32_TAG: u64 = 0x7FFE_0000_0000_0000; @@ -37,6 +41,7 @@ pub const TAG_NULL_I64: &str = "9222246136947933186"; pub const TAG_FALSE_I64: &str = "9222246136947933187"; pub const TAG_TRUE_I64: &str = "9222246136947933188"; pub const TAG_HOLE_I64: &str = "9222246136947933200"; +pub const TAG_TDZ_I64: &str = "9222246136947933201"; pub const POINTER_TAG_I64: &str = "9222527611924643840"; pub const POINTER_MASK_I64: &str = "281474976710655"; pub const INT32_TAG_I64: &str = "9222809086901354496"; diff --git a/crates/perry-codegen/src/stmt/let_stmt.rs b/crates/perry-codegen/src/stmt/let_stmt.rs index 87a1735111..76caaff59d 100644 --- a/crates/perry-codegen/src/stmt/let_stmt.rs +++ b/crates/perry-codegen/src/stmt/let_stmt.rs @@ -293,10 +293,27 @@ pub(crate) fn lower_let( // no initializer (`var x;`) keeps the prior value, matching JS. if ctx.locals.contains_key(&id) { if let Some(init_expr) = init { + // The binding's OWN declaration ends its Temporal Dead Zone: the + // reused-slot write below (plain, unchecked) overwrites any TAG_TDZ + // sentinel with the real value. + ctx.tdz_boxes.remove(&id); crate::expr::lower_expr( ctx, &perry_hir::Expr::LocalSet(id, Box::new(init_expr.clone())), )?; + } else if ctx.tdz_boxes.remove(&id) { + // No-init reuse (`let x;`) of a TDZ-seeded box must still end the + // dead zone by clearing the sentinel to `undefined`; otherwise a + // later legitimate read of `x` would wrongly throw. + if let Some(slot) = ctx.locals.get(&id).cloned() { + let blk = ctx.block(); + let bptr = blk.load(crate::types::I64, &slot); + let undef_bits = crate::nanbox::TAG_UNDEFINED_I64.to_string(); + blk.call_void( + "js_box_set_bits", + &[(crate::types::I64, &bptr), (crate::types::I64, &undef_bits)], + ); + } } return Ok(()); } @@ -881,6 +898,19 @@ pub(crate) fn lower_let( &[(crate::types::I64, &bptr), (I64, &init_bits)], ); } + } else if ctx.tdz_boxes.contains(&id) { + // TDZ box with a no-init declaration (`let x;`): the box was + // seeded with TAG_TDZ at scope entry; running the declaration + // ends the dead zone by initializing the binding to + // `undefined`. Without this the sentinel would survive and a + // later legitimate read of `x` would wrongly throw. + let slot_clone = ctx.locals[&id].clone(); + let bptr = ctx.block().load(I64, &slot_clone); + let undef_bits = crate::nanbox::TAG_UNDEFINED_I64.to_string(); + ctx.block().call_void( + "js_box_set_bits", + &[(crate::types::I64, &bptr), (I64, &undef_bits)], + ); } return Ok(()); } diff --git a/crates/perry-codegen/src/stmt/loops.rs b/crates/perry-codegen/src/stmt/loops.rs index bdfd0df4db..58e16c5731 100644 --- a/crates/perry-codegen/src/stmt/loops.rs +++ b/crates/perry-codegen/src/stmt/loops.rs @@ -1258,7 +1258,7 @@ fn stmt_is_packed_f64_loop_safe( Stmt::Labeled { body, .. } => { stmt_is_packed_f64_loop_safe(ctx, body.as_ref(), arr_id, counter_id) } - Stmt::PreallocateBoxes(_) => true, + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => true, Stmt::Return(_) | Stmt::Throw(_) | Stmt::Break @@ -2274,7 +2274,8 @@ fn collect_guarded_array_aliases_in_stmt( | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => false, + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => false, Stmt::If { condition, then_branch, @@ -2776,7 +2777,8 @@ fn stmt_mutates_local(stmt: &perry_hir::Stmt, local_id: u32) -> bool { | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => false, + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => false, Stmt::If { condition, then_branch, @@ -3163,7 +3165,9 @@ fn stmt_array_length_effect( Stmt::Break | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => { LoopArrayLengthEffect::Preserves } - Stmt::PreallocateBoxes(_) => LoopArrayLengthEffect::Preserves, + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => { + LoopArrayLengthEffect::Preserves + } } } @@ -3587,7 +3591,7 @@ pub(crate) fn stmt_preserves_array_length( aliases, ), Stmt::Break | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => true, - Stmt::PreallocateBoxes(_) => true, + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => true, } } diff --git a/crates/perry-codegen/src/stmt/mod.rs b/crates/perry-codegen/src/stmt/mod.rs index d9c554790b..f948c7d3a7 100644 --- a/crates/perry-codegen/src/stmt/mod.rs +++ b/crates/perry-codegen/src/stmt/mod.rs @@ -557,89 +557,14 @@ pub(crate) fn lower_stmt(ctx: &mut FnCtx<'_>, stmt: &Stmt) -> Result<()> { // these ids skip the allocation and only `js_box_set` the init // value. `LocalGet` / `LocalSet` / `Update` already route through // the box because the id is in `ctx.boxed_vars`. - Stmt::PreallocateBoxes(ids) => { - for id in ids { - if ctx.locals.contains_key(id) { - // A previous PreallocateBoxes (or an unusual nesting) - // already set this up — skip to keep the existing slot. - ctx.prealloc_boxes.insert(*id); - ctx.boxed_vars.insert(*id); - continue; - } - let is_i32_control = - crate::expr::is_compiler_private_async_i32_control_local(ctx, *id); - let is_i1_control = - crate::expr::is_compiler_private_async_i1_control_local(ctx, *id); - let blk = ctx.block(); - let (box_ptr, cell_note) = if is_i32_control { - ( - blk.call( - crate::types::I64, - "js_i32_box_alloc", - &[(crate::types::I32, "0")], - ), - "primitive_i32_control_cell", - ) - } else if is_i1_control { - ( - blk.call( - crate::types::I64, - "js_bool_box_alloc", - &[(crate::types::I32, "0")], - ), - "primitive_i1_control_cell", - ) - } else { - let undef_bits = crate::nanbox::TAG_UNDEFINED_I64.to_string(); - ( - blk.call( - crate::types::I64, - "js_box_alloc_bits", - &[(crate::types::I64, &undef_bits)], - ), - "jsvalue_box_cell", - ) - }; - let slot = ctx.func.alloca_entry(crate::types::I64); - // perry#4926: PreallocateBoxes can sit nested inside an - // If/Try/Labeled body (e.g. the async state-machine - // wrapper), so this block's box-pointer store doesn't - // necessarily dominate every load of the slot. Entry-init - // the slot to TAG_UNDEFINED so paths that bypass this - // statement read a defined sentinel instead of `undef` - // (see the boxed `Stmt::Let` arm in let_stmt.rs). - let undef_bits = crate::nanbox::TAG_UNDEFINED_I64.to_string(); - ctx.func - .entry_allocas_push_store(crate::types::I64, &undef_bits, &slot); - ctx.block().store(crate::types::I64, &box_ptr, &slot); - record_boxed_slot_js_value_bits( - ctx, - *id, - &box_ptr, - "preallocate_boxes.box_ptr_slot", - ); - if cell_note != "jsvalue_box_cell" { - let lowered = LoweredValue::js_value_bits(&box_ptr); - ctx.record_lowered_value( - "CompilerPrivateAsyncControlCell", - Some(*id), - cell_note, - &lowered, - None, - None, - None, - false, - false, - Vec::new(), - ); - } - ctx.locals.insert(*id, slot); - ctx.prealloc_boxes.insert(*id); - ctx.boxed_vars.insert(*id); - crate::expr::emit_shadow_slot_bind_for_local(ctx, *id); - } - Ok(()) - } + Stmt::PreallocateBoxes(ids) => emit_preallocate_boxes(ctx, ids, false), + + // Temporal Dead Zone variant: identical to `PreallocateBoxes` but + // seeds each JSValue box with the TAG_TDZ sentinel so a + // read-before-declaration throws a spec ReferenceError. See the HIR + // `Stmt::PreallocateTdzBoxes` doc and the runtime `js_box_get_bits` + // choke point. + Stmt::PreallocateTdzBoxes(ids) => emit_preallocate_boxes(ctx, ids, true), // #853: every current `perry_hir::Stmt` variant is matched above. // Keep this catch-all so HIR additions land as a clear compile-time @@ -652,6 +577,99 @@ pub(crate) fn lower_stmt(ctx: &mut FnCtx<'_>, stmt: &Stmt) -> Result<()> { } } +fn emit_preallocate_boxes(ctx: &mut FnCtx<'_>, ids: &[u32], tdz: bool) -> Result<()> { + for id in ids { + if ctx.locals.contains_key(id) { + // A previous PreallocateBoxes (or an unusual nesting) + // already set this up -- skip to keep the existing slot. + ctx.prealloc_boxes.insert(*id); + ctx.boxed_vars.insert(*id); + if tdz { + ctx.tdz_boxes.insert(*id); + } + continue; + } + let is_i32_control = crate::expr::is_compiler_private_async_i32_control_local(ctx, *id); + let is_i1_control = crate::expr::is_compiler_private_async_i1_control_local(ctx, *id); + let blk = ctx.block(); + let (box_ptr, cell_note) = if is_i32_control { + ( + blk.call( + crate::types::I64, + "js_i32_box_alloc", + &[(crate::types::I32, "0")], + ), + "primitive_i32_control_cell", + ) + } else if is_i1_control { + ( + blk.call( + crate::types::I64, + "js_bool_box_alloc", + &[(crate::types::I32, "0")], + ), + "primitive_i1_control_cell", + ) + } else { + // Seed the JSValue box with TAG_TDZ (Temporal Dead Zone) when + // requested -- a read before the declaration runs throws a spec + // ReferenceError via the runtime `js_box_get_bits` choke point. + // Compiler-private i32/i1 control cells are never TDZ. + let seed_bits = if tdz { + crate::nanbox::TAG_TDZ_I64.to_string() + } else { + crate::nanbox::TAG_UNDEFINED_I64.to_string() + }; + ( + blk.call( + crate::types::I64, + "js_box_alloc_bits", + &[(crate::types::I64, &seed_bits)], + ), + "jsvalue_box_cell", + ) + }; + let slot = ctx.func.alloca_entry(crate::types::I64); + // perry#4926: PreallocateBoxes can sit nested inside an If/Try/Labeled + // body (e.g. the async state-machine wrapper), so this block's + // box-pointer store doesn't necessarily dominate every load of the + // slot. Entry-init the slot to TAG_UNDEFINED so paths that bypass this + // statement read a defined sentinel instead of `undef` (see the boxed + // `Stmt::Let` arm in let_stmt.rs). The slot holds a *box pointer*, not + // the value, so it is TAG_UNDEFINED-initialized in both the TDZ and + // non-TDZ cases -- the TAG_TDZ sentinel lives in the box cell, not the + // slot. + let undef_bits = crate::nanbox::TAG_UNDEFINED_I64.to_string(); + ctx.func + .entry_allocas_push_store(crate::types::I64, &undef_bits, &slot); + ctx.block().store(crate::types::I64, &box_ptr, &slot); + record_boxed_slot_js_value_bits(ctx, *id, &box_ptr, "preallocate_boxes.box_ptr_slot"); + if cell_note != "jsvalue_box_cell" { + let lowered = LoweredValue::js_value_bits(&box_ptr); + ctx.record_lowered_value( + "CompilerPrivateAsyncControlCell", + Some(*id), + cell_note, + &lowered, + None, + None, + None, + false, + false, + Vec::new(), + ); + } + ctx.locals.insert(*id, slot); + ctx.prealloc_boxes.insert(*id); + ctx.boxed_vars.insert(*id); + if tdz { + ctx.tdz_boxes.insert(*id); + } + crate::expr::emit_shadow_slot_bind_for_local(ctx, *id); + } + Ok(()) +} + fn stmt_variant_name(s: &Stmt) -> &'static str { match s { Stmt::Expr(_) => "Expr", @@ -670,6 +688,7 @@ fn stmt_variant_name(s: &Stmt) -> &'static str { Stmt::Try { .. } => "Try", Stmt::Switch { .. } => "Switch", Stmt::PreallocateBoxes(_) => "PreallocateBoxes", + Stmt::PreallocateTdzBoxes(_) => "PreallocateTdzBoxes", } } diff --git a/crates/perry-hir/src/analysis.rs b/crates/perry-hir/src/analysis.rs index d94a5adc78..e463aded89 100644 --- a/crates/perry-hir/src/analysis.rs +++ b/crates/perry-hir/src/analysis.rs @@ -214,7 +214,7 @@ pub fn collect_local_refs_stmt( Stmt::Throw(expr) => { collect_local_refs_expr(expr, refs, visited); } - Stmt::PreallocateBoxes(_) => { + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => { // Pre-allocates slot+box; no expression sub-tree to visit. } } @@ -320,7 +320,7 @@ pub(crate) fn collect_assigned_locals_stmt(stmt: &Stmt, assigned: &mut Vec { collect_assigned_locals_expr(expr, assigned); } - Stmt::PreallocateBoxes(_) => { + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => { // Slot+box allocation; no assignment to an outer variable. } } @@ -539,7 +539,8 @@ fn substitute_lexical_this_in_stmt(stmt: &mut Stmt, replacement: &Expr) { | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-hir/src/analysis/value_types.rs b/crates/perry-hir/src/analysis/value_types.rs index ba3295414b..7822340068 100644 --- a/crates/perry-hir/src/analysis/value_types.rs +++ b/crates/perry-hir/src/analysis/value_types.rs @@ -451,7 +451,8 @@ impl HirTypeEnv { | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} Stmt::If { condition, then_branch, diff --git a/crates/perry-hir/src/audit.rs b/crates/perry-hir/src/audit.rs index c257197049..2152ed51d3 100644 --- a/crates/perry-hir/src/audit.rs +++ b/crates/perry-hir/src/audit.rs @@ -183,7 +183,7 @@ fn visit_stmt(stmt: &Stmt, out: &mut ModuleAudit) { } } // PreallocateBoxes carries only LocalIds, no Expr / Stmt children. - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-hir/src/capability.rs b/crates/perry-hir/src/capability.rs index c8f60be6cb..943bec99d6 100644 --- a/crates/perry-hir/src/capability.rs +++ b/crates/perry-hir/src/capability.rs @@ -221,7 +221,7 @@ fn visit_stmt(stmt: &Stmt, ctx: &mut WalkCtx) { } } } - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-hir/src/dynamic_import.rs b/crates/perry-hir/src/dynamic_import.rs index b66b7c9d31..19eab27ea6 100644 --- a/crates/perry-hir/src/dynamic_import.rs +++ b/crates/perry-hir/src/dynamic_import.rs @@ -545,7 +545,8 @@ fn collect_local_candidate_defs_from_frames<'a>( | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} }, LocalCandidateFrame::Expr(expr) => { match expr { @@ -748,7 +749,8 @@ fn collect_param_literal_sets_from_frames( | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} }, ParamFrame::Expr(expr) => { if let Expr::Closure { params, body, .. } = expr { @@ -891,7 +893,8 @@ fn collect_const_locals_from_frames<'a>( | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } ConstFrame::Expr(expr) => { @@ -1009,7 +1012,8 @@ fn scan_mutations_from_frames( | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } MutationFrame::Expr(expr) => { @@ -1755,7 +1759,8 @@ fn stmt_has_top_level_await(stmt: &Stmt) -> bool { | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => false, + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => false, } } diff --git a/crates/perry-hir/src/dynamic_import/visitors.rs b/crates/perry-hir/src/dynamic_import/visitors.rs index d7b1fc5a27..6bfd8a1186 100644 --- a/crates/perry-hir/src/dynamic_import/visitors.rs +++ b/crates/perry-hir/src/dynamic_import/visitors.rs @@ -307,7 +307,8 @@ fn visit_stmt_for_dyn_imports(stmt: &mut Stmt, f: &mut F) { | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } @@ -409,7 +410,8 @@ fn visit_stmt_for_dyn_imports_ref(stmt: &Stmt, f: &mut F) { | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } @@ -560,7 +562,8 @@ fn visit_stmt_for_worker_new(stmt: &mut Stmt, f: &mut F) { | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } @@ -662,7 +665,8 @@ fn visit_stmt_for_worker_new_ref(stmt: &Stmt, f: &mut F) { | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-hir/src/egress.rs b/crates/perry-hir/src/egress.rs index 280efcd307..3efcb06b20 100644 --- a/crates/perry-hir/src/egress.rs +++ b/crates/perry-hir/src/egress.rs @@ -213,7 +213,7 @@ fn visit_stmt(stmt: &Stmt, ctx: &mut WalkCtx) { } } } - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-hir/src/ir/stmt.rs b/crates/perry-hir/src/ir/stmt.rs index 30bc86e0cd..847f2a4c54 100644 --- a/crates/perry-hir/src/ir/stmt.rs +++ b/crates/perry-hir/src/ir/stmt.rs @@ -65,6 +65,14 @@ pub enum Stmt { /// forward `let`/`const` bindings whose own `Stmt::Let` would /// otherwise lazily allocate the box at source position. Issue #569. PreallocateBoxes(Vec), + /// Like `PreallocateBoxes`, but seeds each box with the TAG_TDZ sentinel + /// (Temporal Dead Zone) instead of `undefined`. Emitted for lexical + /// `let`/`const`/`class` bindings that are referenced (directly or via a + /// closure) BEFORE their declaration in the same function/module body. A + /// read of such a box before its `Stmt::Let` runs throws a spec + /// ReferenceError; the `Stmt::Let` (or `let x;` with no init) overwrites + /// the sentinel with the real value / `undefined`, ending the dead zone. + PreallocateTdzBoxes(Vec), } /// A case in a switch statement diff --git a/crates/perry-hir/src/js_transform/cross_module_natives.rs b/crates/perry-hir/src/js_transform/cross_module_natives.rs index e73cf06c13..fba21a18d3 100644 --- a/crates/perry-hir/src/js_transform/cross_module_natives.rs +++ b/crates/perry-hir/src/js_transform/cross_module_natives.rs @@ -707,7 +707,7 @@ pub fn fix_native_instance_stmt( } Stmt::Throw(e) => fix_native_instance_expr(e, native_instances, local_id_instances), Stmt::Break | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => {} - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-hir/src/js_transform/imports.rs b/crates/perry-hir/src/js_transform/imports.rs index 2b5b7178cc..583f29401d 100644 --- a/crates/perry-hir/src/js_transform/imports.rs +++ b/crates/perry-hir/src/js_transform/imports.rs @@ -460,7 +460,7 @@ pub fn transform_stmt( } } Stmt::Break | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => {} - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-hir/src/js_transform/local_natives.rs b/crates/perry-hir/src/js_transform/local_natives.rs index 8df8e96e5e..df4bf51fa5 100644 --- a/crates/perry-hir/src/js_transform/local_natives.rs +++ b/crates/perry-hir/src/js_transform/local_natives.rs @@ -358,7 +358,7 @@ pub fn fix_class_field_stmt( } } Stmt::Break | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => {} - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } @@ -924,7 +924,7 @@ pub fn fix_native_instance_stmt_with_locals( } } Stmt::Break | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => {} - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-hir/src/lockdown.rs b/crates/perry-hir/src/lockdown.rs index 9b9e946aa0..ee02c1eda3 100644 --- a/crates/perry-hir/src/lockdown.rs +++ b/crates/perry-hir/src/lockdown.rs @@ -170,7 +170,7 @@ fn visit_stmt(stmt: &Stmt, ctx: &mut WalkCtx) { } } } - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-hir/src/lower/context.rs b/crates/perry-hir/src/lower/context.rs index 9a54aa614c..e8ace731d7 100644 --- a/crates/perry-hir/src/lower/context.rs +++ b/crates/perry-hir/src/lower/context.rs @@ -116,6 +116,7 @@ impl LoweringContext { unresolved_ident_as_global: false, with_env_stack: Vec::new(), var_hoisted_ids: HashSet::new(), + tdz_forward_ids: HashSet::new(), catch_param_scopes: Vec::new(), annexb_block_fn_var_ids: HashMap::new(), annexb_block_fn_names_all: HashSet::new(), diff --git a/crates/perry-hir/src/lower/expr_function.rs b/crates/perry-hir/src/lower/expr_function.rs index e7143dcc3e..0f1372c1c3 100644 --- a/crates/perry-hir/src/lower/expr_function.rs +++ b/crates/perry-hir/src/lower/expr_function.rs @@ -893,6 +893,9 @@ fn lower_fn_expr_anon(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> Resul // snapshot of the empty slot. ctx.var_hoisted_ids.insert(id); hoisted_id_set.insert(id); + // Lexical let/const forward-decl: TDZ-eligible. + // A read before the declaration runs throws. + ctx.tdz_forward_ids.insert(id); ctx.lexical_forward_decls.insert(ident.id.span.lo.0, id); } } @@ -1115,9 +1118,25 @@ fn lower_fn_expr_anon(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> Resul } } prealloc.sort(); - if !prealloc.is_empty() { - let mut with_prealloc: Vec = Vec::with_capacity(combined.len() + 1); - with_prealloc.push(Stmt::PreallocateBoxes(prealloc)); + // Split TDZ-seeded lexical `let`/`const` boxes from ordinary + // boxes (see block.rs Phase 5 for the rationale). + let mut tdz_prealloc: Vec = Vec::new(); + let mut plain_prealloc: Vec = Vec::new(); + for id in prealloc { + if ctx.tdz_forward_ids.contains(&id) { + tdz_prealloc.push(id); + } else { + plain_prealloc.push(id); + } + } + if !plain_prealloc.is_empty() || !tdz_prealloc.is_empty() { + let mut with_prealloc: Vec = Vec::with_capacity(combined.len() + 2); + if !plain_prealloc.is_empty() { + with_prealloc.push(Stmt::PreallocateBoxes(plain_prealloc)); + } + if !tdz_prealloc.is_empty() { + with_prealloc.push(Stmt::PreallocateTdzBoxes(tdz_prealloc)); + } with_prealloc.extend(combined); combined = with_prealloc; } diff --git a/crates/perry-hir/src/lower/lowering_context.rs b/crates/perry-hir/src/lower/lowering_context.rs index 8922f59405..6283b1da94 100644 --- a/crates/perry-hir/src/lower/lowering_context.rs +++ b/crates/perry-hir/src/lower/lowering_context.rs @@ -386,6 +386,14 @@ pub struct LoweringContext { /// continue to lexically shadow the object environment. pub(crate) with_env_stack: Vec, pub(crate) var_hoisted_ids: HashSet, + /// LocalIds of lexical let/const bindings that are forward-referenced + /// (read before their declaration, directly or via a closure) in the + /// current function/module body. These get a TDZ-seeded box via + /// Stmt::PreallocateTdzBoxes so a read-before-declaration throws a spec + /// ReferenceError. A subset of var_hoisted_ids restricted to lexical + /// (non-var) bindings. Populated by pre_register_forward_captured_lets + /// and drained when the body's prealloc set is assembled. + pub(crate) tdz_forward_ids: HashSet, /// Names bound by an enclosing `catch (e)` parameter that is currently in /// scope (a stack, innermost last). Annex B B.3.4: a `var e = init;` whose /// name collides with a live catch parameter assigns to that *catch diff --git a/crates/perry-hir/src/lower_decl/block.rs b/crates/perry-hir/src/lower_decl/block.rs index d0cde31e9b..1f855d0627 100644 --- a/crates/perry-hir/src/lower_decl/block.rs +++ b/crates/perry-hir/src/lower_decl/block.rs @@ -77,6 +77,10 @@ pub(crate) fn pre_register_forward_captured_lets( if !already_in_scope { let id = ctx.define_local(name, Type::Any); ctx.var_hoisted_ids.insert(id); + // Lexical let/const forward-decl: mark TDZ-eligible + // so its box is seeded with the TAG_TDZ sentinel and + // a read before the declaration throws. + ctx.tdz_forward_ids.insert(id); forward_boxed_ids.push(id); ctx.lexical_forward_decls.insert(span_lo, id); } @@ -1185,11 +1189,31 @@ pub fn lower_fn_body_block_stmt( } prealloc.sort(); - // Phase 5: assemble the final body — PreallocateBoxes (if any), - // then the hoisted FnDecl Lets, then everything else. + // Split the prealloc set into TDZ-seeded lexical `let`/`const` boxes and + // ordinary (`var` / hoisted-closure / FnDecl-capture) boxes. Only genuine + // lexical bindings recorded in `tdz_forward_ids` get the TAG_TDZ sentinel; + // everything else keeps the historical `undefined`-seeded behavior so a + // forward-captured `var` or hoisted function still reads `undefined`, never + // throws. The dead zone ends when the binding's own `Stmt::Let` runs. + let mut tdz_prealloc: Vec = Vec::new(); + let mut plain_prealloc: Vec = Vec::new(); + for id in prealloc { + if ctx.tdz_forward_ids.contains(&id) { + tdz_prealloc.push(id); + } else { + plain_prealloc.push(id); + } + } + + // Phase 5: assemble the final body — PreallocateBoxes / + // PreallocateTdzBoxes (if any), then the hoisted FnDecl Lets, then + // everything else. let mut result: Vec = Vec::new(); - if !prealloc.is_empty() { - result.push(Stmt::PreallocateBoxes(prealloc)); + if !plain_prealloc.is_empty() { + result.push(Stmt::PreallocateBoxes(plain_prealloc)); + } + if !tdz_prealloc.is_empty() { + result.push(Stmt::PreallocateTdzBoxes(tdz_prealloc)); } result.extend(var_slot_lets); result.extend(hoisted_lets); @@ -1360,7 +1384,8 @@ pub fn collect_refs_in_closure_bodies_stmt( | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-hir/src/lower_decl/class_captures.rs b/crates/perry-hir/src/lower_decl/class_captures.rs index ba27375cb0..6712ff7e74 100644 --- a/crates/perry-hir/src/lower_decl/class_captures.rs +++ b/crates/perry-hir/src/lower_decl/class_captures.rs @@ -813,6 +813,7 @@ pub(crate) fn append_new_args_stmt( | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-hir/src/monomorph/defaults.rs b/crates/perry-hir/src/monomorph/defaults.rs index 5e8bba700a..05784e5f2c 100644 --- a/crates/perry-hir/src/monomorph/defaults.rs +++ b/crates/perry-hir/src/monomorph/defaults.rs @@ -173,7 +173,7 @@ fn fill_defaults_in_stmt(stmt: &mut Stmt, cx: &DefaultFill) { } } Stmt::Break | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => {} - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-hir/src/monomorph/driver.rs b/crates/perry-hir/src/monomorph/driver.rs index 88ecd1a2f0..361d41a16a 100644 --- a/crates/perry-hir/src/monomorph/driver.rs +++ b/crates/perry-hir/src/monomorph/driver.rs @@ -201,7 +201,7 @@ fn collect_instantiations_in_stmt( } } Stmt::Break | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => {} - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-hir/src/monomorph/substitute_expr.rs b/crates/perry-hir/src/monomorph/substitute_expr.rs index a5d6940a38..4dadb0fb84 100644 --- a/crates/perry-hir/src/monomorph/substitute_expr.rs +++ b/crates/perry-hir/src/monomorph/substitute_expr.rs @@ -1127,5 +1127,6 @@ fn substitute_stmt(stmt: &Stmt, substitutions: &HashMap) -> Stmt { .collect(), }, Stmt::PreallocateBoxes(ids) => Stmt::PreallocateBoxes(ids.clone()), + Stmt::PreallocateTdzBoxes(ids) => Stmt::PreallocateTdzBoxes(ids.clone()), } } diff --git a/crates/perry-hir/src/monomorph/update_call_sites.rs b/crates/perry-hir/src/monomorph/update_call_sites.rs index d5a48a7106..4b218fab16 100644 --- a/crates/perry-hir/src/monomorph/update_call_sites.rs +++ b/crates/perry-hir/src/monomorph/update_call_sites.rs @@ -120,7 +120,7 @@ fn update_call_sites_in_stmt( } } Stmt::Break | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => {} - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-hir/src/stable_hash/stmts.rs b/crates/perry-hir/src/stable_hash/stmts.rs index 46ec0963bd..c6b36c6d1c 100644 --- a/crates/perry-hir/src/stable_hash/stmts.rs +++ b/crates/perry-hir/src/stable_hash/stmts.rs @@ -105,6 +105,10 @@ impl SH for Stmt { tag(h, 15); ids.hash(h); } + Stmt::PreallocateTdzBoxes(ids) => { + tag(h, 16); + ids.hash(h); + } } } } diff --git a/crates/perry-runtime/src/box.rs b/crates/perry-runtime/src/box.rs index 297b954c10..0058a34ff5 100644 --- a/crates/perry-runtime/src/box.rs +++ b/crates/perry-runtime/src/box.rs @@ -196,7 +196,21 @@ pub extern "C" fn js_box_get_bits(ptr: *mut Box) -> i64 { // now see `undefined`. return crate::value::TAG_UNDEFINED as i64; } - (*ptr).value as i64 + let bits = (*ptr).value; + // Temporal Dead Zone: a lexical `let`/`const`/`class` box seeded with + // the TAG_TDZ sentinel at scope entry throws a spec ReferenceError when + // read before its declaration runs (which overwrites the sentinel with + // a real value). TAG_TDZ is a reserved bit pattern no legitimate value + // ever holds, so this branch is only ever taken on a genuine + // read-before-initialization — making the check zero-regression for + // every already-initialized box. The name is passed as `undefined` + // because this choke point is name-agnostic (it serves direct, + // closure-captured, and compound reads alike); the resulting message is + // the spec-generic form. + if bits == crate::value::TAG_TDZ { + crate::error::js_throw_reference_error_tdz(f64::from_bits(crate::value::TAG_UNDEFINED)); + } + bits as i64 } } diff --git a/crates/perry-runtime/src/error.rs b/crates/perry-runtime/src/error.rs index 96f04c25e0..eae9003794 100644 --- a/crates/perry-runtime/src/error.rs +++ b/crates/perry-runtime/src/error.rs @@ -926,6 +926,31 @@ fn throw_reference_error_message(message: &'static [u8]) -> ! { crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) } +/// Temporal Dead Zone ReferenceError. Thrown when a lexical `let`/`const`/ +/// `class` binding is read, `typeof`-d, or compound-assigned before its +/// declaration has been evaluated — i.e. while its box still holds the +/// `TAG_TDZ` sentinel. `name` is the NaN-boxed binding name (or `undefined` +/// when codegen could not thread a name through, e.g. a captured box read). +/// Message matches V8/Node byte-for-byte: `Cannot access x before +/// initialization`. +#[no_mangle] +pub extern "C" fn js_throw_reference_error_tdz(name: f64) -> f64 { + let name = value_to_lossy_string(name); + let msg = if name.is_empty() { + "Cannot access uninitialized variable before initialization".to_string() + } else { + format!("Cannot access {} before initialization", name) + }; + let msg_str = js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + let err_ptr = js_referenceerror_new(msg_str); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err_ptr as i64)) +} + +/// Keepalive anchor for the auto-optimize whole-program build (generated-code- +/// and runtime-only callee). +#[used] +static KEEP_JS_THROW_REFERENCE_ERROR_TDZ: extern "C" fn(f64) -> f64 = js_throw_reference_error_tdz; + #[no_mangle] pub extern "C" fn js_throw_reference_error_unresolved_get() -> f64 { throw_reference_error_message(b"identifier is not defined") diff --git a/crates/perry-runtime/src/value/mod.rs b/crates/perry-runtime/src/value/mod.rs index 70726c741c..7d83ef9a6a 100644 --- a/crates/perry-runtime/src/value/mod.rs +++ b/crates/perry-runtime/src/value/mod.rs @@ -56,7 +56,7 @@ mod tests; pub(crate) use tags::{ BIGINT_TAG, INT32_MASK, INT32_TAG, JS_HANDLE_TAG, POINTER_MASK, POINTER_TAG, SHORT_STRING_DATA_MASK, SHORT_STRING_LEN_MASK, SHORT_STRING_LEN_SHIFT, SHORT_STRING_TAG, - STRING_TAG, TAG_FALSE, TAG_HOLE, TAG_MASK, TAG_NULL, TAG_TRUE, TAG_UNDEFINED, + STRING_TAG, TAG_FALSE, TAG_HOLE, TAG_MASK, TAG_NULL, TAG_TDZ, TAG_TRUE, TAG_UNDEFINED, }; pub use tags::{ JS_HANDLE_CALL_METHOD, JS_HANDLE_TYPEOF, JS_NATIVE_ASYNC_HOOKS_CONSTRUCT, diff --git a/crates/perry-runtime/src/value/tags.rs b/crates/perry-runtime/src/value/tags.rs index 1962ae9dff..aded82b68f 100644 --- a/crates/perry-runtime/src/value/tags.rs +++ b/crates/perry-runtime/src/value/tags.rs @@ -36,6 +36,16 @@ pub(crate) const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; /// payload can never be mistaken for a hole. pub(crate) const TAG_HOLE: u64 = 0x7FFC_0000_0000_0010; +/// TDZ (Temporal Dead Zone) sentinel. A lexical `let`/`const`/`class` binding +/// created at scope entry but not yet initialized holds this value in its box +/// cell. Reading it (via `js_box_get_bits`) throws a spec `ReferenceError` +/// ("Cannot access ... before initialization"). Chosen in the same 0x7FFC +/// singleton namespace, distinct from UNDEFINED/NULL/FALSE/TRUE/HOLE so no +/// legitimate NaN-box payload can ever be mistaken for a TDZ hole. Unlike a +/// hole, code never silently coerces it to `undefined` — the whole point is to +/// throw on any observation before the declaration is evaluated. +pub(crate) const TAG_TDZ: u64 = 0x7FFC_0000_0000_0011; + /// Pointer tag: 0x7FFD_XXXX_XXXX_XXXX (48 bits for pointer) - objects/arrays pub(crate) const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; pub(crate) const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; diff --git a/crates/perry-transform/src/deforest/scan.rs b/crates/perry-transform/src/deforest/scan.rs index 44dbe28a3a..b75016ca93 100644 --- a/crates/perry-transform/src/deforest/scan.rs +++ b/crates/perry-transform/src/deforest/scan.rs @@ -261,7 +261,8 @@ fn scan_stmt_for_closures( | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } @@ -431,7 +432,8 @@ impl StmtRefAllWalker<'_> { | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-transform/src/deforest/walk.rs b/crates/perry-transform/src/deforest/walk.rs index 93579b2ea7..79316e4881 100644 --- a/crates/perry-transform/src/deforest/walk.rs +++ b/crates/perry-transform/src/deforest/walk.rs @@ -165,7 +165,7 @@ pub fn max_in_stmt(stmt: &Stmt, max_id: &mut LocalId) { } } Stmt::Labeled { body, .. } => max_in_stmt(body, max_id), - Stmt::PreallocateBoxes(ids) => { + Stmt::PreallocateBoxes(ids) | Stmt::PreallocateTdzBoxes(ids) => { for id in ids { *max_id = (*max_id).max(*id); } diff --git a/crates/perry-transform/src/generator/id_scan.rs b/crates/perry-transform/src/generator/id_scan.rs index 134f9b76aa..a31f9cc33e 100644 --- a/crates/perry-transform/src/generator/id_scan.rs +++ b/crates/perry-transform/src/generator/id_scan.rs @@ -262,7 +262,7 @@ pub fn scan_stmt_for_max_local(stmt: &Stmt, max_id: &mut LocalId) { // closure-conversion pass that ran before this scan); they are // usually also visible via captures lists, but max them here so the // scan does not depend on that invariant. - Stmt::PreallocateBoxes(ids) => { + Stmt::PreallocateBoxes(ids) | Stmt::PreallocateTdzBoxes(ids) => { for id in ids { *max_id = (*max_id).max(*id); } diff --git a/crates/perry-transform/src/generator/iter_result_rewrite.rs b/crates/perry-transform/src/generator/iter_result_rewrite.rs index a359539871..b5d84f6db0 100644 --- a/crates/perry-transform/src/generator/iter_result_rewrite.rs +++ b/crates/perry-transform/src/generator/iter_result_rewrite.rs @@ -71,7 +71,8 @@ pub fn rewrite_stmt(stmt: &mut Stmt) { | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} Stmt::Let { init, .. } => { if let Some(e) = init.as_mut() { rewrite_expr(e); diff --git a/crates/perry-transform/src/generator/lower/call_this.rs b/crates/perry-transform/src/generator/lower/call_this.rs index 63bc817014..014cfd06e1 100644 --- a/crates/perry-transform/src/generator/lower/call_this.rs +++ b/crates/perry-transform/src/generator/lower/call_this.rs @@ -23,7 +23,8 @@ pub(crate) fn generator_stmt_uses_call_this(stmt: &Stmt) -> bool { | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => false, + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => false, Stmt::If { condition, then_branch, diff --git a/crates/perry-transform/src/inline/analysis.rs b/crates/perry-transform/src/inline/analysis.rs index 904d982537..109941a308 100644 --- a/crates/perry-transform/src/inline/analysis.rs +++ b/crates/perry-transform/src/inline/analysis.rs @@ -481,7 +481,7 @@ pub fn construction_stmt_can_affect_method_lookup( getter_names, setter_names, ), - Stmt::PreallocateBoxes(_) => false, + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => false, Stmt::While { .. } | Stmt::DoWhile { .. } | Stmt::For { .. } diff --git a/crates/perry-transform/src/inline/closure_analysis.rs b/crates/perry-transform/src/inline/closure_analysis.rs index 805dfeb103..5cbcddf569 100644 --- a/crates/perry-transform/src/inline/closure_analysis.rs +++ b/crates/perry-transform/src/inline/closure_analysis.rs @@ -639,7 +639,7 @@ pub fn has_simple_control_flow(stmts: &[Stmt]) -> bool { | Stmt::LabeledContinue(_) => { return false; } - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } true @@ -811,7 +811,7 @@ pub fn find_max_local_id(stmts: &[Stmt]) -> LocalId { } } Stmt::Break | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => {} - Stmt::PreallocateBoxes(ids) => { + Stmt::PreallocateBoxes(ids) | Stmt::PreallocateTdzBoxes(ids) => { for id in ids { *max_id = (*max_id).max(*id); } diff --git a/crates/perry-transform/src/inline/cross_module.rs b/crates/perry-transform/src/inline/cross_module.rs index 21472a4832..f48df4fbde 100644 --- a/crates/perry-transform/src/inline/cross_module.rs +++ b/crates/perry-transform/src/inline/cross_module.rs @@ -81,7 +81,7 @@ pub fn is_cross_module_safe(body: &[Stmt]) -> bool { && finally.as_ref().is_none_or(|f| f.iter().all(check_stmt)) } Stmt::Labeled { body, .. } => check_stmt(body.as_ref()), - Stmt::PreallocateBoxes(_) => true, + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => true, } } body.iter().all(check_stmt) @@ -331,7 +331,7 @@ pub fn is_cross_module_safe_with_externs(body: &[Stmt], extern_names: &mut Vec check_stmt(body.as_ref(), extern_names), - Stmt::PreallocateBoxes(_) => true, + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => true, } } body.iter().all(|s| check_stmt(s, extern_names)) @@ -468,7 +468,7 @@ pub fn body_references_class_in_set(stmts: &[Stmt], set: &HashSet) -> bo .is_some_and(|f| f.iter().any(|s| check_stmt(s, set))) } Stmt::Labeled { body, .. } => check_stmt(body.as_ref(), set), - Stmt::PreallocateBoxes(_) => false, + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => false, } } stmts.iter().any(|s| check_stmt(s, set)) diff --git a/crates/perry-transform/src/inline/exact_receivers.rs b/crates/perry-transform/src/inline/exact_receivers.rs index b60c6245fe..5b16e44d15 100644 --- a/crates/perry-transform/src/inline/exact_receivers.rs +++ b/crates/perry-transform/src/inline/exact_receivers.rs @@ -72,7 +72,7 @@ pub fn apply_exact_receiver_stmt_effect(stmt: &Stmt, facts: &mut ExactReceiverFa | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => {} - Stmt::PreallocateBoxes(ids) => { + Stmt::PreallocateBoxes(ids) | Stmt::PreallocateTdzBoxes(ids) => { for id in ids { facts.remove(id); } @@ -277,7 +277,8 @@ pub fn collect_exact_receiver_refs_in_stmt( | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-transform/src/inline/mod.rs b/crates/perry-transform/src/inline/mod.rs index 7c8cc85cad..583a0c9c0e 100644 --- a/crates/perry-transform/src/inline/mod.rs +++ b/crates/perry-transform/src/inline/mod.rs @@ -215,7 +215,7 @@ pub fn inline_functions( } } Stmt::Labeled { body, .. } => walk_stmt_exprs(body.as_ref(), f), - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-transform/src/inline/substitute.rs b/crates/perry-transform/src/inline/substitute.rs index 0aeb8a880b..e17bcb7908 100644 --- a/crates/perry-transform/src/inline/substitute.rs +++ b/crates/perry-transform/src/inline/substitute.rs @@ -339,7 +339,7 @@ pub fn substitute_locals_in_stmts( } substitute_locals_in_stmts(body, param_map, next_local_id); } - Stmt::PreallocateBoxes(ids) => { + Stmt::PreallocateBoxes(ids) | Stmt::PreallocateTdzBoxes(ids) => { // Issue #569: remap each id in the prealloc list. Inlining // can rename body locals so the slot+box allocation must // refer to the new ids. If a callee with a PreallocateBoxes diff --git a/crates/perry-transform/src/inline/super_detect.rs b/crates/perry-transform/src/inline/super_detect.rs index 7629f5593e..b3ac8ffe9e 100644 --- a/crates/perry-transform/src/inline/super_detect.rs +++ b/crates/perry-transform/src/inline/super_detect.rs @@ -124,7 +124,8 @@ fn stmt_contains_lexical_super(stmt: &Stmt) -> bool { | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => false, + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => false, } } diff --git a/crates/perry-transform/src/unroll/escape_analysis.rs b/crates/perry-transform/src/unroll/escape_analysis.rs index dfe6527c05..e9be9b2c49 100644 --- a/crates/perry-transform/src/unroll/escape_analysis.rs +++ b/crates/perry-transform/src/unroll/escape_analysis.rs @@ -204,7 +204,8 @@ fn count_local_refs_stmt(stmt: &Stmt, counts: &mut HashMap) { | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => {} + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} } } diff --git a/crates/perry-transform/src/unroll/mod.rs b/crates/perry-transform/src/unroll/mod.rs index 0db9d0ee11..87e12eaf77 100644 --- a/crates/perry-transform/src/unroll/mod.rs +++ b/crates/perry-transform/src/unroll/mod.rs @@ -508,7 +508,7 @@ fn stmt_is_unrollable(stmt: &Stmt, iv_id: LocalId, loop_depth: u32) -> bool { .all(|s| stmt_is_unrollable(s, iv_id, loop_depth + 1)) }) } - Stmt::PreallocateBoxes(_) => true, + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => true, } } @@ -647,7 +647,7 @@ fn substitute_localget_with_int_in_stmt(stmt: &mut Stmt, iv_id: LocalId, value: substitute_localget_with_int_in_stmt(body, iv_id, value); } Stmt::Break | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => {} - Stmt::PreallocateBoxes(_) => {} + Stmt::PreallocateBoxes(_) | Stmt::PreallocateTdzBoxes(_) => {} } } @@ -825,7 +825,7 @@ fn refresh_in_stmt( } Stmt::Labeled { body, .. } => refresh_in_stmt(body, remap, next_id, next_func_id), Stmt::Break | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => {} - Stmt::PreallocateBoxes(ids) => { + Stmt::PreallocateBoxes(ids) | Stmt::PreallocateTdzBoxes(ids) => { for id in ids.iter_mut() { alloc_fresh(remap, next_id, id); } diff --git a/crates/perry/src/commands/compile/collect_modules/crypto_ns.rs b/crates/perry/src/commands/compile/collect_modules/crypto_ns.rs index 77df0f2552..7cfbbeed9a 100644 --- a/crates/perry/src/commands/compile/collect_modules/crypto_ns.rs +++ b/crates/perry/src/commands/compile/collect_modules/crypto_ns.rs @@ -120,7 +120,8 @@ fn stmt_uses_global_crypto_namespace(stmt: &Stmt) -> bool { | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => false, + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => false, } }