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
3 changes: 3 additions & 0 deletions changelog.d/6764-async-hooks-final.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

- Locked `node:async_hooks` parity at 195/195 fixtures, including strict frozen provider-table writes and portable lifecycle/provider checks across Windows and Unix.
6 changes: 6 additions & 0 deletions changelog.d/8849-array-length-truncation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Strict writes to a statically proven Array's `length` now retain their ArraySetLength lowering,
and ordinary dense truncation clears discarded slots in one guarded runtime region instead of
performing descriptor and side-table deletion work for every element. The generic path remains for
sloppy, explicit-receiver, sparse, and descriptor-bearing cases. On the unchanged codehz/ecs
15k-command workload, an 11-pair Apple-silicon cohort improved the median from 31.541 ms to
28.354 ms (10.03%, 11/11 wins, 22/22 semantic-oracle passes).
3 changes: 3 additions & 0 deletions changelog.d/8850-node-api-host-core.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added the opt-in, GC-safe Node-API host core with opaque handle scopes,
value/property APIs, native callbacks, references, and pending exceptions as
the runtime foundation for prebuilt `.node` addon support.
62 changes: 61 additions & 1 deletion crates/perry-codegen/src/expr/call_return_array_index_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@ fn store_class(receiver_selector: i64) -> Class {
strict: true,
})],
);
let clear = function(
4,
"clear",
Vec::new(),
Type::Void,
vec![Stmt::Expr(Expr::PutValueSet {
target: Box::new(call_get_data(0)),
key: Box::new(Expr::String("length".to_string())),
value: Box::new(Expr::Integer(0)),
receiver: Box::new(call_get_data(receiver_selector)),
strict: true,
})],
);
Class {
id: 1,
name: "Store".to_string(),
Expand All @@ -87,7 +100,7 @@ fn store_class(receiver_selector: i64) -> Class {
heritage_lexically_shadowed: false,
fields: Vec::new(),
constructor: None,
methods: vec![get_data, write],
methods: vec![get_data, write, clear],
getters: Vec::new(),
setters: Vec::new(),
static_accessor_names: Vec::new(),
Expand Down Expand Up @@ -128,6 +141,16 @@ fn write_method_ir(ir: &str) -> &str {
&method_and_rest[..end + 3]
}

fn clear_method_ir(ir: &str) -> &str {
let signature = "define double @perry_method_call_return_array_put_value_ts__Store__clear(";
let start = ir.find(signature).expect("clear method is present in IR");
let method_and_rest = &ir[start..];
let end = method_and_rest
.find("\n}\n")
.expect("clear method has a closing brace");
&method_and_rest[..end + 3]
}

#[test]
fn same_call_returned_array_uses_array_index_store_and_evaluates_receiver_once() {
let ir = compile_store_ir(0);
Expand All @@ -152,6 +175,43 @@ fn same_call_returned_array_uses_array_index_store_and_evaluates_receiver_once()
);
}

#[test]
fn same_call_returned_array_uses_array_length_store_and_evaluates_receiver_once() {
let ir = compile_store_ir(0);
let clear_ir = clear_method_ir(&ir);

assert!(
clear_ir.contains("call void @js_array_set_length_strict("),
"a call with an Array return type must use ArraySetLength semantics:\n{clear_ir}"
);
assert_eq!(
clear_ir
.matches("@perry_method_call_return_array_put_value_ts__Store__getData")
.count(),
1,
"the duplicated PutValue target/receiver trees represent one source evaluation:\n{clear_ir}"
);
assert!(
!clear_ir.contains("@js_put_value_set_ic_miss("),
"a proven Array length write must not retain the generic property PIC:\n{clear_ir}"
);
}

#[test]
fn distinct_call_returned_array_length_receiver_stays_on_explicit_receiver_path() {
let ir = compile_store_ir(1);
let clear_ir = clear_method_ir(&ir);

assert!(
!clear_ir.contains("call void @js_array_set_length_strict("),
"different target and receiver expressions must not collapse to one Array write:\n{clear_ir}"
);
assert!(
clear_ir.contains("@js_put_value_set"),
"the explicit-receiver PutValue fallback must remain present:\n{clear_ir}"
);
}

#[test]
fn distinct_call_receiver_stays_on_explicit_receiver_put_value_path() {
let ir = compile_store_ir(1);
Expand Down
50 changes: 46 additions & 4 deletions crates/perry-codegen/src/expr/computed_store_rooting_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ fn compile_body(name: &str, body: Vec<Stmt>) -> String {
}

fn compile_body_with_params(name: &str, params: Vec<Param>, body: Vec<Stmt>) -> String {
compile_body_with_params_and_strict(name, params, body, true)
}

fn compile_body_with_params_and_strict(
name: &str,
params: Vec<Param>,
body: Vec<Stmt>,
is_strict: bool,
) -> String {
let mut hir = HirModule::new(name);
hir.functions.push(Function {
id: 0,
Expand All @@ -66,7 +75,7 @@ fn compile_body_with_params(name: &str, params: Vec<Param>, body: Vec<Stmt>) ->
body,
is_async: false,
is_generator: false,
is_strict: true,
is_strict,
is_exported: false,
captures: Vec::new(),
decorators: Vec::new(),
Expand Down Expand Up @@ -445,25 +454,30 @@ fn erased_receiver_inline_store_roots_receiver_and_key_across_rhs() {
};
let collecting = compile("erased_store_collecting", allocating_value());
let inert = compile("erased_store_inert", inert_value());
let callee = "@js_dyn_index_set(";
let callee = "@js_dyn_index_set_strict(";
assert!(
collecting.contains(callee) && inert.contains(callee),
"both fixtures must reach the #5525 inline dynamic-store arm:\n{collecting}\n{inert}"
);
assert_call_operand_rooted_across_operand(
&collecting,
"js_dyn_index_set",
"js_dyn_index_set_strict",
0,
2,
"the erased receiver",
);
assert_call_operand_rooted_across_operand(
&collecting,
"js_dyn_index_set",
"js_dyn_index_set_strict",
1,
2,
"the erased property key",
);
assert_eq!(
call_operand_of(&collecting, "js_dyn_index_set_strict", 3),
"1",
"ES module computed stores must preserve strict assignment semantics"
);
assert_eq!(
root_slots(&collecting),
root_slots(&inert) + 2,
Expand All @@ -472,6 +486,34 @@ fn erased_receiver_inline_store_roots_receiver_and_key_across_rhs() {
);
}

/// A source-text module is strict, but its top-level statements are emitted in
/// a synthetic module-init function that is not itself marked strict. When a
/// strict `PutValueSet` takes the untyped index-store optimization, preserve
/// the reference's flag rather than substituting the container function's.
#[test]
fn put_value_index_fast_path_preserves_explicit_module_strictness() {
let _native_roots = crate::codegen::helpers::NativeRootsPin::native();
let receiver = Expr::LocalGet(1);
let ir = compile_body_with_params_and_strict(
"strict_put_value_index_fast_path",
vec![param(1, "receiver", Type::Any), param(2, "key", Type::Any)],
vec![Stmt::Expr(Expr::PutValueSet {
target: Box::new(receiver.clone()),
key: Box::new(Expr::LocalGet(2)),
value: Box::new(Expr::Integer(1)),
receiver: Box::new(receiver),
strict: true,
})],
false,
);

assert_eq!(
call_operand_of(&ir, "js_dyn_index_set_strict", 3),
"1",
"the strict PutValue reference must survive a non-strict module-init container"
);
}

/// #7640 E follow-up — a cached `BufferViewSlot::data_slot` is safe across a
/// collecting operand only when the construction proves fresh inline storage.
/// View-backed reads/writes must decline before evaluating either operand and
Expand Down
5 changes: 4 additions & 1 deletion crates/perry-codegen/src/expr/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
super::objects_arrays_lit::lower(ctx, expr)
}
Expr::IndexGet { .. } => super::index_get::lower(ctx, expr),
Expr::IndexSet { .. } => super::index_set::lower(ctx, expr, value_discarded),
Expr::IndexSet { .. } => {
let strict = ctx.is_strict_fn;
super::index_set::lower(ctx, expr, value_discarded, strict)
}
Expr::PropertySet { .. } => super::property_set::lower(ctx, expr),
Expr::PropertyGet { .. } => super::property_get::lower(ctx, expr),
Expr::Conditional { .. } => super::conditional::lower(ctx, expr),
Expand Down
15 changes: 12 additions & 3 deletions crates/perry-codegen/src/expr/index_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,16 +355,23 @@ pub(crate) fn lower(
expr: &Expr,
// #7590: THIS expression's value is discarded (not merely the statement's).
value_discarded: bool,
// `PutValueSet` may route a strict module-level reference through this
// fast path even though the synthetic module-init function is non-strict.
assignment_strict: bool,
) -> Result<String> {
match expr {
Expr::IndexSet {
object,
index,
value,
} => {
if let Some(result) =
super::typed_array_rmw::try_lower_guarded_uint32_add(ctx, object, index, value)?
{
if let Some(result) = super::typed_array_rmw::try_lower_guarded_uint32_add(
ctx,
object,
index,
value,
assignment_strict,
)? {
if value_discarded {
return Ok(double_literal(0.0));
}
Expand Down Expand Up @@ -612,6 +619,7 @@ pub(crate) fn lower(
Expr::String(_) | Expr::WtfString(_) | Expr::SymbolFor(_)
) || is_string_expr(ctx, index);
if recv_unknown && !index_is_static_string_or_symbol {
let strict = assignment_strict;
return rooting::with_operands_rooted_across(
ctx,
&[object, index],
Expand All @@ -637,6 +645,7 @@ pub(crate) fn lower(
&vals[0],
&vals[1],
&val_double,
strict,
))
},
);
Expand Down
13 changes: 10 additions & 3 deletions crates/perry-codegen/src/expr/index_set_typed_array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub(super) fn lower_inline_dyn_typed_array_set(
obj_box: &str,
idx_d: &str,
val_double: &str,
strict: bool,
) -> String {
let tag_mask = crate::nanbox::i64_literal(crate::nanbox::TAG_MASK);
let pointer_tag = crate::nanbox::POINTER_TAG_I64;
Expand Down Expand Up @@ -262,12 +263,18 @@ pub(super) fn lower_inline_dyn_typed_array_set(
blk.br(&merge_label);
}

// ---- slow: the unchanged runtime setter ----
// ---- slow: preserve the source function's assignment strictness ----
ctx.current_block = slow_idx;
let strict = if strict { "1" } else { "0" };
ctx.block().call(
DOUBLE,
"js_dyn_index_set",
&[(DOUBLE, obj_box), (DOUBLE, idx_d), (DOUBLE, val_double)],
"js_dyn_index_set_strict",
&[
(DOUBLE, obj_box),
(DOUBLE, idx_d),
(DOUBLE, val_double),
(I32, strict),
],
);
ctx.block().br(&merge_label);

Expand Down
21 changes: 21 additions & 0 deletions crates/perry-codegen/src/expr/proxy_reflect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,23 @@ fn put_value_static_property_fast_path(
let Expr::String(property) = key else {
return None;
};
// Source-level `arr.length = value` lowers to `PutValueSet`, while the
// Array-exotic length implementation lives in `PropertySet::lower`.
// Preserve that statically proven receiver contract here just as
// `put_value_index_fast_path` below does for Array index writes. The two
// receiver trees represent the one source evaluation, so use the shared
// structural identity check and let `PropertySet::lower` evaluate it once.
//
// Only strict writes may take this route: the existing Array length arm
// calls `js_array_set_length_strict`, whereas a rejected sloppy PutValue
// must remain a silent no-op through the generic strict-aware runtime.
if strict
&& property == "length"
&& same_put_value_receiver_expr(target, receiver)
&& is_array_expr(ctx, target)
{
return Some(property.clone());
}
// #6542: this fast path lowers to `js_object_set_field_by_name`, which has
// no `strict` parameter and throws unconditionally when the field is
// non-writable (frozen/sealed object, `writable: false` descriptor). That
Expand Down Expand Up @@ -1586,6 +1603,10 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// path returns the assigned value to ITS caller, which may
// well consume it. Never the discarded form.
false,
// Preserve the reference's own strictness. Module init is
// a synthetic non-strict function even though module code
// carries strict PutValue references.
*strict,
);
}
if let Some(result) =
Expand Down
29 changes: 24 additions & 5 deletions crates/perry-codegen/src/expr/typed_array_rmw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,15 +209,22 @@ fn emit_generic_set(
object: &Expr,
index: &Expr,
value: &str,
assignment_strict: bool,
) -> Result<String> {
// Re-read the immutable reference temporaries after any allocating RHS;
// their slots are the GC-visible source of truth.
let object_box = lower_expr(ctx, object)?;
let index_box = lower_expr(ctx, index)?;
let strict = if assignment_strict { "1" } else { "0" };
Ok(ctx.block().call(
DOUBLE,
"js_dyn_index_set",
&[(DOUBLE, &object_box), (DOUBLE, &index_box), (DOUBLE, value)],
"js_dyn_index_set_strict",
&[
(DOUBLE, &object_box),
(DOUBLE, &index_box),
(DOUBLE, value),
(I32, strict),
],
))
}

Expand All @@ -230,6 +237,7 @@ pub(super) fn try_lower_guarded_uint32_add(
object: &Expr,
index: &Expr,
value: &Expr,
assignment_strict: bool,
) -> Result<Option<String>> {
if !enabled() {
return Ok(None);
Expand Down Expand Up @@ -334,7 +342,13 @@ pub(super) fn try_lower_guarded_uint32_add(
let store_end = ctx.block().label.clone();

ctx.current_block = set_fallback_idx;
let set_fallback_value = emit_generic_set(ctx, candidate.object, candidate.index, &sum)?;
let set_fallback_value = emit_generic_set(
ctx,
candidate.object,
candidate.index,
&sum,
assignment_strict,
)?;
ctx.block().br(&merge_label);
let set_fallback_end = ctx.block().label.clone();

Expand All @@ -344,8 +358,13 @@ pub(super) fn try_lower_guarded_uint32_add(
// stores, and every abrupt-completion case.
ctx.current_block = full_fallback_idx;
let generic_sum = lower_expr(ctx, value)?;
let full_fallback_value =
emit_generic_set(ctx, candidate.object, candidate.index, &generic_sum)?;
let full_fallback_value = emit_generic_set(
ctx,
candidate.object,
candidate.index,
&generic_sum,
assignment_strict,
)?;
ctx.block().br(&merge_label);
let full_fallback_end = ctx.block().label.clone();

Expand Down
Loading
Loading