diff --git a/changelog.d/9106-for-head-counter-keeps-init-slot.md b/changelog.d/9106-for-head-counter-keeps-init-slot.md new file mode 100644 index 0000000000..bd4470ca64 --- /dev/null +++ b/changelog.d/9106-for-head-counter-keeps-init-slot.md @@ -0,0 +1,3 @@ +### Fixed + +- Classic `for` heads shaped `for (let i = 0, len = arr.length; i < len; i++)` regained their versioned packed-loop fast clones: when hoisting the tail declarators around a literal-initialized counter is provably unobservable, the counter stays in the loop's own init slot the counted-loop matchers key on, while order-observable heads keep the #9062 source-order lowering. diff --git a/crates/perry-hir/src/lower/for_multi_decl_tests.rs b/crates/perry-hir/src/lower/for_multi_decl_tests.rs index 32845b9132..924971f89a 100644 --- a/crates/perry-hir/src/lower/for_multi_decl_tests.rs +++ b/crates/perry-hir/src/lower/for_multi_decl_tests.rs @@ -79,3 +79,58 @@ fn function_classic_for_lexical_declarators_lower_in_source_order() { .expect("run should lower"); assert_source_ordered_prelude(&function.body); } + +#[test] +fn safe_counter_head_keeps_the_for_init_slot() { + // #9106: `let i = 0, len = arr.length` — a literal-initialized counter + // that no tail declarator mentions. Hoisting the tail around it is + // unobservable, so the counter stays in `For::init` (the versioned + // counted-loop matchers key their admission on it) and only the tail + // moves to the loop-scoped prelude. + let hir = lower( + "function run(arr: number[]) { let sum = 0; for (let i = 0, len = arr.length; i < len; i++) { sum += arr[i]; } return sum; }", + ); + let function = hir + .functions + .iter() + .find(|function| function.name == "run") + .expect("run should lower"); + let for_index = function + .body + .iter() + .position(|stmt| matches!(stmt, Stmt::For { .. })) + .expect("classic for should be present"); + let len_id = function.body[..for_index] + .iter() + .find_map(|stmt| match stmt { + Stmt::Let { id, name, .. } if name == "len" => Some(*id), + _ => None, + }) + .expect("tail binding should be hoisted into the prelude"); + assert!( + hir.classic_for_lexical_bindings.contains(&len_id), + "hoisted tail binding keeps per-iteration capture semantics" + ); + let crate::ir::Stmt::For { + init: Some(init), .. + } = &function.body[for_index] + else { + panic!( + "counter must stay in For::init: {:#?}", + function.body[for_index] + ); + }; + let counter_id = match init.as_ref() { + Stmt::Let { + id, + name, + init: Some(crate::ir::Expr::Integer(0)), + .. + } if name == "i" => *id, + other => panic!("counter Let expected in For::init, got {other:#?}"), + }; + assert!( + !hir.classic_for_lexical_bindings.contains(&counter_id), + "the init-slot counter is handled by the For machinery, not the prelude set" + ); +} diff --git a/crates/perry-hir/src/lower/stmt.rs b/crates/perry-hir/src/lower/stmt.rs index d348b2da36..e0a0802be2 100644 --- a/crates/perry-hir/src/lower/stmt.rs +++ b/crates/perry-hir/src/lower/stmt.rs @@ -1575,13 +1575,22 @@ pub(crate) fn lower_stmt( // head into the loop-scoped prelude in source order, // registering each binding before the next // initializer is lowered. - let has_multiple_decls = var_decl.decls.len() > 1; - for decl in - var_decl - .decls - .iter() - .skip(if has_multiple_decls { 0 } else { 1 }) - { + // + // #9106 carve-out: when hoisting the tail around a + // literal-initialized first declarator is provably + // unobservable, keep that declarator in `For::init` + // — the versioned counted-loop matchers key their + // counter on it (`for (let i = 0, len = arr.length; + // i < len; i++)`). + let first_decl_keeps_init_slot = var_decl.decls.len() == 1 + || crate::lower_decl::for_head_first_decl_keeps_init_slot( + &var_decl.decls, + ); + for decl in var_decl.decls.iter().skip(if first_decl_keeps_init_slot { + 1 + } else { + 0 + }) { if let Some(init_ast) = decl.init.as_ref() { module.init.extend(predeclare_implicit_assignment_targets( ctx, init_ast, @@ -1629,7 +1638,7 @@ pub(crate) fn lower_stmt( init: init_expr, }); } - if has_multiple_decls { + if !first_decl_keeps_init_slot { None } else if let Some(decl) = var_decl.decls.first() { if let Some(init_ast) = decl.init.as_ref() { diff --git a/crates/perry-hir/src/lower_decl/body_stmt.rs b/crates/perry-hir/src/lower_decl/body_stmt.rs index c15cd696b1..56e955f02d 100644 --- a/crates/perry-hir/src/lower_decl/body_stmt.rs +++ b/crates/perry-hir/src/lower_decl/body_stmt.rs @@ -732,13 +732,22 @@ pub fn lower_body_stmt(ctx: &mut LoweringContext, stmt: &ast::Stmt) -> Result 1; - for decl in - var_decl - .decls - .iter() - .skip(if has_multiple_decls { 0 } else { 1 }) - { + // + // #9106 carve-out: when hoisting the tail around a + // literal-initialized first declarator is provably + // unobservable, keep that declarator in `For::init` + // — the versioned counted-loop matchers key their + // counter on it (`for (let i = 0, len = arr.length; + // i < len; i++)`). + let first_decl_keeps_init_slot = var_decl.decls.len() == 1 + || crate::lower_decl::for_head_first_decl_keeps_init_slot( + &var_decl.decls, + ); + for decl in var_decl.decls.iter().skip(if first_decl_keeps_init_slot { + 1 + } else { + 0 + }) { if let Some(init_ast) = decl.init.as_ref() { result.extend(predeclare_implicit_assignment_targets( ctx, init_ast, @@ -788,7 +797,7 @@ pub fn lower_body_stmt(ctx: &mut LoweringContext, stmt: &ast::Stmt) -> Result bool { + use swc_ecma_visit::{Visit, VisitWith}; + let Some((first, tail)) = decls.split_first() else { + return false; + }; + let ast::Pat::Ident(first_ident) = &first.name else { + return false; + }; + if !matches!(first.init.as_deref(), Some(ast::Expr::Lit(_))) { + return false; + } + struct Mentions<'a> { + sym: &'a str, + found: bool, + } + impl Visit for Mentions<'_> { + fn visit_ident(&mut self, ident: &ast::Ident) { + if ident.sym.as_ref() == self.sym { + self.found = true; + } + } + } + let mut mentions = Mentions { + sym: first_ident.id.sym.as_ref(), + found: false, + }; + for decl in tail { + decl.visit_with(&mut mentions); + } + !mentions.found +} diff --git a/crates/perry-hir/src/lower_decl/mod.rs b/crates/perry-hir/src/lower_decl/mod.rs index 3b4ad692c1..31c8011879 100644 --- a/crates/perry-hir/src/lower_decl/mod.rs +++ b/crates/perry-hir/src/lower_decl/mod.rs @@ -55,10 +55,10 @@ pub(crate) use enum_decl::{compute_enum_members, lower_enum_decl}; pub(crate) use fn_decl::lower_fn_decl; pub(crate) use helpers::{ append_synthetic_arguments_param, body_has_use_strict, body_uses_arguments, - build_default_param_stmts, collect_let_decls_in_stmt, is_inspect_custom_key, - is_symbol_iterator_key, lower_well_known_computed_method, mapped_argument_parameter_ids, - params_are_simple_arguments_list, params_use_arguments, symbol_well_known_key, - with_static_member_context, WellKnownComputedMethod, + build_default_param_stmts, collect_let_decls_in_stmt, for_head_first_decl_keeps_init_slot, + is_inspect_custom_key, is_symbol_iterator_key, lower_well_known_computed_method, + mapped_argument_parameter_ids, params_are_simple_arguments_list, params_use_arguments, + symbol_well_known_key, with_static_member_context, WellKnownComputedMethod, }; pub(crate) use interface_decl::lower_interface_decl; pub(crate) use private_members::{