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
42 changes: 42 additions & 0 deletions changelog.d/7237-i32-chain-double-rounding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
### Fixed

- **Integer arithmetic in a local no longer evaluates past double precision (#7232).**
`(x * 1103515245 + 12345) & 0x7fffffff` — an LCG step — printed `654583775`
where Node prints `654583808`. The i32-native fast path
(`crates/perry-codegen/src/expr/i32_fast_path.rs`) evaluated the whole chain
in exact two's-complement `mul/add i32`; ECMAScript evaluates it in doubles,
rounding at every operator. The ~2^61 product is past 2^53, so the double had
already discarded the low bits the exact chain still carried, and the mask
read them straight back. Wrong straight-line and loop-carried, correct only
through a function boundary (where the intermediate gets NaN-boxed and
therefore rounded) — so PRNG seeds, hash mixing, checksum accumulators and ID
arithmetic diverged silently, with no throw and no warning.

The old admission rule required only that every integer *literal* fit in i32,
which is neither necessary (`Math.imul` is defined as an exact low-32
multiply) nor sufficient: `1103515245` fits, and its product with an
i32-range local does not. It is replaced by a magnitude bound carried through
the whole chain and capped at 2^53, the largest integer a double represents
exactly: **below the cap the JS double *is* the exact integer and
`low32(exact) == ToInt32(double)`; above it the two models are different
numbers**, so the chain now falls onto the f64 path whose `fmul`/`fadd` round
where the spec says to. The cap applies to `Add`/`Sub` as well as `Mul` —
two ceiling-width products sum to 2^54 — and both emitters of the chain
consult the same bound, so the gate and the last-resort arithmetic arm cannot
drift apart.

The bound is measured rather than assumed, so correct code keeps its fast
path: an integer literal contributes its own bit width (`h * 31 + c` is 37
bits, not 64), `x & m` with a non-negative literal mask lands in `[0, m]`,
`x >> k` / `x >>> k` by a literal count drop `k` bits, a `const` bound to a
numeric literal contributes *its* width (which is what keeps
`buf[y * WIDTH + x]` exact), and `Math.imul` is exempt entirely. Across all
30 programs in `benchmarks/suite/`, 29 emit byte-identical LLVM IR to before;
the exception is `11_prime_sieve`, whose `for (let j = i * i; …)` preheader
is genuinely unbounded and now rounds.

Covered by `test-files/test_gap_7232_i32_chain_double_rounding.ts` (the
issue's three shapes, every ToInt32-shaped consumer, the 2^53 boundary from
both sides, and the chains that must stay exact) and by fourteen unit tests in
`crates/perry-codegen/src/expr/i32_fast_path/bits_tests.rs` that are
sabotage-checked in both directions.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
5 changes: 5 additions & 0 deletions crates/perry-codegen/src/expr/arrays_finds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ fn lower_index_i32(ctx: &mut FnCtx<'_>, index: &Expr) -> Result<String> {
ctx.flat_const_arrays,
&ctx.array_row_aliases,
ctx.integer_locals,
&ctx.const_number_locals,
ctx.clamp3_functions,
ctx.clamp_u8_functions,
ctx.integer_returning_functions,
Expand Down Expand Up @@ -832,6 +833,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
ctx.flat_const_arrays,
&ctx.array_row_aliases,
ctx.integer_locals,
&ctx.const_number_locals,
ctx.clamp3_functions,
ctx.clamp_u8_functions,
ctx.integer_returning_functions,
Expand All @@ -843,6 +845,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
ctx.flat_const_arrays,
&ctx.array_row_aliases,
ctx.integer_locals,
&ctx.const_number_locals,
ctx.clamp3_functions,
ctx.clamp_u8_functions,
ctx.integer_returning_functions,
Expand Down Expand Up @@ -922,6 +925,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
ctx.flat_const_arrays,
&ctx.array_row_aliases,
ctx.integer_locals,
&ctx.const_number_locals,
ctx.clamp3_functions,
ctx.clamp_u8_functions,
ctx.integer_returning_functions,
Expand All @@ -933,6 +937,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
ctx.flat_const_arrays,
&ctx.array_row_aliases,
ctx.integer_locals,
&ctx.const_number_locals,
ctx.clamp3_functions,
ctx.clamp_u8_functions,
ctx.integer_returning_functions,
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/expr/bigint_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ fn can_lower_i32_for_collection_value(ctx: &FnCtx<'_>, value: &Expr) -> bool {
ctx.flat_const_arrays,
&ctx.array_row_aliases,
ctx.integer_locals,
&ctx.const_number_locals,
ctx.clamp3_functions,
ctx.clamp_u8_functions,
ctx.integer_returning_functions,
Expand Down
39 changes: 9 additions & 30 deletions crates/perry-codegen/src/expr/buffer_access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,41 +200,19 @@ pub(crate) fn access_facts_for_spec(
}
}

// Both of these used to branch on `can_lower_expr_as_i32` into two identical
// arms — `lower_expr_native(.., I32)` either way, so the predicate's answer was
// computed and thrown away. `lower_expr_native` makes the same decision
// internally and correctly; the outer call was pure cost, and #7232 made it a
// whole-subtree walk instead of a shape check. (CodeRabbit, PR #7237.)

fn lower_index_i32_value(ctx: &mut FnCtx<'_>, index: &Expr) -> Result<LoweredValue> {
let value = if can_lower_expr_as_i32(
index,
&ctx.i32_counter_slots,
ctx.flat_const_arrays,
&ctx.array_row_aliases,
ctx.native_facts.integer_locals(),
ctx.clamp3_functions,
ctx.clamp_u8_functions,
ctx.integer_returning_functions,
ctx.i32_identity_functions,
) {
lower_expr_native(ctx, index, crate::native_value::ExpectedNativeRep::I32)?.value
} else {
lower_expr_native(ctx, index, crate::native_value::ExpectedNativeRep::I32)?.value
};
let value = lower_expr_native(ctx, index, crate::native_value::ExpectedNativeRep::I32)?.value;
Ok(LoweredValue::i32(value))
}

fn lower_value_i32(ctx: &mut FnCtx<'_>, value: &Expr) -> Result<String> {
if can_lower_expr_as_i32(
value,
&ctx.i32_counter_slots,
ctx.flat_const_arrays,
&ctx.array_row_aliases,
ctx.native_facts.integer_locals(),
ctx.clamp3_functions,
ctx.clamp_u8_functions,
ctx.integer_returning_functions,
ctx.i32_identity_functions,
) {
Ok(lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I32)?.value)
} else {
Ok(lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I32)?.value)
}
Ok(lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I32)?.value)
}

pub(crate) fn can_lower_integer_typed_array_store_value(ctx: &FnCtx<'_>, value: &Expr) -> bool {
Expand All @@ -244,6 +222,7 @@ pub(crate) fn can_lower_integer_typed_array_store_value(ctx: &FnCtx<'_>, value:
ctx.flat_const_arrays,
&ctx.array_row_aliases,
ctx.native_facts.integer_locals(),
&ctx.const_number_locals,
ctx.clamp3_functions,
ctx.clamp_u8_functions,
ctx.integer_returning_functions,
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/expr/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ pub(crate) fn lower_channel_reduction(ctx: &mut FnCtx<'_>, r: &ChannelReduction)
&flat_ca,
&ara,
&int_locals,
&ctx.const_number_locals,
ctx.clamp3_functions,
ctx.clamp_u8_functions,
ctx.integer_returning_functions,
Expand All @@ -349,6 +350,7 @@ pub(crate) fn lower_channel_reduction(ctx: &mut FnCtx<'_>, r: &ChannelReduction)
&flat_ca,
&ara,
&int_locals,
&ctx.const_number_locals,
ctx.clamp3_functions,
ctx.clamp_u8_functions,
ctx.integer_returning_functions,
Expand Down
Loading
Loading