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
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2726,6 +2726,8 @@ pub(crate) mod string_window;
mod ptr_numarray_access;
mod ta_param_f64_read;
#[cfg(test)]
mod unary_bigint_tests;
#[cfg(test)]
mod unary_bitnot_tests;
pub(crate) use index_get::{
numeric_index_has_integer_array_index_proof, packed_f64_loop_index_parts,
Expand Down
24 changes: 15 additions & 9 deletions crates/perry-codegen/src/expr/unary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ use perry_hir::{Expr, UnaryOp};

use crate::lower_conditional::lower_expr_with_truthy;
use crate::type_analysis::{
expr_may_return_boxed_value_from_raw_f64_fallback, is_bigint_expr, is_numeric_expr,
is_provably_not_bigint,
expr_may_return_boxed_value_from_raw_f64_fallback, is_numeric_expr, is_provably_not_bigint,
};
use crate::types::{DOUBLE, I32, I64};

Expand All @@ -19,16 +18,23 @@ use super::{is_known_i32_range, lower_expr, FnCtx};
pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
match expr {
Expr::Unary { op, operand } => {
let numeric = is_numeric_expr(ctx, operand)
let statically_numeric = is_numeric_expr(ctx, operand);
let numeric = statically_numeric
&& !expr_may_return_boxed_value_from_raw_f64_fallback(ctx, operand);
let native_bitnot =
matches!(op, UnaryOp::BitNot) && numeric && is_provably_not_bigint(ctx, operand);
let bitnot_known_i32 = native_bitnot && is_known_i32_range(ctx, operand);
// `-<bigint>` must stay a BigInt (`typeof -1n === "bigint"`).
// `fneg` on a NaN-boxed BigInt flips the NaN payload's sign bit
// and produces a garbage number, so route negation through the
// runtime dynamic helper when the operand is statically bigint.
let is_big = matches!(op, UnaryOp::Neg) && is_bigint_expr(ctx, operand);
// Unary minus performs ToNumeric, not ToNumber: a possible BigInt
// operand must use the dynamic helper so `-1n` remains a BigInt.
// A statically numeric expression is already proven to yield a
// Number (even if a boxed cold fallback still needs coercion), and
// a separately proven non-BigInt value can use `js_number_coerce`.
// Everything else may be a BigInt at runtime — notably an indexed
// read from a plain array — and routing it through ToNumber would
// silently round large BigInts before `fneg` (#9142).
let dynamic_neg = matches!(op, UnaryOp::Neg)
&& !statically_numeric
&& !is_provably_not_bigint(ctx, operand);
let (v, precomputed_truthy) = if matches!(op, UnaryOp::Not) {
let (boxed, truthy) = lower_expr_with_truthy(ctx, operand)?;
(boxed, Some(truthy))
Expand All @@ -38,7 +44,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
let blk = ctx.block();
match op {
UnaryOp::Neg => {
if is_big {
if dynamic_neg {
Ok(blk.call(DOUBLE, "js_dynamic_neg", &[(DOUBLE, &v)]))
} else if numeric {
Ok(blk.fneg(&v))
Expand Down
49 changes: 49 additions & 0 deletions crates/perry-codegen/src/expr/unary_bigint_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//! BigInt-aware unary negation routing.

use perry_hir::types::Type;
use perry_hir::{Expr, Stmt, UnaryOp};

use crate::temp_root_coverage::main_ir_for as ir_for;

const VALUES: u32 = 1;
const RESULT: u32 = 2;

#[test]
fn bigint_array_element_negation_uses_dynamic_numeric_dispatch() {
let ir = ir_for(
"bigint_array_element_negation",
vec![
Stmt::Let {
id: VALUES,
name: "values".to_string(),
ty: Type::Array(Box::new(Type::BigInt)),
mutable: false,
init: Some(Expr::Array(vec![Expr::BigInt(
"18446744073709551616".to_string(),
)])),
},
Stmt::Let {
id: RESULT,
name: "result".to_string(),
ty: Type::BigInt,
mutable: false,
init: Some(Expr::Unary {
op: UnaryOp::Neg,
operand: Box::new(Expr::IndexGet {
object: Box::new(Expr::LocalGet(VALUES)),
index: Box::new(Expr::Integer(0)),
}),
}),
},
],
);

assert!(
ir.contains("call double @js_dynamic_neg("),
"a BigInt array element must retain its tag and exact value:\n{ir}"
);
assert!(
!ir.contains("call double @js_number_coerce("),
"BigInt negation must use ToNumeric rather than ToNumber:\n{ir}"
);
}
12 changes: 11 additions & 1 deletion crates/perry-codegen/src/type_analysis/numeric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,18 @@ pub(crate) fn is_numeric_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool {
// operand must not be treated as numeric. (`!x` is a boolean, not
// a number — handled by `is_bool_expr`.)
Expr::Unary { op, operand } => {
// The result is a clean f64 only when the unary lowering did NOT
// route through the dynamic helper — and that lowering keys off
// `is_provably_not_bigint`, a POSITIVE proof, not off
// `!is_bigint_expr`, which merely says "we failed to prove it is
// one". An indexed read from a `bigint[]` is exactly the gap
// between those two: unproven either way, so it takes the dynamic
// path and yields a NaN-boxed BigInt. Claiming that numeric made
// the ENCLOSING operator emit a native `fneg`/`fmul` over the
// BigInt payload, so `-(-a[0])` came back `NaN` and
// `(-a[0]) * (-a[1])` came back `-2n` (#9142 follow-up).
matches!(op, UnaryOp::Neg | UnaryOp::Pos | UnaryOp::BitNot)
&& !is_bigint_expr(ctx, operand)
&& (is_numeric_expr(ctx, operand) || is_provably_not_bigint(ctx, operand))
}
// Explicit numeric-coercion node — lowers to `js_number_coerce`,
// which always yields a clean f64.
Expand Down
11 changes: 11 additions & 0 deletions test-files/test_gap_9142_bigint_array_negation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// #9142: unary minus on an indexed BigInt must preserve BigInt semantics.
// The old lowering treated array elements as Numbers, rounding values above
// 2^53 through f64 before negating them.

const inferred = [2n ** 64n];
console.log(-inferred[0]);
console.log(typeof -inferred[0]);

const annotated: bigint[] = [2n ** 100n + 12345n];
console.log(-annotated[0]);
console.log(typeof -annotated[0]);
Loading