Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 20 additions & 17 deletions crates/perry-codegen/src/expr/literals_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -636,25 +636,28 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// 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)
Expand Down
9 changes: 9 additions & 0 deletions crates/perry-codegen/src/stmt/stable_packed_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions crates/perry/tests/issue_8690_loop_versioned_arraylike.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading