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
23 changes: 16 additions & 7 deletions crates/perry-codegen/src/expr/array_push.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ fn emit_array_handle_length(
}
let blk = ctx.block();
let len_i32 = blk.call(I32, "js_array_length", &[(I64, array_handle)]);
blk.sitofp(I32, &len_i32, DOUBLE)
blk.uitofp(I32, &len_i32, DOUBLE)
}

fn emit_array_box_length(ctx: &mut FnCtx<'_>, array_box: &str, value_discarded: bool) -> String {
Expand Down Expand Up @@ -489,7 +489,7 @@ fn lower_array_push_spec_order(
let recv_handle = unbox_to_i64(blk, &recv_box);
let new_handle = blk.call(
I64,
"js_array_push_f64",
"js_array_push_f64_spec",
&[(I64, &recv_handle), (DOUBLE, &v)],
);
let new_box = nanbox_pointer_inline(blk, &new_handle);
Expand Down Expand Up @@ -725,7 +725,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) ->
let arr_handle = unbox_to_i64(blk, &arr_box);
let new_handle = blk.call(
I64,
"js_array_push_f64",
"js_array_push_f64_spec",
&[(I64, &arr_handle), (DOUBLE, &v)],
);
let new_box = nanbox_pointer_inline(blk, &new_handle);
Expand All @@ -741,7 +741,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) ->
ctx.record_lowered_value_with_access_mode_and_facts(
"NumericArrayPush",
Some(*array_id),
"js_array_push_f64",
"js_array_push_f64_spec",
&fallback,
Some(BoundsState::Unknown),
None,
Expand Down Expand Up @@ -872,7 +872,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) ->
let blk = ctx.block();
let new_handle = blk.call(
I64,
"js_array_push_f64",
"js_array_push_f64_spec",
&[(I64, &arr_handle), (DOUBLE, &v)],
);
let new_box = nanbox_pointer_inline(blk, &new_handle);
Expand Down Expand Up @@ -960,6 +960,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) ->
let integrity_bits = blk.and(I16, &obj_flags, "1031");
blk.icmp_eq(I16, &integrity_bits, "0")
};
// A sticky runtime byte records indexed properties on
// Array/Object.prototype (and custom Array prototypes).
// Such a property can intercept push with an inherited
// setter, so the raw append is valid only while the default
// prototype chain remains pristine.
let invalidated =
blk.load_volatile(I8, "@PERRY_ARRAY_INDEX_FAST_PATH_INVALIDATED");
let prototype_clean = blk.icmp_eq(I8, &invalidated, "0");
let clean = blk.and(I1, &clean, &prototype_clean);
let length = blk.safe_load_i32_from_ptr(&arr_handle);
let cap_addr = blk.add(I64, &arr_handle, "4");
let cap_ptr = blk.inttoptr(I64, &cap_addr);
Expand Down Expand Up @@ -1093,7 +1102,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) ->
let blk = ctx.block();
let new_handle = blk.call(
I64,
"js_array_push_f64",
"js_array_push_f64_spec",
&[(I64, &arr_handle), (DOUBLE, &v)],
);
let new_box = nanbox_pointer_inline(blk, &new_handle);
Expand All @@ -1114,7 +1123,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) ->
let arr_handle = unbox_to_i64(blk, &arr_box);
let new_handle = blk.call(
I64,
"js_array_push_f64",
"js_array_push_f64_spec",
&[(I64, &arr_handle), (DOUBLE, &v)],
);
let new_box = nanbox_pointer_inline(blk, &new_handle);
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/expr/array_push_guard_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ fn a_metadata_selected_add_keeps_the_runtime_number_guard() {
assert!(
generic.contains("call i32 @js_typed_feedback_numeric_array_push_guard")
&& generic.contains("call i64 @js_array_numeric_push_f64_unboxed")
&& generic.contains("call i64 @js_array_push_f64"),
&& generic.contains("call i64 @js_array_push_f64_spec"),
"a declared-number addition must validate the live value and retain the generic push fallback:\n{generic}"
);
assert!(
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/expr/arrays_finds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1251,7 +1251,7 @@ pub(crate) fn lower(
crate::lower_array_method::emit_grow_mutator_writeback(ctx, *array_id, &new_box)?;
let blk = ctx.block();
let len_i32 = blk.call(I32, "js_array_length", &[(I64, &new_handle)]);
let len_f64 = blk.sitofp(I32, &len_i32, DOUBLE);
let len_f64 = blk.uitofp(I32, &len_i32, DOUBLE);
Ok(len_f64)
}

Expand Down
9 changes: 9 additions & 0 deletions crates/perry-codegen/src/expr/logical_collections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
&[(DOUBLE, &recv_box), (DOUBLE, &sep)],
)
}
// flat(depth?): undefined selects the default depth 1.
"flat" => {
let depth = nth(0).unwrap_or_else(undef);
blk.call(
DOUBLE,
"js_arraylike_flat",
&[(DOUBLE, &recv_box), (DOUBLE, &depth)],
)
}
// slice(start?, end?): has-flags distinguish omitted from undefined.
"slice" => {
let (has_start, start) = match nth(0) {
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/lower_array_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -933,7 +933,7 @@ pub(crate) fn lower_array_method(
let len_i32 = ctx
.block()
.call(I32, "js_array_length", &[(I64, &new_handle)]);
Ok(ctx.block().sitofp(I32, &len_i32, DOUBLE))
Ok(ctx.block().uitofp(I32, &len_i32, DOUBLE))
}
// Issue #655 (chained-receiver path): without this arm, a
// chained `obj.field.splice(...)` resolved through `is_array_expr`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@
}
let blk = ctx.block();
let len_i32 = blk.call(I32, "js_array_length", &[(I64, &new_handle)]);
return Ok(blk.sitofp(I32, &len_i32, DOUBLE));
return Ok(blk.uitofp(I32, &len_i32, DOUBLE));
}

if module == "array" && (method == "push_single" || method == "push") {
Expand Down Expand Up @@ -376,7 +376,7 @@
}
let blk = ctx.block();
let len_i32 = blk.call(I32, "js_array_length", &[(I64, &new_handle)]);
return Ok(blk.sitofp(I32, &len_i32, DOUBLE));
return Ok(blk.uitofp(I32, &len_i32, DOUBLE));
}

if module == "array" && (method == "pop_back" || method == "pop") {
Expand Down
7 changes: 7 additions & 0 deletions crates/perry-codegen/src/native_value/verify/raw_f64.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,14 @@ pub(crate) fn validate_js_value_bits_record(record: &NativeRepRecord, errors: &m
pub(crate) fn raw_f64_dynamic_fallback_record(record: &NativeRepRecord) -> bool {
matches!(
(record.expr_kind.as_str(), record.consumer.as_str()),
// #8858 added the spec-compliant observable push helper alongside the
// original own-property one; BOTH are still emitted (the original at
// expr/static_method.rs, expr/static_field_meta.rs and
// rooting/temp_root.rs), so both must keep the raw_f64_layout
// requirement. Replacing the entry rather than adding to it silently
// dropped it for `js_array_push_f64`.
("NumericArrayPush", "js_array_push_f64")
| ("NumericArrayPush", "js_array_push_f64_spec")
| (
"NumericArrayIndexGet",
"js_typed_feedback_array_index_get_fallback_boxed"
Expand Down
4 changes: 3 additions & 1 deletion crates/perry-codegen/src/runtime_decls/arrays.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use super::*;
/// - `js_array_push_f64(arr, value) -> arr*` — push element, may realloc
/// and return a NEW pointer that the caller must use going forward
/// - `js_array_get_f64(arr, index) -> f64` — read typed-number element
/// - `js_array_length(arr) -> u32` — length (u32, sitofp'd to double for
/// - `js_array_length(arr) -> u32` — length (u32, uitofp'd to double for
/// our number ABI)
pub fn declare_phase_b_arrays(module: &mut LlModule) {
module.declare_function("js_array_alloc", I64, &[I32]);
Expand Down Expand Up @@ -42,6 +42,7 @@ pub fn declare_phase_b_arrays(module: &mut LlModule) {
// blob_len). Returns the nanboxed JS value (a fresh, mutable array).
module.declare_function("js_value_from_const_descriptor", DOUBLE, &[PTR, I32]);
module.declare_function("js_array_push_f64", I64, &[I64, DOUBLE]);
module.declare_function("js_array_push_f64_spec", I64, &[I64, DOUBLE]);
module.declare_function("js_array_push_guard", VOID, &[I64]);
module.declare_function("js_array_push_hole", I64, &[I64]);
module.declare_function("js_array_numeric_push_f64_unboxed", I64, &[I64, DOUBLE]);
Expand Down Expand Up @@ -279,6 +280,7 @@ pub fn declare_phase_b_arrays(module: &mut LlModule) {
);
module.declare_function("js_arraylike_at", DOUBLE, &[DOUBLE, DOUBLE]);
module.declare_function("js_arraylike_join", DOUBLE, &[DOUBLE, DOUBLE]);
module.declare_function("js_arraylike_flat", DOUBLE, &[DOUBLE, DOUBLE]);
module.declare_function(
"js_arraylike_slice",
DOUBLE,
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-hir/src/analysis/value_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1590,7 +1590,7 @@ fn infer_arraylike_method_type<F: HirTypeFacts + ?Sized>(
.map(|callback| function_return_type_from_expr(callback, env))
.unwrap_or(Type::Any),
)),
"filter" | "slice" | "splice" => {
"filter" | "slice" | "splice" | "flat" => {
Type::Array(Box::new(array_element_type_from_expr(receiver, env)))
}
"concat" => Type::Array(Box::new(Type::Any)),
Expand Down
29 changes: 29 additions & 0 deletions crates/perry-hir/src/lower/expr_assign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,22 @@ pub(crate) fn lower_ident_assignment(
throw_type_error_const_assignment(&name),
]));
}
// A JavaScript `var` inferred from an array literal can later hold an
// ordinary object. Lowering is source-ordered, so revoke the local's
// Array specialization as soon as a certainly non-array object is
// assigned; subsequent `x.unshift(...)` / indexed reads must use
// dynamic dispatch rather than ArrayHeader intrinsics. The module-wide
// type-widening pass mirrors this decision onto the already-emitted
// `Stmt::Let` declaration for codegen.
let invalidate_array_type = matches!(
ctx.lookup_local_type(&name),
Some(Type::Array(_)) | Some(Type::Tuple(_))
) && assignment_is_certainly_non_array_object(&value);
if invalidate_array_type {
if let Some(ty) = ctx.locals.lookup_type_mut(&name) {
*ty = Type::Any;
}
}
let local_set = Expr::LocalSet(id, value);
let mirrors_script_var = super::lower_expr::global_script_this_enabled()
&& ctx.script_var_decl_names.contains(&name)
Expand Down Expand Up @@ -540,6 +556,19 @@ pub(crate) fn lower_ident_assignment(
}
}

fn assignment_is_certainly_non_array_object(value: &Expr) -> bool {
matches!(
value,
Expr::This
| Expr::Object(_)
| Expr::ObjectSpread { .. }
| Expr::ObjectAssign { .. }
| Expr::Closure { .. }
| Expr::Null
| Expr::Undefined
) || matches!(value, Expr::New { class_name, .. } if class_name != "Array")
}

fn lower_assignment_target(
ctx: &mut LoweringContext,
target: &ast::AssignTarget,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,7 @@ fn try_arraylike_receiver_method(
| "lastIndexOf"
| "includes"
| "slice"
| "flat"
| "at"
| "join"
// Generic mutators with dedicated runtime engines (#4597
Expand Down Expand Up @@ -724,7 +725,7 @@ fn try_arraylike_receiver_method(
// materialize-then-call behavior. `Expr::ArrayFromArrayLikeHoley` keeps
// absent indexed keys as holes (vs `Array.from({ length })` creating
// present undefined slots), so the flatMap callback doesn't visit holes.
// Everything else (mutators, flat, etc.) bails BEFORE lowering the receiver
// Everything else bails BEFORE lowering the receiver
// so unrelated shapes keep the existing member-call behavior.
if method != "flatMap" {
return Ok(None);
Expand Down
13 changes: 13 additions & 0 deletions crates/perry-hir/src/lower/expr_member/member_tail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,11 +409,24 @@ pub(crate) fn lower_member_tail(
// `function_prototype_fallback_target`).
let receiver_is_regexp_ctor = property == "RegExp";
let receiver_is_function_ctor = property == "Function";
// #5898: unknown Array statics are ordinary property reads
// on the constructor object. Preserve that receiver so the
// runtime can walk Array -> Function.prototype (for example
// after `Function.prototype.myproperty = 1`). Known Array
// statics keep the collapsed expression shape used by their
// intrinsic call paths.
let receiver_is_array_ctor_unknown_static = property == "Array"
&& !outer_static_member
.map(|member| {
crate::analysis::is_builtin_static_function_member(property, member)
})
.unwrap_or(false);
if !outer_is_prototype_or_proto
&& !outer_is_constructor_property
&& !receiver_is_namespace_value
&& !receiver_is_regexp_ctor
&& !receiver_is_function_ctor
&& !receiver_is_array_ctor_unknown_static
&& !outer_is_websocket_static
&& !outer_is_reified_object_static_value
&& !outer_is_reified_builtin_static_value
Expand Down
26 changes: 26 additions & 0 deletions crates/perry-hir/src/lower/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,32 @@ fn test_lower_define_and_lookup_local() {
assert_eq!(ctx.lookup_local_type("x"), Some(&Type::Number));
}

#[test]
fn array_inference_is_revoked_after_plain_object_assignment() {
let source = r#"
var value = [1];
value.unshift(0);
value = { 0: 1 };
value.unshift(0);
"#;
let module =
perry_parser::parse_typescript(source, "array-reassign.js").expect("source parses");
let hir =
super::lower_module(&module, "array-reassign", "array-reassign.js").expect("source lowers");
let value_type = hir.init.iter().find_map(|stmt| match stmt {
Stmt::Let { name, ty, .. } if name == "value" => Some(ty),
_ => None,
});
assert_eq!(value_type, Some(&Type::Any));

let dump = format!("{hir:?}");
assert_eq!(
dump.matches("ArrayUnshift").count(),
1,
"only the call before the object reassignment may stay specialized: {dump}"
);
}

#[test]
fn local_declaration_span_survives_ast_to_hir_lowering() {
let source = "function build() {\n const boxed = makeValue();\n return boxed;\n}\n";
Expand Down
54 changes: 53 additions & 1 deletion crates/perry-hir/src/lower/type_widening.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,28 @@ struct WidenSets {
object_like: HashSet<LocalId>,
/// Assigned a primitive that is not a JS number → widen a numeric declared type.
non_number_primitive: HashSet<LocalId>,
/// Assigned a certainly non-array object -> revoke Array/Tuple intrinsics.
non_array_object: HashSet<LocalId>,
}

fn rhs_certainly_non_array_object(expr: &Expr) -> bool {
matches!(
expr,
Expr::This
| Expr::Object(_)
| Expr::ObjectSpread { .. }
| Expr::ObjectAssign { .. }
| Expr::Closure { .. }
| Expr::Null
| Expr::Undefined
) || matches!(expr, Expr::New { class_name, .. } if class_name != "Array")
}

fn visit_expr(expr: &Expr, out: &mut WidenSets, env: &HirTypeEnv) {
if let Expr::LocalSet(id, rhs) = expr {
if rhs_certainly_non_array_object(rhs) {
out.non_array_object.insert(*id);
}
if rhs_certainly_object_like(rhs, env) {
out.object_like.insert(*id);
} else if rhs_certainly_non_number_primitive(rhs, env) {
Expand Down Expand Up @@ -174,6 +192,7 @@ fn widen_lets_stmt(stmt: &mut Stmt, sets: &WidenSets) {
sets.object_like.contains(id) || sets.non_number_primitive.contains(id)
}
Type::String | Type::Boolean => sets.object_like.contains(id),
Type::Array(_) | Type::Tuple(_) => sets.non_array_object.contains(id),
_ => false,
};
if widen {
Expand Down Expand Up @@ -276,7 +295,10 @@ impl TypeWidening {
}

pub(crate) fn apply(&self, stmts: &mut [Stmt]) {
if self.sets.object_like.is_empty() && self.sets.non_number_primitive.is_empty() {
if self.sets.object_like.is_empty()
&& self.sets.non_number_primitive.is_empty()
&& self.sets.non_array_object.is_empty()
{
return;
}
for s in stmts {
Expand Down Expand Up @@ -464,6 +486,36 @@ mod tests {
assert_eq!(let_ty(&module.init, 1), &Type::Any);
}

#[test]
fn widens_array_local_assigned_plain_object() {
let mut module = Module::new("type-widening-test");
module.init = vec![
Stmt::Let {
id: 1,
name: "value".to_string(),
ty: Type::Array(Box::new(Type::Number)),
mutable: true,
init: Some(Expr::Array(vec![Expr::Number(1.0)])),
},
Stmt::Expr(Expr::LocalSet(
1,
Box::new(Expr::New {
class_name: "__AnonShape_test".to_string(),
args: vec![],
type_args: vec![],
byte_offset: 0,
cap_args_appended: 0,
}),
)),
];

let mut widening = TypeWidening::from_module(&module);
widening.collect(&module.init);
widening.apply(&mut module.init);

assert_eq!(let_ty(&module.init, 1), &Type::Any);
}

#[test]
fn widens_primitive_local_assigned_object_null_union() {
let mut module = Module::new("type-widening-test");
Expand Down
Loading
Loading