Skip to content
Closed
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ not_unsafe_ptr_arg_deref = "allow"
# SWC for TypeScript parsing
swc_ecma_parser = "32.0"
swc_ecma_ast = "19.0"
swc_ecma_visit = "19.0"
swc_common = "18.0"
swc_ecma_codegen = "21.0"
swc_ecma_transforms_base = "32.0"
Expand Down
16 changes: 16 additions & 0 deletions crates/perry-codegen/src/codegen/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1565,6 +1565,22 @@ fn collect_inline_invoked_static_blocks(
{
out.insert((class_name.clone(), method_name.clone()));
}
// `ClassExprFresh` invokes its static blocks directly from the
// per-evaluation source-order plan. Treat those calls as inline too;
// otherwise the module-init fallback below invokes every block once
// more with no fresh class object armed as `this`.
if let Expr::ClassExprFresh {
template,
static_init_order,
..
} = e
{
for step in static_init_order {
if let perry_hir::ClassFreshStaticInit::Block(index) = step {
out.insert((template.clone(), format!("__perry_static_init_{index}")));
}
}
}
if let Expr::Closure { body, .. } = e {
for s in body {
walk_stmt(s, out);
Expand Down
11 changes: 10 additions & 1 deletion crates/perry-codegen/src/codegen/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1048,7 +1048,16 @@ pub(super) fn compile_method(
// .pathname` threw. Forward this synthesized ctor's params to the
// runtime dynamic-parent super dispatcher, mirroring the explicit
// `Expr::SuperCall` dynamic-parent path in `expr/this_super_call.rs`.
if builtin_parent_runtime.is_none() && class.extends_expr.is_some() {
let parent_is_uncallable_builtin = class
.extends_name
.as_deref()
.map(crate::expr::is_other_builtin_constructor_name)
.unwrap_or(false)
&& class.extends_name.as_deref() != Some("SharedArrayBuffer");
if builtin_parent_runtime.is_none()
&& class.extends_expr.is_some()
&& !parent_is_uncallable_builtin
{
if let Some(cid) = ctx.class_ids.get(&class.name).copied().filter(|c| *c != 0) {
let undef_lit =
crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED));
Expand Down
16 changes: 9 additions & 7 deletions crates/perry-codegen/src/expr/binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use crate::types::{DOUBLE, I1, I128, I32, I64};

use crate::rooting::with_operands_rooted;

use super::{is_known_finite, lower_expr, FnCtx};
use super::{is_known_i32_range, lower_expr, FnCtx};

/// `helper(left, right)` with each operand rooted across the other's lowering
/// and the group released on every path out (#6951).
Expand Down Expand Up @@ -1101,7 +1101,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// entirely — just fptosi + sitofp (identity for in-range
// values, LLVM eliminates via instcombine).
BinaryOp::BitOr
if matches!(right.as_ref(), Expr::Integer(0)) && is_known_finite(ctx, left) =>
if matches!(right.as_ref(), Expr::Integer(0))
&& is_known_i32_range(ctx, left) =>
{
let blk = ctx.block();
let li = blk.toint32_fast(&l);
Expand All @@ -1112,8 +1113,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
| BinaryOp::BitXor
| BinaryOp::Shl
| BinaryOp::Shr => {
let l_safe = is_known_finite(ctx, left);
let r_safe = is_known_finite(ctx, right);
let l_safe = is_known_i32_range(ctx, left);
let r_safe = is_known_i32_range(ctx, right);
let blk = ctx.block();
let li = if l_safe {
blk.toint32_fast(&l)
Expand All @@ -1136,15 +1137,16 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
blk.sitofp(I32, &v, DOUBLE)
}
BinaryOp::UShr
if matches!(right.as_ref(), Expr::Integer(0)) && is_known_finite(ctx, left) =>
if matches!(right.as_ref(), Expr::Integer(0))
&& is_known_i32_range(ctx, left) =>
{
let blk = ctx.block();
let li = blk.toint32_fast(&l);
blk.uitofp(I32, &li, DOUBLE)
}
BinaryOp::UShr => {
let l_safe = is_known_finite(ctx, left);
let r_safe = is_known_finite(ctx, right);
let l_safe = is_known_i32_range(ctx, left);
let r_safe = is_known_i32_range(ctx, right);
let blk = ctx.block();
let li = if l_safe {
blk.toint32_fast(&l)
Expand Down
75 changes: 10 additions & 65 deletions crates/perry-codegen/src/expr/compare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,16 +257,6 @@ fn lower_string_literal_strict_eq(
ctx.block().phi(I1, &incoming)
}

/// Magnitude comparands for the inline heap-address test in
/// [`lower_strict_eq_inline_any`]. These mirror
/// `perry-runtime::value::addr_class::{HANDLE_BAND_MAX, is_valid_obj_ptr}`:
/// a `POINTER_TAG` payload below `HANDLE_BAND_MAX` is a registry id
/// (net.Socket, fetch, zlib, revocable Proxy, UI widget), NOT an address, and
/// dereferencing one reads unmapped low memory. Anything outside the window
/// takes the runtime call instead of a header load.
const HANDLE_BAND_MAX_I64: &str = "1048576";
const HEAP_ADDR_CEILING_I64: &str = "140737488355328";

/// Inline prefix for the generic `===`/`!==` tail — the arm where BOTH
/// operands are statically unconstrained, which emitted one
/// `js_eq` → `js_jsvalue_equals` call per comparison and nothing else.
Expand All @@ -277,7 +267,7 @@ const HEAP_ADDR_CEILING_I64: &str = "140737488355328";
/// **misses**, so a fast path that settles only the hit is worth nothing —
/// each case below settles one direction of the real traffic.
///
/// Four cases leave without a call. Each is an exact restatement of what
/// Three cases leave without a call. Each is an exact restatement of what
/// `js_jsvalue_equals` computes for that input, not an approximation:
///
/// * **identical bits** ⇒ equal, *unless* the value is a plain (untagged)
Expand All @@ -291,16 +281,12 @@ const HEAP_ADDR_CEILING_I64: &str = "140737488355328";
/// pattern — which is the argument `lower_string_strict_eq_inline` and the
/// runtime's own both-short-string arm already rely on.
/// * **both INT32, different bits** ⇒ different integers, same argument.
/// * **both `POINTER_TAG`, different payloads, and neither header carries
/// `GC_FLAG_FORWARDED`** ⇒ distinct objects. The runtime's pointer arm is
/// `resolve_forwarding(a) == resolve_forwarding(b)`, and
/// `resolve_forwarding` returns its argument unchanged when the forwarding
/// bit is clear — so two *unforwarded* distinct addresses are exactly its
/// `0` case. Anything forwarded (a post-`js_array_grow` alias, a stale
/// pre-evacuation pointer) takes the call and gets the full walk. The
/// header read is the same one `expr/array_push.rs` emits — `gc_flags` at
/// `ptr - 7`, mask `GC_FLAG_FORWARDED` (0x80) — behind the same magnitude
/// guard the runtime applies before any `GcHeader` dereference.
///
/// Distinct `POINTER_TAG` values always take the runtime call. Not every
/// pointer-tag payload is a GC allocation: registered and well-known symbols,
/// for example, are process-lifetime `Box` allocations with no `GcHeader`.
/// Generated code has no access to the runtime's allocation registries, so an
/// address-magnitude check cannot make reading `ptr - GC_HEADER_SIZE` safe.
///
/// Everything else — a raw-bits module-level object slot (top16 zero), a heap
/// string, a bigint, a mixed pair, a boxed wrapper — falls through to
Expand All @@ -314,18 +300,12 @@ fn lower_strict_eq_inline_any(ctx: &mut FnCtx<'_>, l: &str, r: &str) -> String {

let same_idx = ctx.new_block("anyeq.same");
let diff_idx = ctx.new_block("anyeq.diff");
let canon_idx = ctx.new_block("anyeq.canon");
let band_idx = ctx.new_block("anyeq.band");
let fwd_idx = ctx.new_block("anyeq.fwd");
let slow_idx = ctx.new_block("anyeq.slow");
let true_idx = ctx.new_block("anyeq.true");
let false_idx = ctx.new_block("anyeq.false");
let merge_idx = ctx.new_block("anyeq.merge");
let same_l = ctx.block_label(same_idx);
let diff_l = ctx.block_label(diff_idx);
let canon_l = ctx.block_label(canon_idx);
let band_l = ctx.block_label(band_idx);
let fwd_l = ctx.block_label(fwd_idx);
let slow_l = ctx.block_label(slow_idx);
let true_l = ctx.block_label(true_idx);
let false_l = ctx.block_label(false_idx);
Expand All @@ -349,21 +329,12 @@ fn lower_strict_eq_inline_any(ctx: &mut FnCtx<'_>, l: &str, r: &str) -> String {
let same_ok = ctx.block().or(I1, &tagged, &not_nan);
ctx.block().cond_br(&same_ok, &true_l, &slow_l);

// Different bits: only a same-tag pair whose encoding is canonical, or a
// pair of unforwarded heap pointers, is decidable here.
// Different bits: only a same-tag pair whose encoding is canonical is
// decidable here. Pointer pairs need the runtime's allocation registries
// before either payload can safely be treated as a GC allocation.
ctx.current_block = diff_idx;
let l_tag = ctx.block().lshr(I64, &l_bits, "48");
let r_tag = ctx.block().lshr(I64, &r_bits, "48");
let l_ptr = ctx
.block()
.icmp_eq(I64, &l_tag, crate::nanbox::POINTER_TAG_TOP16_I64);
let r_ptr = ctx
.block()
.icmp_eq(I64, &r_tag, crate::nanbox::POINTER_TAG_TOP16_I64);
let both_ptr = ctx.block().and(I1, &l_ptr, &r_ptr);
ctx.block().cond_br(&both_ptr, &band_l, &canon_l);

ctx.current_block = canon_idx;
let l_sso = ctx
.block()
.icmp_eq(I64, &l_tag, crate::nanbox::SHORT_STRING_TAG_TOP16_I64);
Expand All @@ -381,32 +352,6 @@ fn lower_strict_eq_inline_any(ctx: &mut FnCtx<'_>, l: &str, r: &str) -> String {
let canonical = ctx.block().or(I1, &both_sso, &both_i32);
ctx.block().cond_br(&canonical, &false_l, &slow_l);

// Both POINTER_TAG. Classify by magnitude before touching a header.
ctx.current_block = band_idx;
let l_addr = ctx.block().and(I64, &l_bits, POINTER_MASK_I64);
let r_addr = ctx.block().and(I64, &r_bits, POINTER_MASK_I64);
let l_above = ctx.block().icmp_uge(I64, &l_addr, HANDLE_BAND_MAX_I64);
let l_below = ctx.block().icmp_ult(I64, &l_addr, HEAP_ADDR_CEILING_I64);
let r_above = ctx.block().icmp_uge(I64, &r_addr, HANDLE_BAND_MAX_I64);
let r_below = ctx.block().icmp_ult(I64, &r_addr, HEAP_ADDR_CEILING_I64);
let l_heap = ctx.block().and(I1, &l_above, &l_below);
let r_heap = ctx.block().and(I1, &r_above, &r_below);
let both_heap = ctx.block().and(I1, &l_heap, &r_heap);
ctx.block().cond_br(&both_heap, &fwd_l, &slow_l);

ctx.current_block = fwd_idx;
let l_flags_addr = ctx.block().sub(I64, &l_addr, "7");
let l_flags_ptr = ctx.block().inttoptr(I64, &l_flags_addr);
let l_flags = ctx.block().load(I8, &l_flags_ptr);
let r_flags_addr = ctx.block().sub(I64, &r_addr, "7");
let r_flags_ptr = ctx.block().inttoptr(I64, &r_flags_addr);
let r_flags = ctx.block().load(I8, &r_flags_ptr);
let either = ctx.block().or(I8, &l_flags, &r_flags);
// GC_FLAG_FORWARDED = 0x80; LLVM i8 literals are signed.
let fwd_bits = ctx.block().and(I8, &either, "-128");
let no_fwd = ctx.block().icmp_eq(I8, &fwd_bits, "0");
ctx.block().cond_br(&no_fwd, &false_l, &slow_l);

ctx.current_block = slow_idx;
let slow_res = ctx
.block()
Expand Down
105 changes: 17 additions & 88 deletions crates/perry-codegen/src/expr/i32_fast_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,89 +27,19 @@ use native_narrow::{
lower_expr_native_u8,
};

/// Returns true if `e` provably produces a finite double whose magnitude is
/// small enough (`|v| < 2^63`) for the unguarded `toint32_fast` lowering.
/// Used to skip the NaN/Inf/range guard in `toint32` for integer-arithmetic
/// hot paths — saving 5 instructions per bitwise op.
pub(crate) fn is_known_finite(ctx: &FnCtx<'_>, e: &Expr) -> bool {
known_finite_magnitude_bits(ctx, e).is_some_and(|bits| bits <= 62)
}

/// Conservative magnitude bound for `e`'s numeric value: `Some(b)` proves the
/// value is finite AND `|v| < 2^b`. `toint32_fast` is a bare
/// `fptosi f64 → i64` + `trunc` — exactly JS ToInt32 for every `|v| < 2^63`,
/// but LLVM *poison* at or beyond it. Finiteness alone is NOT enough:
/// `(1e20) | 0` and nested integer multiplies (`(a*a)*a | 0` with i32-range
/// `a`) are finite yet exceed 2^63, and pre-fix produced NaN instead of the
/// ToInt32-wrapped value (CodeRabbit review on #5466; the same hole shipped
/// on main). Composition keeps the proof airtight where the old boolean
/// recursion silently escalated: Add/Sub grow the bound by one bit, Mul sums
/// the operand bounds, and anything unprovable returns `None` so callers fall
/// back to the guarded `toint32` runtime helper.
fn known_finite_magnitude_bits(ctx: &FnCtx<'_>, e: &Expr) -> Option<u32> {
match e {
Expr::Integer(n) => Some(64 - n.unsigned_abs().leading_zeros()),
// Pod layout sizes/alignments/offsets are u32-class quantities.
Expr::PodLayoutSizeOf { .. }
| Expr::PodLayoutAlignOf { .. }
| Expr::PodLayoutOffsetOf { .. } => Some(32),
// Number literals can be NaN or ±Infinity (e.g., `Number(NaN)`,
// `Number(f64::INFINITY)`). Inspect the value: `fptosi NaN` is
// poison in LLVM and produced subnormal-double output (which
// downstream code interpreted as a NaN-boxed string with
// STRING_TAG bits, leading to garbled `console.log` output).
Expr::Number(n) => {
if !n.is_finite() {
return None;
}
let magnitude = n.abs();
if magnitude < 1.0 {
Some(0)
} else {
Some(magnitude.log2() as u32 + 1)
}
}
Expr::LocalGet(id) | Expr::Update { id, .. } => (ctx.integer_locals.contains(id)
|| ctx.unsigned_i32_locals.contains(id))
.then_some(32),
Expr::Uint8ArrayGet { .. } | Expr::BufferIndexGet { .. } => Some(8),
// In-bounds loads from an int-element typed array are integers in
// i32 range by construction (see `ta_int_elem_load_is_i32_provable`),
// as are i32-tier masked-window plain-array loads (the dense-i32
// range guard proved every window value is an i32 integer).
Expr::IndexGet { object, index }
if ta_int_elem_load_is_i32_provable(ctx, object, index)
|| super::masked_window::masked_window_i32_load_is_provable(ctx, object, index) =>
{
Some(32)
}
Expr::MathImul(_, _) => Some(32), // Math.imul returns i32 → always finite
Expr::Call { callee, .. } => {
matches!(callee.as_ref(), Expr::FuncRef(fid) if ctx.integer_returning_functions.contains(fid))
.then_some(32)
}
Expr::Binary { op, left, right } => match op {
BinaryOp::Add | BinaryOp::Sub => {
let l = known_finite_magnitude_bits(ctx, left)?;
let r = known_finite_magnitude_bits(ctx, right)?;
Some(l.max(r) + 1)
}
BinaryOp::Mul => {
let l = known_finite_magnitude_bits(ctx, left)?;
let r = known_finite_magnitude_bits(ctx, right)?;
Some(l + r)
}
// Bitwise results are already ToInt32/ToUint32-wrapped.
BinaryOp::BitAnd
| BinaryOp::BitOr
| BinaryOp::BitXor
| BinaryOp::Shl
| BinaryOp::Shr
| BinaryOp::UShr => Some(32),
_ => None,
},
_ => None,
}
/// Returns true if `e` is proven to fit a signed i32 at this program point.
///
/// `toint32_fast` currently emits `fptosi f64 -> i64` followed by `trunc`, but
/// LLVM's optimized output may combine that pair into `fptosi f64 -> i32`.
/// Its caller therefore needs an i32-range proof, not merely finiteness or an
/// i64-range magnitude bound. In particular, `integer_locals` proves only that
/// every write is integer-valued: a mutable local can hold the out-of-i32
/// result of a prior `*=` or `+=`. Treating that coarse fact as a 32-bit bound
/// made the final `c &= 0x7fffffff` in #7232 convert a ~1.5e18 double with
/// poison and print `0` instead of applying ECMAScript ToInt32 wrapping.
pub(crate) fn is_known_i32_range(ctx: &FnCtx<'_>, e: &Expr) -> bool {
super::range_facts::int_range_expr(ctx, e)
.is_some_and(|range| range.min >= i64::from(i32::MIN) && range.max <= i64::from(i32::MAX))
}

/// (Issue #50) If `IndexGet { object, index }` is a flat-const access
Expand Down Expand Up @@ -355,10 +285,9 @@ fn is_i32_chain_op(op: BinaryOp) -> bool {

/// Magnitude bound of `left <op> right` from the operands' bounds.
///
/// `Add`/`Sub` grow the bound by one bit and `Mul` sums them — the same
/// composition [`known_finite_magnitude_bits`] uses — but capped at 2^53
/// instead of 2^63, because this bound gates *exact integer arithmetic* rather
/// than a single `fptosi`.
/// `Add`/`Sub` grow the bound by one bit and `Mul` sums them, capped at 2^53
/// because this bound gates exact integer arithmetic rather than ToInt32
/// materialization.
///
/// The ToInt32/ToUint32-wrapped operators reset the bound to 32. Two of them
/// carry a tighter one, which is what keeps masked/shifted hash mixing on the
Expand Down Expand Up @@ -1438,7 +1367,7 @@ fn lower_expr_native_i32(ctx: &mut FnCtx<'_>, e: &Expr) -> Result<LoweredValue>
// Index/internal i32 materialization — packed-store RHS and
// numeric-index consumers prove their ranges upstream, so
// keep the lean guard here (see toint32 vs toint32_wrap).
if is_known_finite(ctx, e) {
if is_known_i32_range(ctx, e) {
Some(ctx.block().toint32_fast(&lowered.value))
} else {
Some(ctx.block().toint32(&lowered.value))
Expand Down
Loading
Loading