diff --git a/crates/perry-codegen/src/expr/literals_vars.rs b/crates/perry-codegen/src/expr/literals_vars.rs index 40c16cce90..3e5071094d 100644 --- a/crates/perry-codegen/src/expr/literals_vars.rs +++ b/crates/perry-codegen/src/expr/literals_vars.rs @@ -636,25 +636,28 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // still see the current value. if let Some(i32_slot) = ctx.i32_counter_slots.get(id).cloned() { let structurally_i32 = can_lower_expr_as_i32_in_current_region(ctx, value); - // When `x` is a canonical raw Number, `x | 0` may feed the canonical - // i32 slot directly: materializing a double here only to - // convert it back would duplicate the spec ToInt32. Keep the - // canonical gate explicit — declared Number types are erased, - // so a local can still hold a String or BigInt at runtime. + // Within a stable-packed numeric clone, when `x` is a + // canonical raw Number, `x | 0` may feed the canonical i32 + // slot directly: materializing a double here only to convert + // it back would duplicate the spec ToInt32. Keep BOTH gates + // explicit. Declared Number types are erased, so a local can + // still hold a String or BigInt at runtime; and other loop + // clones own narrower indexed-load contracts that their + // existing assignment lowering must continue to see. // - // This is a claim about the VALUE the ordinary lowering of `x` - // produces, NOT a licence to re-evaluate `x`'s operands - // natively — see the lowering below. + // The canonical gate is a claim about the VALUE the ordinary + // lowering of `x` produces, NOT a licence to re-evaluate `x`'s + // operands natively — see the lowering below. let explicit_numeric_toint32 = matches!( - value.as_ref(), - Expr::Binary { - op: BinaryOp::BitOr, - left, - right, - .. - } if matches!(right.as_ref(), Expr::Integer(0)) - && crate::type_analysis::expr_produces_canonical_raw_f64(ctx, left) - ); + value.as_ref(), + Expr::Binary { + op: BinaryOp::BitOr, + left, + right, + .. + } if matches!(right.as_ref(), Expr::Integer(0)) + && crate::type_analysis::expr_produces_canonical_raw_f64(ctx, left) + ) && !ctx.stable_packed_loop_facts.is_empty(); if !ctx.closure_captures.contains_key(id) && !(ctx.boxed_vars.contains(id) && !ctx.module_globals.contains_key(id)) && (structurally_i32 || explicit_numeric_toint32) diff --git a/crates/perry-codegen/src/stmt/stable_packed_loop.rs b/crates/perry-codegen/src/stmt/stable_packed_loop.rs index e667a5bfab..460a861385 100644 --- a/crates/perry-codegen/src/stmt/stable_packed_loop.rs +++ b/crates/perry-codegen/src/stmt/stable_packed_loop.rs @@ -215,10 +215,19 @@ fn match_candidate( }, _ => return None, }; + let receiver = Expr::LocalGet(array_id); if ctx.reassigned_locals.contains(&array_id) || ctx.closure_captures.contains_key(&array_id) || (ctx.locals.contains_key(&array_id) && ctx.boxed_vars.contains(&array_id)) || (!ctx.locals.contains_key(&array_id) && !ctx.module_globals.contains_key(&array_id)) + // TypedArrays have their own element-width-aware indexed lowering. + // Even though the runtime guard would decline their non-Array header, + // emitting the speculative clone can feed its numeric facts into + // function-wide native-representation selection. In particular a + // Uint32Array XOR then lost the required signed i32 canonicalization + // in the generic copy. Known TypedArrays are never valid candidates, + // so reject them before cloning rather than relying on the guard. + || crate::type_analysis::is_typed_array_expr(ctx, &receiver) || super::loops::stmts_mutate_local(body, counter_id) // A fast-loop `break` reaches that clone's exit block. Live-length // versions use the same block to enter the generic continuation, so diff --git a/crates/perry/tests/issue_8690_loop_versioned_arraylike.rs b/crates/perry/tests/issue_8690_loop_versioned_arraylike.rs index ad686ca6a0..b1eda124b3 100644 --- a/crates/perry/tests/issue_8690_loop_versioned_arraylike.rs +++ b/crates/perry/tests/issue_8690_loop_versioned_arraylike.rs @@ -188,6 +188,42 @@ console.log(checksum); } } +#[test] +fn typed_array_loops_keep_their_width_aware_lowering() { + let dir = tempfile::tempdir().expect("tempdir"); + let source = r#" +function xorU32(values: Uint32Array, n: number): number { + let result = 0 | 0; + for (let i = 0; i < n; i++) { + result = (result ^ values[i & 7]) | 0; + } + return result | 0; +} + +const values = Uint32Array.from([ + 1, 4000000000, 0xffffffff, 7, 0x80000000, 0, 42, 999, +]); +console.log(xorU32(values, 8)); +"#; + let (bin, stderr) = compile(dir.path(), source, true); + let output = run(&bin, dir.path(), false); + assert_success(&output); + assert_eq!(String::from_utf8_lossy(&output.stdout), "-1852517324\n"); + + let ll_path = stderr + .lines() + .find_map(|line| line.split("kept LLVM IR: ").nth(1)) + .map(str::trim) + .map(PathBuf::from) + .unwrap_or_else(|| panic!("PERRY_LLVM_KEEP_IR did not report an IR path\n{stderr}")); + let ir = std::fs::read_to_string(&ll_path).expect("read kept LLVM IR"); + let xor = function_ir(&ir, "__xorU32("); + assert!( + !xor.contains("js_packed_arraylike_loop_guard"), + "a statically known TypedArray must not enter Array loop versioning\n{xor}" + ); +} + #[test] fn mutations_and_moving_gc_resume_with_generic_semantics() { let dir = tempfile::tempdir().expect("tempdir");