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
7 changes: 6 additions & 1 deletion crates/perry-codegen/src/collectors/i32_locals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ pub fn is_strictly_i32_bounded_expr(
use perry_hir::{BinaryOp, Expr};
match e {
Expr::Integer(_) => true,
Expr::Update { .. } => true,
// `x++`/`x--` is i32-bounded ONLY when the updated local is itself a
// known integer (a loop counter). The previous unconditional `true`
// truncated `var y = x++` to an i32 slot even when `x` held a
// fractional/non-number value — so `var x = 1.1; var y = x++` stored
// `y = (i32)1.1 = 1` instead of the spec's coerced old value `1.1`.
Expr::Update { id, .. } => known_int_locals.contains(id),
// `expr | 0` / `expr >>> 0` ToInt32/ToUint32 idioms — explicit i32
// coercion, hard-bounded.
Expr::Binary { op, right, .. }
Expand Down
14 changes: 9 additions & 5 deletions crates/perry-codegen/src/expr/compare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
op,
CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge
);
let bigint_fast_path = if is_relational_op {
is_bigint_expr(ctx, left) && is_bigint_expr(ctx, right)
} else {
is_bigint_expr(ctx, left) || is_bigint_expr(ctx, right)
};
// The `js_bigint_cmp` fast path is valid ONLY when BOTH operands are
// statically BigInt. The previous equality variant fired when *either*
// side was BigInt and fed `js_bigint_cmp` a non-BigInt operand
// (`0n != undefined`, `0n == ""`), dereferencing an undefined/string
// NaN-box as a BigIntHeader → garbage. Mixed-type BigInt equality now
// falls through to `js_loose_eq` (loose, with full BigInt coercion) /
// `fcmp` (strict, where a type mismatch is correctly never-equal).
// Relational mixed-type already fell through to `js_rel_*`.
let bigint_fast_path = is_bigint_expr(ctx, left) && is_bigint_expr(ctx, right);
if bigint_fast_path {
let l = lower_expr(ctx, left)?;
let r = lower_expr(ctx, right)?;
Expand Down
48 changes: 32 additions & 16 deletions crates/perry-codegen/src/expr/literals_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,30 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// module globals.
Expr::Update { id, op, prefix } => {
super::invalidate_local_write_facts(ctx, *id);
// Spec ToNumber: `x++`/`++x` coerce the operand to a number
// (ToNumber on bool/null/string/object-with-valueOf) before the
// add/sub, and the *returned* value (postfix) is the coerced
// number, not the original boxed operand. Statically-integer loop
// counters already hold a real f64 and skip the call to keep the
// hot path a single `fadd`. (BigInt operands convert to Number here
// — full BigInt-preserving `++`/`--` additionally needs the Update
// result's static type to be inferred as BigInt so `===` uses the
// value path, not `fcmp`; deferred.)
let needs_numeric_coerce =
!ctx.integer_locals.contains(id) && !ctx.unsigned_i32_locals.contains(id);
let coerce_old = |blk: &mut crate::block::LlBlock, raw: &str| -> String {
if needs_numeric_coerce {
blk.call(DOUBLE, "js_number_coerce", &[(DOUBLE, raw)])
} else {
raw.to_string()
}
};
let step_new = |blk: &mut crate::block::LlBlock, old_num: &str| -> String {
match op {
UpdateOp::Increment => blk.fadd(old_num, "1.0"),
UpdateOp::Decrement => blk.fsub(old_num, "1.0"),
}
};
// Closure capture path: runtime get + add/sub + runtime set.
if let Some(&capture_idx) = ctx.closure_captures.get(id) {
let closure_ptr = ctx
Expand All @@ -681,10 +705,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
);
let box_ptr = blk.bitcast_double_to_i64(&cap_dbl);
let old = blk.call(DOUBLE, "js_box_get", &[(I64, &box_ptr)]);
let new = match op {
UpdateOp::Increment => blk.fadd(&old, "1.0"),
UpdateOp::Decrement => blk.fsub(&old, "1.0"),
};
let old = coerce_old(blk, &old);
let new = step_new(blk, &old);
blk.call_void("js_box_set", &[(I64, &box_ptr), (DOUBLE, &new)]);
return Ok(if *prefix { new } else { old });
}
Expand All @@ -694,10 +716,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
&[(I64, &closure_ptr), (I32, &idx_str)],
);
let blk = ctx.block();
let new = match op {
UpdateOp::Increment => blk.fadd(&old, "1.0"),
UpdateOp::Decrement => blk.fsub(&old, "1.0"),
};
let old = coerce_old(blk, &old);
let new = step_new(blk, &old);
blk.call_void(
"js_closure_set_capture_f64",
&[(I64, &closure_ptr), (I32, &idx_str), (DOUBLE, &new)],
Expand All @@ -713,10 +733,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
let box_dbl = blk.load(DOUBLE, &slot);
let box_ptr = blk.bitcast_double_to_i64(&box_dbl);
let old = blk.call(DOUBLE, "js_box_get", &[(I64, &box_ptr)]);
let new = match op {
UpdateOp::Increment => blk.fadd(&old, "1.0"),
UpdateOp::Decrement => blk.fsub(&old, "1.0"),
};
let old = coerce_old(blk, &old);
let new = step_new(blk, &old);
blk.call_void("js_box_set", &[(I64, &box_ptr), (DOUBLE, &new)]);
return Ok(if *prefix { new } else { old });
}
Expand All @@ -731,10 +749,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
};
let blk = ctx.block();
let old = blk.load(DOUBLE, &storage);
let new = match op {
UpdateOp::Increment => blk.fadd(&old, "1.0"),
UpdateOp::Decrement => blk.fsub(&old, "1.0"),
};
let old = coerce_old(blk, &old);
let new = step_new(blk, &old);
if storage_is_root {
// Module globals are registered mutable GC roots and route
// through the root helper; the raw store below is stack-only.
Expand Down
16 changes: 15 additions & 1 deletion crates/perry-hir/src/lower/lower_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,21 @@ pub(crate) fn lower_expr(ctx: &mut LoweringContext, expr: &ast::Expr) -> Result<
None
}
}
_ => None,
// Any other right-hand side (a primitive literal like
// `x instanceof true`, `this`, a call `x instanceof f()`,
// a parenthesized/conditional class ref, …) is NOT a
// statically-resolvable class name. The old `_ => "Object"`
// `ty` substitution silently treated these as
// `instanceof Object` and returned `false`; ECMAScript
// requires evaluating the operand and throwing a TypeError
// when it is not a constructor (`true instanceof true`,
// `({}) instanceof this`). Lower the operand to a value and
// route through `js_instanceof_dynamic`, which both resolves
// every constructor shape and throws on a non-callable RHS.
_ => match lower_expr(ctx, &bin.right) {
Ok(e) => Some(Box::new(e)),
Err(_) => None,
},
};
return Ok(Expr::InstanceOf { expr, ty, ty_expr });
}
Expand Down
74 changes: 74 additions & 0 deletions crates/perry-runtime/src/builtins/arithmetic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ pub extern "C" fn js_eq(a: JSValue, b: JSValue) -> JSValue {
JSValue::bool(result != 0)
}

/// Whether `v` has ECMAScript Type Object (not a primitive). True for plain
/// objects, arrays, functions, Dates and boxed primitive wrappers; false for
/// Symbols (which are NaN-boxed pointers but are primitives) and for the
/// native-handle id-space below `0x100000` (sockets, zlib streams, …) which
/// must not be dereferenced as heap objects in a coercion path.
fn eq_is_object(v: JSValue) -> bool {
if !v.is_pointer() {
return false;
}
let ptr = v.as_pointer::<u8>();
if ptr.is_null() || (ptr as usize) < 0x100000 {
return false;
}
!crate::symbol::is_registered_symbol(ptr as usize)
}

/// JS abstract equality (==). Implements the coercion rules:
/// - Same type: use strict equality
/// - null == undefined: true
Expand Down Expand Up @@ -82,6 +98,14 @@ pub extern "C" fn js_loose_eq(a: JSValue, b: JSValue) -> JSValue {
if a.is_null() || a.is_undefined() || b.is_null() || b.is_undefined() {
return JSValue::bool(false);
}
// Object == Object → reference equality (ES2024 §7.2.15 step 1, same Type).
// Distinct object identities already failed the same-bits fast path above,
// so two objects here are never equal — and we must NOT unwrap a boxed
// wrapper when the other side is also an object (`new Boolean(true) !=
// new Boolean(true)` is `true`).
if eq_is_object(a) && eq_is_object(b) {
return JSValue::bool(false);
}
// Boxed primitives compare via their wrapped primitive value under
// abstract equality (`new Number(5) == 5`, and sloppy primitive accessors
// return boxed receivers).
Expand All @@ -91,6 +115,56 @@ pub extern "C" fn js_loose_eq(a: JSValue, b: JSValue) -> JSValue {
if let Some((_, payload)) = boxed_primitive_payload(f64::from_bits(b.bits())) {
return js_loose_eq(a, JSValue::from_bits(payload.to_bits()));
}
// Object == primitive → ToPrimitive(object), then retry (ES2024 §7.2.15
// steps 10-11). Object-vs-object was settled above; symbols are primitives
// (`eq_is_object` excludes them) and correctly fall through to not-equal.
// Done before the BigInt block so `0n == { valueOf() { return 0n } }` works.
if eq_is_object(a) {
let pa = unsafe { rel_to_primitive(f64::from_bits(a.bits())) };
return js_loose_eq(JSValue::from_bits(pa.to_bits()), b);
}
if eq_is_object(b) {
let pb = unsafe { rel_to_primitive(f64::from_bits(b.bits())) };
return js_loose_eq(a, JSValue::from_bits(pb.to_bits()));
}
// BigInt abstract equality (ES2024 §7.2.15). Neither side is
// null/undefined here and boxed wrappers (incl. `Object(0n)`) have already
// been unwrapped above.
if a.is_bigint() || b.is_bigint() {
// BigInt == BigInt → compare by mathematical value.
if a.is_bigint() && b.is_bigint() {
return JSValue::bool(
crate::bigint::js_bigint_cmp(a.as_bigint_ptr(), b.as_bigint_ptr()) == 0,
);
}
let (big, other) = if a.is_bigint() { (a, b) } else { (b, a) };
// BigInt == Boolean → ToNumber(boolean) then BigInt == Number.
let other = if other.is_bool() {
JSValue::number(if other.as_bool() { 1.0 } else { 0.0 })
} else {
other
};
// BigInt == Number → exact integer comparison (NaN/±Infinity/fractional
// are never equal). `bigint_cmp_f64` returns 0 only on exact equality.
if other.is_number() {
return JSValue::bool(
crate::bigint::bigint_cmp_f64(big.as_bigint_ptr(), other.as_number()) == 0,
);
}
// BigInt == String → StringToBigInt(string); a non-numeric string makes
// the result `false` (StringToBigInt is undefined → not equal).
if other.is_any_string() {
let s = unsafe { string_content_for_bigint(f64::from_bits(other.bits())) };
return match crate::bigint::string_to_bigint(&s) {
Some(ny) => {
JSValue::bool(crate::bigint::js_bigint_cmp(big.as_bigint_ptr(), ny) == 0)
}
None => JSValue::bool(false),
};
}
// BigInt == Symbol / anything else → not equal.
return JSValue::bool(false);
}
// Both strings (heap STRING_TAG and/or inline SHORT_STRING_TAG):
// content compare. The previous `is_string() && is_string()` test
// missed any SSO operand — `JSON.parse(...).foo == "perry"` returned
Expand Down