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
6 changes: 5 additions & 1 deletion crates/perry-codegen/src/expr/instance_misc1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,16 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
} => {
let obj = lower_expr(ctx, object)?;
let (key_box, key_raw) = emit_with_key(ctx, property);
// HasBinding probe AFTER the RHS evaluates — matches V8/node
// (`with (o) { var x = delete o.x; }` writes the hoisted var,
// not o.x — test262 variable/binding-resolution.js judges
// against node's order, not the spec's resolve-reference-first).
let value_reg = lower_expr(ctx, value)?;
let had = ctx.block().call(
I32,
"js_with_has_binding",
&[(DOUBLE, &obj), (I64, &key_raw)],
);
let value_reg = lower_expr(ctx, value)?;
let had_bool = ctx.block().icmp_ne(I32, &had, "0");

let hit_idx = ctx.new_block("with.set.hit");
Expand Down
12 changes: 12 additions & 0 deletions crates/perry-codegen/src/runtime_decls/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,8 @@ pub fn declare_phase_b_strings(module: &mut LlModule) {
// RegExp.escape(str) — #2899. Takes/returns NaN-boxed f64 (string).
module.declare_function("js_regexp_escape", DOUBLE, &[DOUBLE]);
module.declare_function("js_get_string_pointer_unified", I64, &[DOUBLE]);
// Strict-equality (`===`) compare for switch case dispatch.
module.declare_function("js_switch_strict_equals", I32, &[DOUBLE, DOUBLE]);
module.declare_function("js_value_to_str_ptr_for_ffi", I64, &[DOUBLE]);
// Closes #580: alias-on-copy refcount bump for string locals. The
// call site at `crates/perry-codegen/src/stmt.rs:725` was added by
Expand Down Expand Up @@ -1177,6 +1179,11 @@ pub fn declare_phase_b_strings(module: &mut LlModule) {
module.declare_function("js_throw_symbol_constructor_type_error", DOUBLE, &[]);
module.declare_function("js_throw_bigint_constructor_type_error", DOUBLE, &[]);
module.declare_function("js_throw_strict_eval_arguments_syntax_error", DOUBLE, &[]);
module.declare_function(
"js_throw_restricted_function_property_assignment",
DOUBLE,
&[],
);
module.declare_function("js_throw_math_constructor_type_error", DOUBLE, &[]);
module.declare_function("js_webcrypto_illegal_constructor", DOUBLE, &[]);
module.declare_function("js_throw_type_error_const_assignment", DOUBLE, &[DOUBLE]);
Expand All @@ -1186,6 +1193,11 @@ pub fn declare_phase_b_strings(module: &mut LlModule) {
&[DOUBLE],
);
module.declare_function("js_throw_reference_error_unresolved_get", DOUBLE, &[]);
// with-statement implicit-global sentinel (HOLE) helpers.
module.declare_function("js_with_implicit_unset", DOUBLE, &[]);
module.declare_function("js_with_implicit_read", DOUBLE, &[DOUBLE, DOUBLE]);
// Iterator-protocol result validation (for-of lazy loop).
module.declare_function("js_iterator_result_validate", DOUBLE, &[DOUBLE]);
module.declare_function("js_throw_reference_error_this_before_super", DOUBLE, &[]);
module.declare_function("js_throw_reference_error_super_delete", DOUBLE, &[]);
module.declare_function(
Expand Down
16 changes: 14 additions & 2 deletions crates/perry-codegen/src/stmt/loops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,20 @@ pub(crate) fn lower_for(
// Saves ~25-30% on `for (let i = 0; i < arr.length; i++) arr[i] = i`
// and `for (let i = 0; i < arr.length; i++) for (let j = 0; j <
// arr.length; j++) ...` patterns.
let hoist_classification: Option<(u32, u32, perry_hir::CompareOp)> =
condition.and_then(|cond| classify_for_length_hoist(cond, body));
let hoist_classification: Option<(u32, u32, perry_hir::CompareOp)> = condition
.and_then(|cond| classify_for_length_hoist(cond, body))
// `__arr_N` is the for-of desugar's holder — an ALIAS of the user's
// iterable local. Body mutations go through the user's name
// (`array.push(1)` → ArrayPush on the user id), so the walker above
// can't see them against the holder id. Spec ForOf reads the live
// length every step (array-expand/contract in test262), so never
// hoist for desugared for-of loops; user-written `i < arr.length`
// loops keep the peephole.
.filter(|(arr_id, _, _)| {
!ctx.local_id_to_name
.get(arr_id)
.is_some_and(|n| n.starts_with("__arr_"))
});
let hoisted_length_arr_id: Option<u32> = hoist_classification.map(|(arr, _, _)| arr);
let hoisted_index_bounds_are_safe = hoist_classification.is_some_and(|(_, counter_id, op)| {
matches!(op, perry_hir::CompareOp::Lt)
Expand Down
107 changes: 46 additions & 61 deletions crates/perry-codegen/src/stmt/switch_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,91 +36,76 @@ pub(crate) fn lower_switch(
let exit_idx = ctx.new_block("switch.exit");
let exit_label = ctx.block_label(exit_idx);

// Branch from the discriminant block into the first test (or
// straight into the body if there are zero cases — degenerate but
// legal).
if let Some(&first_test) = test_blocks.first() {
let first_test_label = ctx.block_label(first_test);
ctx.block().br(&first_test_label);
} else {
if cases.is_empty() {
ctx.block().br(&exit_label);
ctx.current_block = exit_idx;
return Ok(());
}

// Find the default case index, if any. The "fall-through to default
// when nothing matches" target is the default's body block; if
// there's no default, we fall through to exit.
// Find the default case index, if any. The "no case matched" target
// is the default's body block; if there's no default, it is exit.
let default_idx = cases.iter().position(|c| c.test.is_none());
let no_match_target_label = match default_idx {
Some(i) => ctx.block_label(body_blocks[i]),
None => exit_label.clone(),
};

// Branch from the discriminant block into the first *case* test.
// A leading `default:` is skipped — its body only runs when no case
// test anywhere in the block matches.
let first_case_test = cases.iter().position(|c| c.test.is_some());
match first_case_test {
Some(i) => {
let first_test_label = ctx.block_label(test_blocks[i]);
ctx.block().br(&first_test_label);
}
None => {
// Only a default clause: run it unconditionally.
ctx.block().br(&no_match_target_label);
}
}

// Push break target. Switch has no continue, so we use exit for both.
ctx.loop_targets
.push((exit_label.clone(), exit_label.clone(), ctx.try_depth));

// Compile each test block. Each test compares dv against the case
// expression with fcmp oeq, jumps to the body on match, otherwise
// jumps to the next test (or to no_match_target if this is the last).
// jumps to the next *case* test (or to no_match_target if this is the
// last). The default clause is NOT part of the test chain: per spec
// CaseBlockEvaluation, every case test — including ones written
// *after* `default:` — is tried first, and the default body only
// runs when no case matched. A non-match at the default's source
// position must therefore skip over it to the next case test.
for (i, case) in cases.iter().enumerate() {
ctx.current_block = test_blocks[i];
let body_label = ctx.block_label(body_blocks[i]);
let next_label = if i + 1 < test_blocks.len() {
ctx.block_label(test_blocks[i + 1])
} else {
no_match_target_label.clone()
let next_case_test = ((i + 1)..cases.len()).find(|&j| cases[j].test.is_some());
let next_label = match next_case_test {
Some(j) => ctx.block_label(test_blocks[j]),
None => no_match_target_label.clone(),
};

if let Some(test_expr) = case.test.as_ref() {
let cv = lower_expr(ctx, test_expr)?;
// If either the discriminant or the case value is a static
// string expression (e.g. `switch (typeof x) { case "foo": }`),
// compare by string content via js_string_equals. Two allocations
// of the same text have different pointers, so icmp on bits
// would report them unequal. Dispatch through the unified
// string-pointer getter which returns null for non-strings —
// js_string_equals treats null as "not equal", matching the
// expected fall-through behavior.
let either_string = crate::type_analysis::is_string_expr(ctx, discriminant)
|| crate::type_analysis::is_string_expr(ctx, test_expr);
if either_string {
let blk = ctx.block();
let l_handle = blk.call(
crate::types::I64,
"js_get_string_pointer_unified",
&[(crate::types::DOUBLE, &dv)],
);
let r_handle = blk.call(
crate::types::I64,
"js_get_string_pointer_unified",
&[(crate::types::DOUBLE, &cv)],
);
let i32_eq = blk.call(
crate::types::I32,
"js_string_equals",
&[
(crate::types::I64, &l_handle),
(crate::types::I64, &r_handle),
],
);
let cmp = blk.icmp_ne(crate::types::I32, &i32_eq, "0");
blk.cond_br(&cmp, &body_label, &next_label);
} else {
// fcmp on NaN-tagged string/pointer values is always
// false (NaN comparisons are unordered). For switch on
// strings or any value that might be NaN-tagged, compare
// the i64 bit patterns instead. This works for numbers
// too — equal doubles have equal bits except for ±0
// which the JS spec treats as equal anyway and Number(0)
// === Number(-0) is true.
let blk = ctx.block();
let dv_bits = blk.bitcast_double_to_i64(&dv);
let cv_bits = blk.bitcast_double_to_i64(&cv);
let cmp = blk.icmp_eq(crate::types::I64, &dv_bits, &cv_bits);
blk.cond_br(&cmp, &body_label, &next_label);
}
// CaseClauseIsSelected is strict equality (`===`). One runtime
// helper covers every value-kind correctly: string content
// compare (heap + SSO), IEEE numeric compare (NaN never
// matches, -0 == +0, int32-boxed == raw double), and bit
// identity for objects/null/undefined/booleans. The previous
// two-path lowering (js_get_string_pointer_unified +
// js_string_equals, raw bit compare otherwise) made
// `switch (1)` match `case '1'` through the unified getter's
// number→string property-key coercion (S12.11_A1_T2) and
// `switch (NaN)` match `case NaN` through bit equality.
let blk = ctx.block();
let i32_eq = blk.call(
crate::types::I32,
"js_switch_strict_equals",
&[(crate::types::DOUBLE, &dv), (crate::types::DOUBLE, &cv)],
);
let cmp = blk.icmp_ne(crate::types::I32, &i32_eq, "0");
blk.cond_br(&cmp, &body_label, &next_label);
} else {
// Default case test block: unconditional jump to its body.
ctx.block().br(&body_label);
Expand Down
114 changes: 76 additions & 38 deletions crates/perry-codegen/src/stmt/try_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,50 @@ use super::*;
/// js_throw so the throw propagates instead of being swallowed.
/// 6. finally runs (if present), then falls through to merge (only the
/// normal-completion path reaches this merge finally)
/// Emit `js_try_push()` + setjmp in the CURRENT block, branching to
/// `exc_label` on a longjmp (exception) and `normal_label` otherwise.
///
/// CRITICAL: setjmp must carry `returns_twice` on the call site too (not
/// just the declaration). Without it, LLVM -O2 promotes alloca-backed
/// locals to SSA registers and the longjmp return path sees stale
/// pre-setjmp values. The standard `blk.call()` doesn't support call
/// attributes, so the instruction is emitted manually.
///
/// setjmp variant selection — must match the declaration in
/// `runtime_decls.rs`:
/// - Apple: `_setjmp` (LLVM-IR name) → linker `__setjmp` = fast variant
/// (skips the sigprocmask / sigaltstack syscalls, ~500 ns each on
/// macOS arm64).
/// - Linux: `setjmp` is already fast — no swap needed.
/// - Windows: `_setjmp(buf, frame_ptr)` (different ABI).
fn emit_setjmp_dispatch(ctx: &mut FnCtx<'_>, exc_label: &str, normal_label: &str) {
use crate::types::{I32, PTR};
let blk = ctx.block();
let jmpbuf = blk.call(PTR, "js_try_push", &[]);
let sjr_reg = blk.next_reg();
if cfg!(target_os = "windows") {
blk.emit_raw(format!(
"{} = call i32 @_setjmp(ptr {}, ptr null) #0",
sjr_reg, jmpbuf
));
} else if cfg!(target_vendor = "apple") {
blk.emit_raw(format!(
"{} = call i32 @_setjmp(ptr {}) #0",
sjr_reg, jmpbuf
));
} else {
blk.emit_raw(format!("{} = call i32 @setjmp(ptr {}) #0", sjr_reg, jmpbuf));
}
let is_exc = blk.icmp_ne(I32, &sjr_reg, "0");
blk.cond_br(&is_exc, exc_label, normal_label);
}

pub(crate) fn lower_try(
ctx: &mut FnCtx<'_>,
body: &[perry_hir::Stmt],
catch: Option<&perry_hir::CatchClause>,
finally: Option<&[perry_hir::Stmt]>,
) -> Result<()> {
use crate::types::{I32, PTR};

// Mark the enclosing function so IR emission adds `#1`
// (noinline optnone). At -O2 on aarch64, LLVM's mem2reg/SROA will
// otherwise promote allocas to SSA registers across the setjmp
Expand All @@ -42,39 +78,7 @@ pub(crate) fn lower_try(
let finally_label = ctx.block_label(finally_idx);

// --- current block: setjmp dispatch ---
let blk = ctx.block();
let jmpbuf = blk.call(PTR, "js_try_push", &[]);
// CRITICAL: setjmp must carry `returns_twice` on the call site
// too (not just the declaration). Without it, LLVM -O2 promotes
// alloca-backed locals to SSA registers and the longjmp return
// path sees stale pre-setjmp values instead of the try-body's
// assignments. The standard `blk.call()` doesn't support call
// attributes, so we emit the instruction manually.
let sjr_reg = blk.next_reg();
// setjmp variant selection — must match the declaration in
// `runtime_decls.rs`. See that file for the rationale; the short
// version:
// - Apple: `_setjmp` (LLVM-IR name) → linker `__setjmp` = fast
// variant (skips the sigprocmask / sigaltstack syscalls that
// normally cost ~500 ns each on macOS arm64).
// - Linux: `setjmp` is already fast — no swap needed.
// - Windows: `_setjmp(buf, frame_ptr)` (different ABI).
if cfg!(target_os = "windows") {
blk.emit_raw(format!(
"{} = call i32 @_setjmp(ptr {}, ptr null) #0",
sjr_reg, jmpbuf
));
} else if cfg!(target_vendor = "apple") {
blk.emit_raw(format!(
"{} = call i32 @_setjmp(ptr {}) #0",
sjr_reg, jmpbuf
));
} else {
blk.emit_raw(format!("{} = call i32 @setjmp(ptr {}) #0", sjr_reg, jmpbuf));
}
let sjr = sjr_reg;
let is_exc = blk.icmp_ne(I32, &sjr, "0");
blk.cond_br(&is_exc, &catch_label, &try_body_label);
emit_setjmp_dispatch(ctx, &catch_label, &try_body_label);

// --- try body ---
ctx.current_block = try_body_idx;
Expand Down Expand Up @@ -105,9 +109,43 @@ pub(crate) fn lower_try(
ctx.locals.insert(*id, slot.clone());
ctx.block().store(DOUBLE, &exc, &slot);
}
lower_stmts(ctx, &clause.body)?;
if !ctx.block().is_terminated() {
ctx.block().br(&finally_label);
if let Some(f) = finally {
// Per spec TryStatement : try Block Catch Finally — a throw
// escaping the CATCH body must still run the finally, whose
// own abrupt completion (throw) replaces the pending one.
// Protect the catch body with its own frame: on a longjmp out
// of it, run a dedicated copy of the finally body, then
// re-raise the catch's exception (unless the finally itself
// terminated abruptly — its terminator stands).
// Refs test262 S12.14_A7_T2/T3, S12.14_A13_T3.
let cbody_idx = ctx.new_block("try.catch.body");
let cfail_idx = ctx.new_block("try.catch.fail");
let cbody_label = ctx.block_label(cbody_idx);
let cfail_label = ctx.block_label(cfail_idx);
emit_setjmp_dispatch(ctx, &cfail_label, &cbody_label);

ctx.current_block = cbody_idx;
ctx.try_depth += 1;
lower_stmts(ctx, &clause.body)?;
ctx.try_depth -= 1;
if !ctx.block().is_terminated() {
ctx.block().call_void("js_try_end", &[]);
ctx.block().br(&finally_label);
}

ctx.current_block = cfail_idx;
ctx.block().call_void("js_try_end", &[]);
let exc2 = ctx.block().call(DOUBLE, "js_get_exception", &[]);
lower_stmts(ctx, f)?;
if !ctx.block().is_terminated() {
ctx.block().call_void("js_throw", &[(DOUBLE, &exc2)]);
ctx.block().unreachable();
}
} else {
lower_stmts(ctx, &clause.body)?;
if !ctx.block().is_terminated() {
ctx.block().br(&finally_label);
}
}
} else {
// No catch clause: this is a `try { ... } finally { ... }`
Expand Down
1 change: 1 addition & 0 deletions crates/perry-hir/src/destructuring/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ mod assignment_stmt;
mod helpers;
mod pattern_binding;
mod var_decl;
mod var_decl_sources;

pub(crate) use assignment_expr::lower_destructuring_assignment;
pub(crate) use assignment_stmt::{
Expand Down
Loading