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
22 changes: 21 additions & 1 deletion crates/perry-codegen/src/expr/index_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use crate::type_analysis::{
is_numeric_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name,
};
#[allow(unused_imports)]
use crate::types::{DOUBLE, I1, I32, I64, I8, PTR};
use crate::types::{DOUBLE, I1, I16, I32, I64, I8, PTR};

use super::arrays_finds::lower_buffer_index_get_i32;
#[allow(unused_imports)]
Expand Down Expand Up @@ -378,6 +378,16 @@ fn lower_bounded_array_index_get(
let fwd_bits = blk.and(I8, &gc_flags, "128"); // GC_FLAG_FORWARDED
let is_fwd = blk.icmp_ne(I8, &fwd_bits, "0");
let needs_slow = blk.or(I1, &is_lazy, &is_fwd);
// Index accessors / custom attribute descriptors (`Object.defineProperty
// (arr, i, { get })`) divert element reads through the descriptor tables —
// the raw slot load below would bypass them (test262 sort/precise-*).
// GcHeader._reserved (u16 at -6) carries OBJ_FLAG_ARRAY_DESCRIPTORS=0x400.
let obj_flags_addr = blk.sub(I64, &arr_handle, "6");
let obj_flags_ptr = blk.inttoptr(I64, &obj_flags_addr);
let obj_flags = blk.load(I16, &obj_flags_ptr);
let desc_bits = blk.and(I16, &obj_flags, "1024");
let has_desc = blk.icmp_ne(I16, &desc_bits, "0");
let needs_slow = blk.or(I1, &needs_slow, &has_desc);

let lazy_idx = ctx.new_block("bidx.lazy");
let fast_idx = ctx.new_block("bidx.fast");
Expand Down Expand Up @@ -443,6 +453,16 @@ fn lower_legacy_array_index_get(
let fwd_bits = blk.and(I8, &gc_flags, "128"); // GC_FLAG_FORWARDED
let is_fwd = blk.icmp_ne(I8, &fwd_bits, "0");
let needs_slow = blk.or(I1, &is_lazy, &is_fwd);
// Index accessors / custom attribute descriptors (`Object.defineProperty
// (arr, i, { get })`) divert element reads through the descriptor tables —
// the raw slot load below would bypass them (test262 sort/precise-*).
// GcHeader._reserved (u16 at -6) carries OBJ_FLAG_ARRAY_DESCRIPTORS=0x400.
let obj_flags_addr = blk.sub(I64, &arr_handle, "6");
let obj_flags_ptr = blk.inttoptr(I64, &obj_flags_addr);
let obj_flags = blk.load(I16, &obj_flags_ptr);
let desc_bits = blk.and(I16, &obj_flags, "1024");
let has_desc = blk.icmp_ne(I16, &desc_bits, "0");
let needs_slow = blk.or(I1, &needs_slow, &has_desc);

let lazy_idx = ctx.new_block("arr.lazy");
let fast_idx = ctx.new_block("arr.fast");
Expand Down
5 changes: 4 additions & 1 deletion crates/perry-codegen/src/expr/instance_misc1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1192,7 +1192,10 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
let out_slot = blk.alloca(I64);
blk.store(I64, "0", &out_slot);
let arr_handle = unbox_to_i64(blk, &arr_box);
let start_i32 = blk.fptosi(DOUBLE, &start_d, I32);
// ToIntegerOrInfinity via the clamping helper: `fptosi` on
// ±Infinity/NaN is LLVM poison — `splice(Infinity, 3)` deleted
// from index 0 (test262 splice/S15.4.4.12_A2.1_T3).
let start_i32 = blk.call(I32, "js_array_splice_delete_count", &[(DOUBLE, &start_d)]);
let count_i32 = blk.call(I32, "js_array_splice_delete_count", &[(DOUBLE, &count_d)]);

let (items_ptr, items_count_str) = if item_vals.is_empty() {
Expand Down
36 changes: 36 additions & 0 deletions crates/perry-codegen/src/expr/logical_collections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,42 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
],
)
}
// sort(comparator?): validated + run by the runtime engine.
"sort" => {
let cmp = nth(0).unwrap_or_else(undef);
blk.call(
DOUBLE,
"js_arraylike_sort",
&[(DOUBLE, &recv_box), (DOUBLE, &cmp)],
)
}
// splice(...) / concat(...): variadic — pass an alloca buffer
// of raw NaN-boxed doubles + count (mirrors the dense
// `js_array_concat_variadic` lowering).
"splice" | "concat" => {
let n = arg_boxes.len();
let (buf_reg, count_str) = if n == 0 {
("null".to_string(), "0".to_string())
} else {
let buf_reg = blk.next_reg();
blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n));
for (i, val) in arg_boxes.iter().enumerate() {
let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]);
blk.store(DOUBLE, val, &slot);
}
(buf_reg, format!("{}", n))
};
let fname = if method == "splice" {
"js_arraylike_splice"
} else {
"js_arraylike_concat"
};
blk.call(
DOUBLE,
fname,
&[(DOUBLE, &recv_box), (PTR, &buf_reg), (I32, &count_str)],
)
}
other => bail!("unsupported generic array-like method '{other}'"),
};
Ok(result)
Expand Down
107 changes: 106 additions & 1 deletion crates/perry-codegen/src/lower_array_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@ pub(crate) fn lower_array_method(
Ok(nanbox_string_inline(blk, &result_handle))
}
"some" | "every" => {
// An explicit `thisArg` (2nd argument) must bind the callback's
// `this`; the dense helpers don't take one (they bind undefined),
// so route through the generic array-like engine, which does
// (real-array receivers keep a fast element path there).
if args.len() >= 2 {
let cb_box = lower_expr(ctx, &args[0])?;
let this_box = lower_expr(ctx, &args[1])?;
let blk = ctx.block();
let gen_fn = if property == "some" {
"js_arraylike_some"
} else {
"js_arraylike_every"
};
return Ok(blk.call(
DOUBLE,
gen_fn,
&[(DOUBLE, &recv_box), (DOUBLE, &cb_box), (DOUBLE, &this_box)],
));
}

// `arr.some()` / `arr.every()` with no callback must throw a
// runtime TypeError ("undefined is not a function"), not fail to
// compile — pad a missing callback with `undefined` and let
Expand Down Expand Up @@ -251,6 +271,22 @@ pub(crate) fn lower_array_method(
// as HIR variants but may reach here as generic MethodCall when
// the HIR lowering doesn't recognize the pattern.
"find" => {
// An explicit `thisArg` (2nd argument) must bind the callback's
// `this`; the dense helpers don't take one (they bind undefined),
// so route through the generic array-like engine, which does
// (real-array receivers keep a fast element path there).
if args.len() >= 2 {
let cb_box = lower_expr(ctx, &args[0])?;
let this_box = lower_expr(ctx, &args[1])?;
let blk = ctx.block();
let gen_fn = "js_arraylike_find";
return Ok(blk.call(
DOUBLE,
gen_fn,
&[(DOUBLE, &recv_box), (DOUBLE, &cb_box), (DOUBLE, &this_box)],
));
}

// 0-arg → runtime TypeError (pad undefined), not compile-fail.
let cb_box = if let Some(arg) = args.first() {
lower_expr(ctx, arg)?
Expand All @@ -268,6 +304,22 @@ pub(crate) fn lower_array_method(
))
}
"findIndex" => {
// An explicit `thisArg` (2nd argument) must bind the callback's
// `this`; the dense helpers don't take one (they bind undefined),
// so route through the generic array-like engine, which does
// (real-array receivers keep a fast element path there).
if args.len() >= 2 {
let cb_box = lower_expr(ctx, &args[0])?;
let this_box = lower_expr(ctx, &args[1])?;
let blk = ctx.block();
let gen_fn = "js_arraylike_findIndex";
return Ok(blk.call(
DOUBLE,
gen_fn,
&[(DOUBLE, &recv_box), (DOUBLE, &cb_box), (DOUBLE, &this_box)],
));
}

// 0-arg → runtime TypeError (pad undefined), not compile-fail.
let cb_box = if let Some(arg) = args.first() {
lower_expr(ctx, arg)?
Expand Down Expand Up @@ -393,6 +445,22 @@ pub(crate) fn lower_array_method(
))
}
"map" => {
// An explicit `thisArg` (2nd argument) must bind the callback's
// `this`; the dense helpers don't take one (they bind undefined),
// so route through the generic array-like engine, which does
// (real-array receivers keep a fast element path there).
if args.len() >= 2 {
let cb_box = lower_expr(ctx, &args[0])?;
let this_box = lower_expr(ctx, &args[1])?;
let blk = ctx.block();
let gen_fn = "js_arraylike_map";
return Ok(blk.call(
DOUBLE,
gen_fn,
&[(DOUBLE, &recv_box), (DOUBLE, &cb_box), (DOUBLE, &this_box)],
));
}

// 0-arg → runtime TypeError (pad undefined), not compile-fail.
let cb_box = if let Some(arg) = args.first() {
lower_expr(ctx, arg)?
Expand All @@ -417,6 +485,22 @@ pub(crate) fn lower_array_method(
Ok(nanbox_pointer_inline(blk, &result))
}
"filter" => {
// An explicit `thisArg` (2nd argument) must bind the callback's
// `this`; the dense helpers don't take one (they bind undefined),
// so route through the generic array-like engine, which does
// (real-array receivers keep a fast element path there).
if args.len() >= 2 {
let cb_box = lower_expr(ctx, &args[0])?;
let this_box = lower_expr(ctx, &args[1])?;
let blk = ctx.block();
let gen_fn = "js_arraylike_filter";
return Ok(blk.call(
DOUBLE,
gen_fn,
&[(DOUBLE, &recv_box), (DOUBLE, &cb_box), (DOUBLE, &this_box)],
));
}

// 0-arg → runtime TypeError (pad undefined), not compile-fail.
let cb_box = if let Some(arg) = args.first() {
lower_expr(ctx, arg)?
Expand All @@ -435,6 +519,22 @@ pub(crate) fn lower_array_method(
Ok(nanbox_pointer_inline(blk, &result))
}
"forEach" => {
// An explicit `thisArg` (2nd argument) must bind the callback's
// `this`; the dense helpers don't take one (they bind undefined),
// so route through the generic array-like engine, which does
// (real-array receivers keep a fast element path there).
if args.len() >= 2 {
let cb_box = lower_expr(ctx, &args[0])?;
let this_box = lower_expr(ctx, &args[1])?;
let blk = ctx.block();
let gen_fn = "js_arraylike_forEach";
return Ok(blk.call(
DOUBLE,
gen_fn,
&[(DOUBLE, &recv_box), (DOUBLE, &cb_box), (DOUBLE, &this_box)],
));
}

// 0-arg → runtime TypeError (pad undefined), not compile-fail.
let cb_box = if let Some(arg) = args.first() {
lower_expr(ctx, arg)?
Expand Down Expand Up @@ -759,7 +859,12 @@ pub(crate) fn lower_array_method(
let out_slot = blk.alloca(I64);
blk.store(I64, "0", &out_slot);
let recv_handle = unbox_to_i64(blk, &recv_box);
let start_i32 = blk.fptosi(DOUBLE, &start_d, I32);
// ToIntegerOrInfinity via the clamping helper: `fptosi` on
// ±Infinity/NaN is LLVM poison — `splice(Infinity, 3)` deleted
// from index 0 (test262 splice/S15.4.4.12_A2.1_T3). The helper
// clamps +Inf → i32::MAX (→ len downstream), -Inf → i32::MIN
// (→ 0 after relative-index resolution), NaN → 0.
let start_i32 = blk.call(I32, "js_array_splice_delete_count", &[(DOUBLE, &start_d)]);
let count_i32 = blk.call(I32, "js_array_splice_delete_count", &[(DOUBLE, &count_d)]);
let (items_ptr, items_count_str) = if item_vals.is_empty() {
("null".to_string(), "0".to_string())
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-codegen/src/runtime_decls/arrays.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ pub fn declare_phase_b_arrays(module: &mut LlModule) {
DOUBLE,
&[DOUBLE, DOUBLE, I32, DOUBLE, I32],
);
module.declare_function("js_arraylike_sort", DOUBLE, &[DOUBLE, DOUBLE]);
module.declare_function("js_arraylike_splice", DOUBLE, &[DOUBLE, PTR, I32]);
module.declare_function("js_arraylike_concat", DOUBLE, &[DOUBLE, PTR, I32]);
// Spread `[...x]` — strict GetIterator/materialization.
module.declare_function("js_array_clone_for_spread", I64, &[DOUBLE]);
module.declare_function("js_array_spread_append", I64, &[I64, DOUBLE]);
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-hir/src/lower/expr_call/intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1789,6 +1789,14 @@ fn try_arraylike_receiver_method(
| "slice"
| "at"
| "join"
// Generic mutators with dedicated runtime engines (#4597
// extension): `sort` sorts the receiver in place via
// Get/HasProperty/Set/Delete; `splice`/`concat` apply the spec
// algorithms over the array-like (test262 sort/call-with-primitive,
// splice/set_length_no_args, concat/call-with-boolean).
| "sort"
| "splice"
| "concat"
);
if generic {
// Receiver lowers before the positional args, matching source order.
Expand Down
33 changes: 26 additions & 7 deletions crates/perry-hir/src/lower_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,14 +278,33 @@ pub(crate) fn infer_type_from_expr(expr: &ast::Expr, ctx: &LoweringContext) -> T
// Template literals are always strings
ast::Expr::Tpl(_) => Type::String,

// Array literals → infer element type from first element
// Array literals → unified element type across ALL elements. Using just
// the first element claimed `Array(Number)` for a mixed literal like
// `[1, true, "x"]`, and codegen trusted that lie: `a[i] === b[j]`
// lowered to a raw `fcmp` where NaN-boxed booleans/strings/undefined
// are unordered → strict equality between two mixed-array loads was
// always false (test262 sort/S15.4.4.11_A2.1_T3 et al). Divergent
// element types now infer `Array(Any)` so the comparison (and every
// other consumer) takes the tag-aware path. A spread element's
// contribution is unknown statically → Any.
ast::Expr::Array(arr) => {
let elem_ty = arr
.elems
.iter()
.find_map(|e| e.as_ref().map(|elem| infer_type_from_expr(&elem.expr, ctx)))
.unwrap_or(Type::Any);
Type::Array(Box::new(elem_ty))
let mut unified: Option<Type> = None;
for e in arr.elems.iter().flatten() {
let t = if e.spread.is_some() {
Type::Any
} else {
infer_type_from_expr(&e.expr, ctx)
};
match &unified {
None => unified = Some(t),
Some(u) if *u == t => {}
Some(_) => {
unified = Some(Type::Any);
break;
}
}
}
Type::Array(Box::new(unified.unwrap_or(Type::Any)))
}

// Variable reference → look up known type
Expand Down
Loading
Loading