diff --git a/changelog.d/8646-computed-property-reflection.md b/changelog.d/8646-computed-property-reflection.md
new file mode 100644
index 0000000000..fe8423f832
--- /dev/null
+++ b/changelog.d/8646-computed-property-reflection.md
@@ -0,0 +1,2 @@
+Completed computed property name reflection so a computed key observes the same
+reflection surface as a literal one.
diff --git a/changelog.d/8647-const-array-descriptor.md b/changelog.d/8647-const-array-descriptor.md
new file mode 100644
index 0000000000..564a6bdcb2
--- /dev/null
+++ b/changelog.d/8647-const-array-descriptor.md
@@ -0,0 +1 @@
+perf(codegen): a large, fully-constant array literal — the minified-bundle data-table shape, a giant nested array of number/bool/null constants — now materializes from a compact static rodata descriptor via ONE `js_value_from_const_descriptor` call, instead of one `js_array_from_values` allocation per sub-array plus a huge procedural body. The Claude Code bundle's `__33499` (a constant numeric array-of-arrays) previously lowered to 11,104 allocations and a 245k-instruction body that made `rewrite-statepoints-for-gc` fan out; on a 3,000-row synthetic the descriptor path replaces 3,000+ allocations with a single blob + call and drops the compile from not-finishing-in-two-minutes to 1.46s, byte-identical to the procedural build across the moving-GC matrix. The runtime builds a fresh, mutable array each call (JS array literals are mutable) under `GcSuppressScope` so partially-built parents held across nested child allocations cannot be collected or moved. Gated on a 256-node minimum so small literals keep the fast inline path; `PERRY_CONST_ARRAY_DESCRIPTOR=0` reverts to the procedural path.
diff --git a/changelog.d/8650-test262-builtins-misc-tail.md b/changelog.d/8650-test262-builtins-misc-tail.md
new file mode 100644
index 0000000000..2280c99a90
--- /dev/null
+++ b/changelog.d/8650-test262-builtins-misc-tail.md
@@ -0,0 +1,7 @@
+Closed the remaining test262 built-ins "misc tail" semantics gaps across the
+class-registry construct path, descriptors, prototype chain and proxy
+put-value.
+
+Also lowers the raw-handle ratchet 925 -> 923: the converted sites use the
+#7341 handle shapes rather than re-baselining, so the debt figure drops with
+the change that pays it down.
diff --git a/changelog.d/8651-array-growth-generation.md b/changelog.d/8651-array-growth-generation.md
new file mode 100644
index 0000000000..624c36be02
--- /dev/null
+++ b/changelog.d/8651-array-growth-generation.md
@@ -0,0 +1,12 @@
+Fixed retained array-growth forwarding stubs pointing into resetting nursery
+space. When an old array outgrew its backing storage, `js_array_grow` could
+allocate the replacement in the copying nursery and leave the permanent old
+stub pointing at it. Minor GC does not trace that forwarding payload as a
+normal array slot, so a later nursery reset could recycle the target while
+stale aliases still followed the old forwarding word.
+
+Array growth now keeps the replacement out of the nursery whenever the source
+is old or otherwise non-moving. A young source uses nursery space only through
+the no-collection allocator; if growth could collect and promote the source,
+the replacement is born old instead. This fixes the intermittent ECS failure
+reported as `Cannot assign to read only property 'length' of object`.
diff --git a/changelog.d/8652-diverged-block-guards.md b/changelog.d/8652-diverged-block-guards.md
new file mode 100644
index 0000000000..bbfbe00c2e
--- /dev/null
+++ b/changelog.d/8652-diverged-block-guards.md
@@ -0,0 +1 @@
+fix(codegen): block-creating lowerings (`lower_index_set_fast`, `emit_persistent_shadow_root_barrier`) now emit nothing once the current block is terminated. When a sub-expression provably diverges — a throwing operand (a captured TDZ access / const-reassignment) emits a throw + `unreachable` — the block is terminated and its trailing setup registers are silently dropped, but the guarded fast path / root barrier still created fresh blocks referencing those dropped registers, which the dialect builder rejected as "register %rN used but never defined". Guarding on `ctx.block().is_terminated()` fixes the (unreachable) dead code. Also adds an env-gated `PERRY_DIALECT_DUMP=
` diagnostic that, on a dialect construction failure, names the offending function and dumps its full IR (the failing unit never parses, so `PERRY_SAVE_LL` cannot capture it). Together with #8633 this lets the Claude Code 2.1.112 bundle codegen all 84 units cleanly (previously failed at unit 25).
diff --git a/crates/perry-codegen/src/codegen/helpers.rs b/crates/perry-codegen/src/codegen/helpers.rs
index bc7c28e872..5567aace3f 100644
--- a/crates/perry-codegen/src/codegen/helpers.rs
+++ b/crates/perry-codegen/src/codegen/helpers.rs
@@ -1181,6 +1181,7 @@ pub(super) fn init_static_fields_early(
let mut cur: Option = c.extends_name.clone();
let mut extends_error = false;
let mut extends_data_view = false;
+ let mut extends_typed_array = false;
let mut depth = 0usize;
while let Some(name) = cur {
if matches!(
@@ -1201,6 +1202,10 @@ pub(super) fn init_static_fields_early(
extends_data_view = true;
break;
}
+ if crate::type_analysis::is_typed_array_class(&name) {
+ extends_typed_array = true;
+ break;
+ }
// Walk user-defined ancestor chain.
if let Some(parent) = ctx.classes.get(&name) {
cur = parent.extends_name.clone();
@@ -1230,6 +1235,14 @@ pub(super) fn init_static_fields_early(
);
}
}
+ if extends_typed_array {
+ if let Some(&cid) = ctx.class_ids.get(&c.name) {
+ ctx.block().call_void(
+ "js_register_class_extends_typed_array",
+ &[(crate::types::I32, &cid.to_string())],
+ );
+ }
+ }
}
// Well-known symbol class hooks: HIR lifts `static [Symbol.hasInstance]`
// and `get [Symbol.toStringTag]` to top-level functions with the
diff --git a/crates/perry-codegen/src/expr/array_literal.rs b/crates/perry-codegen/src/expr/array_literal.rs
index 25e7bf6b21..da9616a8ec 100644
--- a/crates/perry-codegen/src/expr/array_literal.rs
+++ b/crates/perry-codegen/src/expr/array_literal.rs
@@ -74,6 +74,19 @@ pub(crate) fn lower_array_literal(ctx: &mut FnCtx<'_>, elements: &[Expr]) -> Res
return Ok(nanbox_pointer_inline(ctx.block(), &arr));
}
+ // #8583 follow-up: a LARGE, fully-CONSTANT array literal (the minified
+ // data-table shape — a giant nested array of number/bool/null literals)
+ // becomes a static rodata descriptor + ONE bulk-materialization runtime
+ // call, instead of the N per-subarray `js_array_from_values` allocations and
+ // the giant procedural body that made `__33499` fan out under RS4GC (245k
+ // instrs / 11,104 allocations → one call over a compact blob). Small const
+ // arrays fall through to the fast inline bump-alloc path below.
+ if const_array_descriptor_enabled() {
+ if let Some(v) = try_lower_const_array_descriptor(ctx, elements) {
+ return Ok(v);
+ }
+ }
+
// Evaluate all element expressions *before* allocating, so nested
// allocations inside element expressions don't see a half-initialized
// outer array. Each evaluated value is kept in a temp root until the last
@@ -271,3 +284,137 @@ pub(crate) fn lower_array_literal(ctx: &mut FnCtx<'_>, elements: &[Expr]) -> Res
Ok(nanbox_pointer_inline(ctx.block(), &arr))
})
}
+
+/// #8583 follow-up gate. Default ON; `PERRY_CONST_ARRAY_DESCRIPTOR=0/off/false`
+/// reverts every large constant literal to the procedural construction path
+/// (A/B bisection, and an escape hatch if a descriptor ever proves wrong).
+fn const_array_descriptor_enabled() -> bool {
+ !matches!(
+ std::env::var("PERRY_CONST_ARRAY_DESCRIPTOR").as_deref(),
+ Ok("0") | Ok("off") | Ok("false")
+ )
+}
+
+/// A value the const-descriptor path can materialize with no JS evaluation:
+/// number/int/bool/null/undefined, or an array recursively of the same. Strings
+/// and objects decline (v2) — the whole literal then falls back to the
+/// procedural path, so a mixed table is never half-materialized.
+fn is_const_materializable(e: &Expr) -> bool {
+ match e {
+ Expr::Number(_) | Expr::Integer(_) | Expr::Bool(_) | Expr::Null | Expr::Undefined => true,
+ Expr::Array(elems) => elems.iter().all(is_const_materializable),
+ _ => false,
+ }
+}
+
+/// Total materializable nodes (every scalar + every array), the size gate below.
+fn count_const_nodes(e: &Expr) -> usize {
+ match e {
+ Expr::Array(elems) => 1 + elems.iter().map(count_const_nodes).sum::(),
+ _ => 1,
+ }
+}
+
+/// Serialize one constant value into the descriptor blob (must match the tag
+/// bytes in `perry-runtime/src/array/alloc.rs::build_const_value`).
+fn serialize_const_value(e: &Expr, out: &mut Vec) {
+ match e {
+ Expr::Number(n) => {
+ out.push(0);
+ out.extend_from_slice(&n.to_le_bytes());
+ }
+ Expr::Integer(i) => {
+ out.push(0);
+ out.extend_from_slice(&(*i as f64).to_le_bytes());
+ }
+ Expr::Bool(true) => out.push(2),
+ Expr::Bool(false) => out.push(3),
+ Expr::Null => out.push(4),
+ Expr::Undefined => out.push(5),
+ Expr::Array(elems) => {
+ out.push(1);
+ out.extend_from_slice(&(elems.len() as u32).to_le_bytes());
+ for el in elems {
+ serialize_const_value(el, out);
+ }
+ }
+ // Guarded by `is_const_materializable`; unreachable in practice.
+ _ => out.push(5),
+ }
+}
+
+/// Only worth a rodata blob + runtime call for genuinely large tables; small
+/// const arrays keep the fast inline bump-alloc path (no regression). `__33499`
+/// has ~44k nodes; an ordinary `[1,2,3]` has 4 and never qualifies.
+const CONST_DESCRIPTOR_MIN_NODES: usize = 256;
+
+/// If `elements` is a large, fully-constant array literal, emit a static rodata
+/// descriptor and a single `js_value_from_const_descriptor` call and return the
+/// nanboxed value; otherwise `None` (caller falls back to the procedural path).
+fn try_lower_const_array_descriptor(ctx: &mut FnCtx<'_>, elements: &[Expr]) -> Option {
+ if !elements.iter().all(is_const_materializable) {
+ return None;
+ }
+ // Only NESTED constant tables benefit: the fan-out cost is the per-subarray
+ // allocation (a data table lowers to thousands of `js_array_from_values`).
+ // A flat constant scalar array is already a single `js_array_alloc_literal`
+ // + inline stores, so keep that path — it also preserves the precise
+ // per-slot write barriers a later push/store relies on.
+ if !elements.iter().any(|e| matches!(e, Expr::Array(_))) {
+ return None;
+ }
+ let total_nodes: usize = 1 + elements.iter().map(count_const_nodes).sum::();
+ if total_nodes < CONST_DESCRIPTOR_MIN_NODES {
+ return None;
+ }
+
+ // Serialize the outer array: tag 1 (ARRAY) + u32 count + each element.
+ let mut blob: Vec = Vec::new();
+ blob.push(1);
+ blob.extend_from_slice(&(elements.len() as u32).to_le_bytes());
+ for el in elements {
+ serialize_const_value(el, &mut blob);
+ }
+
+ // Emit the blob as a module-private rodata constant (mirrors
+ // `expr/strings.rs::emit_string_literal_global`; `ic_site_counter` is the
+ // module-wide site identity so re-emitted bodies don't collide).
+ let idx = ctx.ic_site_counter;
+ ctx.ic_site_counter += 1;
+ let func_part: String = ctx
+ .func
+ .name
+ .chars()
+ .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
+ .collect();
+ let global_name = format!("perry_const_arr_{}_{}", func_part, idx);
+ let mut lit = String::with_capacity(blob.len() + 4);
+ lit.push('c');
+ lit.push('"');
+ for &b in &blob {
+ if (32..127).contains(&b) && b != b'"' && b != b'\\' {
+ lit.push(b as char);
+ } else {
+ lit.push('\\');
+ lit.push_str(&format!("{:02X}", b));
+ }
+ }
+ lit.push('"');
+ ctx.typed_parse_rodata.push(format!(
+ "@{} = private unnamed_addr constant [{} x i8] {}",
+ global_name,
+ blob.len(),
+ lit
+ ));
+
+ // ONE runtime call materializes the whole nested structure and returns the
+ // nanboxed (DOUBLE) JS value directly — no per-element IR, so no fan-out.
+ let global_ref = format!("@{}", global_name);
+ let len_str = blob.len().to_string();
+ let v = ctx.block().call(
+ DOUBLE,
+ "js_value_from_const_descriptor",
+ &[(PTR, &global_ref), (I32, &len_str)],
+ );
+ Some(v)
+}
diff --git a/crates/perry-codegen/src/expr/index.rs b/crates/perry-codegen/src/expr/index.rs
index 4bfa6cdfee..51f9613929 100644
--- a/crates/perry-codegen/src/expr/index.rs
+++ b/crates/perry-codegen/src/expr/index.rs
@@ -93,6 +93,19 @@ pub(crate) fn lower_index_set_fast(
value_is_canonical_raw_f64: bool,
feedback_site_id: &str,
) -> Result<()> {
+ // #8583-followup: if evaluating an operand diverged — a throwing
+ // sub-expression (e.g. a TDZ access on a captured `let`) emitted a
+ // `js_throw_error_with_code` + `unreachable` — the current block is
+ // terminated. `LlBlock` silently drops any instruction emitted after a
+ // terminator, so the element setup below (`arr_bits`/`arr_handle`/`idx_i32`)
+ // is dropped, but the guarded fast path still creates fresh blocks that
+ // reference those dropped registers, which the dialect builder rejects as
+ // "register %rN used but never defined". The index-set is unreachable on
+ // this path, so emit nothing.
+ if ctx.block().is_terminated() {
+ return Ok(());
+ }
+
// Capture the local slot for the realloc path.
let slot = ctx
.locals
diff --git a/crates/perry-codegen/src/expr/shadow_slot.rs b/crates/perry-codegen/src/expr/shadow_slot.rs
index afa1525900..6b89e15e78 100644
--- a/crates/perry-codegen/src/expr/shadow_slot.rs
+++ b/crates/perry-codegen/src/expr/shadow_slot.rs
@@ -315,6 +315,16 @@ pub(crate) fn emit_shadow_slot_bind_ptr(ctx: &mut FnCtx<'_>, slot_idx: u32, slot
/// `monotonic`, matching the runtime's Rust `Relaxed` readers: the counter is
/// only a gate and does not publish accompanying memory.
pub(crate) fn emit_persistent_shadow_root_barrier(ctx: &mut FnCtx<'_>, value_bits: &str) {
+ // #8583-followup: if computing the value diverged (a throwing sub-expression
+ // — e.g. a TDZ access on a captured `let` — emitted `unreachable`), the
+ // current block is terminated. `LlBlock` drops instructions emitted after a
+ // terminator, so `value_bits`' defining instruction was silently discarded;
+ // the barrier block created below would then reference an undefined register
+ // ("register %rN used but never defined"). The root store is unreachable on
+ // this path, so emit no barrier.
+ if ctx.block().is_terminated() {
+ return;
+ }
let active =
ctx.block()
.load_atomic_monotonic(I32, "@PERRY_INCREMENTAL_MARK_BARRIER_ACTIVE_COUNT", 4);
diff --git a/crates/perry-codegen/src/expr/unary.rs b/crates/perry-codegen/src/expr/unary.rs
index 311e3e2d7a..01f15e13eb 100644
--- a/crates/perry-codegen/src/expr/unary.rs
+++ b/crates/perry-codegen/src/expr/unary.rs
@@ -42,7 +42,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result {
if numeric {
Ok(v)
} else {
- Ok(blk.call(DOUBLE, "js_number_coerce", &[(DOUBLE, &v)]))
+ Ok(blk.call(DOUBLE, "js_dynamic_pos", &[(DOUBLE, &v)]))
}
}
UnaryOp::Not => {
diff --git a/crates/perry-codegen/src/native_emit.rs b/crates/perry-codegen/src/native_emit.rs
index b21d815308..2010ee7090 100644
--- a/crates/perry-codegen/src/native_emit.rs
+++ b/crates/perry-codegen/src/native_emit.rs
@@ -255,20 +255,66 @@ fn stream_frozen_functions<'ctx>(
.map_err(|e| anyhow!("native IR construction failed in @{}: {e:#}", f.name))?;
for item in &f.items {
use crate::function::FinalItem as FI;
- match item {
- FrozenItem::Label(s) => stream.item(&FI::Label(s))?,
- FrozenItem::Blank => stream.item(&FI::Blank)?,
- FrozenItem::Text(s) => stream.item(&FI::Text(s))?,
- FrozenItem::Inst(i) => stream.item(&FI::Inst(i))?,
- }
+ let res = match item {
+ FrozenItem::Label(s) => stream.item(&FI::Label(s)),
+ FrozenItem::Blank => stream.item(&FI::Blank),
+ FrozenItem::Text(s) => stream.item(&FI::Text(s)),
+ FrozenItem::Inst(i) => stream.item(&FI::Inst(i)),
+ };
+ res.map_err(|e| dump_dialect_failure(f, e))?;
}
- let (t, r) = stream.finish()?;
+ let (t, r) = stream.finish().map_err(|e| dump_dialect_failure(f, e))?;
typed += t;
raw += r;
}
Ok((typed, raw))
}
+/// Diagnostic for a dialect construction failure (e.g. "register %rN was used
+/// but never defined"): name the offending function and, when
+/// `PERRY_DIALECT_DUMP=` is set, write the function's full constructed IR
+/// text (typed insts rendered via `render_into`) to `/.ll` so the
+/// malformed use site is visible. The failing unit never parses, so the normal
+/// `PERRY_SAVE_LL` post-parse dump cannot capture it.
+fn dump_dialect_failure(f: &FrozenFunction, e: anyhow::Error) -> anyhow::Error {
+ if let Ok(dir) = std::env::var("PERRY_DIALECT_DUMP") {
+ let _ = std::fs::create_dir_all(&dir);
+ let mut buf = String::new();
+ buf.push_str(&f.header);
+ buf.push('\n');
+ for item in &f.items {
+ match item {
+ FrozenItem::Label(s) => {
+ buf.push_str(s);
+ buf.push('\n');
+ }
+ FrozenItem::Blank => buf.push('\n'),
+ FrozenItem::Text(s) => {
+ buf.push_str(s);
+ buf.push('\n');
+ }
+ FrozenItem::Inst(i) => {
+ i.render_into(&mut buf);
+ buf.push('\n');
+ }
+ }
+ }
+ let safe: String = f
+ .name
+ .chars()
+ .map(|c| {
+ if c.is_alphanumeric() || c == '_' {
+ c
+ } else {
+ '_'
+ }
+ })
+ .collect();
+ let _ = std::fs::write(format!("{dir}/{safe}.ll"), &buf);
+ }
+ anyhow!("native IR construction failed in @{}: {e:#}", f.name)
+}
+
/// Native construction for a module large enough to split into codegen
/// units (#5391): each unit is its own context+module (peak RSS stays
/// ~whole/n, same bound as the per-unit clang model), functions stream with
diff --git a/crates/perry-codegen/src/runtime_decls/arrays.rs b/crates/perry-codegen/src/runtime_decls/arrays.rs
index 6403a3e7b0..2a4e81251e 100644
--- a/crates/perry-codegen/src/runtime_decls/arrays.rs
+++ b/crates/perry-codegen/src/runtime_decls/arrays.rs
@@ -37,6 +37,10 @@ pub fn declare_phase_b_arrays(module: &mut LlModule) {
// #5391: build an array literal from a stack buffer of N values in one call
// (outlines the inline alloc + per-element store/note/barrier). (values_ptr, n).
module.declare_function("js_array_from_values", I64, &[PTR, I32]);
+ // #8583 follow-up: materialize a large, fully-constant nested array literal
+ // from a static rodata descriptor blob in ONE call — (descriptor_ptr,
+ // 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_guard", VOID, &[I64]);
module.declare_function("js_array_push_hole", I64, &[I64]);
diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs
index df1c5204d1..a89983e334 100644
--- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs
+++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs
@@ -119,6 +119,7 @@ pub(crate) fn declare_core(module: &mut LlModule) {
// ========== NaN-boxing / typeof / is_* ==========
module.declare_function("js_dynamic_neg", DOUBLE, &[DOUBLE]);
+ module.declare_function("js_dynamic_pos", DOUBLE, &[DOUBLE]);
module.declare_function("js_dynamic_string_equals", I32, &[DOUBLE, DOUBLE]);
module.declare_function("js_is_nan", DOUBLE, &[DOUBLE]);
module.declare_function("js_jsvalue_compare", I32, &[DOUBLE, DOUBLE]);
diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs
index ad24e28397..c8a1ff4a19 100644
--- a/crates/perry-codegen/src/runtime_decls/strings.rs
+++ b/crates/perry-codegen/src/runtime_decls/strings.rs
@@ -1303,6 +1303,7 @@ pub fn declare_phase_b_strings(module: &mut LlModule) {
module.declare_function("js_instanceof_noncallable_rhs", DOUBLE, &[]);
module.declare_function("js_register_class_extends_error", VOID, &[I32]);
module.declare_function("js_register_class_extends_data_view", VOID, &[I32]);
+ module.declare_function("js_register_class_extends_typed_array", VOID, &[I32]);
module.declare_function("js_register_class_id", VOID, &[I32]);
// #1021 NestJS: surface Perry class names to V8 so `metatype.name`
// is non-empty. Codegen emits one call per registered class id at
diff --git a/crates/perry-codegen/src/type_analysis.rs b/crates/perry-codegen/src/type_analysis.rs
index c15c84113c..01d70ace64 100644
--- a/crates/perry-codegen/src/type_analysis.rs
+++ b/crates/perry-codegen/src/type_analysis.rs
@@ -38,8 +38,8 @@ pub(crate) use numeric::{
pub(crate) use pod::{
add_operands_have_pod_materialization_hazard,
expr_may_return_boxed_value_from_raw_f64_fallback, expression_has_numeric_length,
- is_fixed_width_buffer_numeric_read, is_numeric_typed_array_class, is_typed_array_expr,
- numeric_proof_is_declared_only, pod_record_field_is_numeric,
+ is_fixed_width_buffer_numeric_read, is_numeric_typed_array_class, is_typed_array_class,
+ is_typed_array_expr, numeric_proof_is_declared_only, pod_record_field_is_numeric,
scalar_replaced_array_element_is_raw_f64, scalar_replaced_field_is_raw_f64,
scalar_replaced_field_raw_f64_store_state,
};
diff --git a/crates/perry-hir/src/lower/const_fold_fn.rs b/crates/perry-hir/src/lower/const_fold_fn.rs
index 39bb4f830f..7257daf37d 100644
--- a/crates/perry-hir/src/lower/const_fold_fn.rs
+++ b/crates/perry-hir/src/lower/const_fold_fn.rs
@@ -36,6 +36,9 @@ use super::global_eval_hoist::{
use super::lower_expr::lower_expr;
use super::LoweringContext;
+mod param_early_error;
+use param_early_error::fn_ctor_kind_param_early_error;
+
/// Lower an expression that throws a `SyntaxError` when the enclosing call
/// site is evaluated — a throwing IIFE in value position. Used when a folded
/// `new Function(...)` / `Function(...)` body is not syntactically valid JS,
@@ -311,37 +314,6 @@ pub(crate) fn try_const_fold_function_construct_kind(
None => (String::new(), String::new()),
};
- // CSP capability-probe handling (`PERRY_EVAL_CSP`). A trivial no-op
- // `new Function("")` / `Function("")` is the canonical runtime-codegen
- // feature-test (`try { new Function(""), true } catch { false }`). perry is
- // ahead-of-time compiled and cannot generate code from a runtime string, so
- // under CSP mode this probe must report "unavailable" — throw at
- // *construction* (not when called), exactly as a CSP `unsafe-eval`-blocked
- // environment does — so probing callers (e.g. zod 4's validator JIT) take
- // their non-codegen interpreter fallback. Only the trivial empty-body no-op
- // is refused; real literal bodies (`return 42`, the `return this` globalThis
- // polyfill) still fold, preserving spec behavior by default.
- //
- // The probe passes AT LEAST ONE string argument (`new Function("")`). A
- // ZERO-argument `new Function()` is not a codegen request at all — it
- // constructs the empty function `anonymous() {}` — so it must NEVER throw,
- // even under the default CSP mode (#5835 Intl-ctor test regressed here:
- // `new Function()` used purely as a constructable-with-settable-prototype
- // scaffold for `Reflect.construct` began throwing). Gate the refusal on
- // there actually being an argument.
- if !consts.is_empty()
- && body_src.trim().is_empty()
- && crate::eval_classifier::eval_csp_probe_unavailable()
- {
- return synth_throwing_iife(
- ctx,
- "throw new TypeError(\"Function: runtime dynamic code generation is \
- unavailable in this ahead-of-time compiled binary\");",
- span,
- )
- .map(Some);
- }
-
// Assemble the exact source text the spec's CreateDynamicFunction
// prescribes: newlines around the body and *before the closing paren*
// so a `//` comment in the params or body can't swallow a delimiter.
@@ -374,10 +346,49 @@ pub(crate) fn try_const_fold_function_construct_kind(
// prologue makes duplicate or `eval`/`arguments` parameter names a
// SyntaxError, and a private name (`o.#f`) outside any class body is a
// SyntaxError regardless of mode (AllPrivateIdentifiersValid).
- if fn_ctor_strict_param_early_error(fn_expr) || fn_body_has_stray_private_name(fn_expr) {
+ if fn_ctor_kind_param_early_error(fn_expr, kind)
+ || fn_ctor_strict_param_early_error(fn_expr)
+ || fn_body_has_stray_private_name(fn_expr)
+ {
return synth_function_syntax_error(ctx, surface, span).map(Some);
}
+ // CSP capability-probe handling (`PERRY_EVAL_CSP`). A trivial no-op
+ // `new Function("")` / `Function("")` is the canonical runtime-codegen
+ // feature-test (`try { new Function(""), true } catch { false }`). perry is
+ // ahead-of-time compiled and cannot generate code from a runtime string, so
+ // under CSP mode this probe must report "unavailable" — throw at
+ // *construction* (not when called), exactly as a CSP `unsafe-eval`-blocked
+ // environment does — so probing callers (e.g. zod 4's validator JIT) take
+ // their non-codegen interpreter fallback. Only the trivial empty-body no-op
+ // is refused; real literal bodies (`return 42`, the `return this` globalThis
+ // polyfill) still fold, preserving spec behavior by default.
+ //
+ // Parameter/body syntax and CreateDynamicFunction early errors take
+ // precedence over this Perry-specific capability signal. In particular,
+ // `GeneratorFunction("x = yield", "")` must throw SyntaxError rather than
+ // being mistaken for a valid empty-body capability probe.
+ //
+ // The probe passes AT LEAST ONE string argument (`new Function("")`). A
+ // ZERO-argument `new Function()` is not a codegen request at all — it
+ // constructs the empty function `anonymous() {}` — so it must NEVER throw,
+ // even under the default CSP mode (#5835 Intl-ctor test regressed here:
+ // `new Function()` used purely as a constructable-with-settable-prototype
+ // scaffold for `Reflect.construct` began throwing). Gate the refusal on
+ // there actually being an argument.
+ if !consts.is_empty()
+ && body_src.trim().is_empty()
+ && crate::eval_classifier::eval_csp_probe_unavailable()
+ {
+ return synth_throwing_iife(
+ ctx,
+ "throw new TypeError(\"Function: runtime dynamic code generation is \
+ unavailable in this ahead-of-time compiled binary\");",
+ span,
+ )
+ .map(Some);
+ }
+
let outer_strict = ctx.current_strict;
ctx.current_strict = false;
let lowered_result = lower_fn_expr(ctx, fn_expr);
@@ -1352,7 +1363,7 @@ pub(crate) fn try_eval_function_call_fold(
}
// `var AsyncFunction = (async function(){}).constructor; AsyncFunction(...)`
// — a single-assignment module var recorded as a dynamic-function ctor.
- if ctx.scope_depth == 0 {
+ if ctx.local_decl_scope_depth(id.sym.as_str()) == Some(0) {
if let Some(super::fn_ctor_env::FnCtorShape::DynCtor(kind)) =
ctx.fn_ctor_env.entries.get(id.sym.as_str()).cloned()
{
diff --git a/crates/perry-hir/src/lower/const_fold_fn/param_early_error.rs b/crates/perry-hir/src/lower/const_fold_fn/param_early_error.rs
new file mode 100644
index 0000000000..8dda185ead
--- /dev/null
+++ b/crates/perry-hir/src/lower/const_fold_fn/param_early_error.rs
@@ -0,0 +1,116 @@
+use swc_ecma_ast as ast;
+
+use super::super::fn_ctor_env::DynFnCtorKind;
+
+/// CreateDynamicFunction's parameter early errors are kind-sensitive. Inspect
+/// the parsed parameter AST so keyword-looking text in comments or string
+/// literals is ignored while real `yield` / `await` syntax is rejected.
+pub(super) fn fn_ctor_kind_param_early_error(fn_expr: &ast::FnExpr, kind: DynFnCtorKind) -> bool {
+ let forbidden = match kind {
+ DynFnCtorKind::Generator => &["yield"][..],
+ DynFnCtorKind::Async => &["await"][..],
+ DynFnCtorKind::AsyncGenerator => &["yield", "await"][..],
+ DynFnCtorKind::Plain => return false,
+ };
+
+ fn prop_name_has(name: &ast::PropName, forbidden: &[&str]) -> bool {
+ matches!(name, ast::PropName::Computed(c) if expr_has(&c.expr, forbidden))
+ }
+
+ fn pat_has(pat: &ast::Pat, forbidden: &[&str]) -> bool {
+ match pat {
+ ast::Pat::Ident(id) => forbidden.contains(&id.id.sym.as_ref()),
+ ast::Pat::Array(array) => array
+ .elems
+ .iter()
+ .flatten()
+ .any(|pat| pat_has(pat, forbidden)),
+ ast::Pat::Object(object) => object.props.iter().any(|prop| match prop {
+ ast::ObjectPatProp::KeyValue(kv) => {
+ prop_name_has(&kv.key, forbidden) || pat_has(&kv.value, forbidden)
+ }
+ ast::ObjectPatProp::Assign(assign) => {
+ forbidden.contains(&assign.key.sym.as_ref())
+ || assign
+ .value
+ .as_deref()
+ .is_some_and(|value| expr_has(value, forbidden))
+ }
+ ast::ObjectPatProp::Rest(rest) => pat_has(&rest.arg, forbidden),
+ }),
+ ast::Pat::Assign(assign) => {
+ pat_has(&assign.left, forbidden) || expr_has(&assign.right, forbidden)
+ }
+ ast::Pat::Rest(rest) => pat_has(&rest.arg, forbidden),
+ ast::Pat::Expr(expr) => expr_has(expr, forbidden),
+ ast::Pat::Invalid(_) => false,
+ }
+ }
+
+ fn expr_has(expr: &ast::Expr, forbidden: &[&str]) -> bool {
+ match expr {
+ ast::Expr::Ident(id) => forbidden.contains(&id.sym.as_ref()),
+ ast::Expr::Yield(_) => forbidden.contains(&"yield"),
+ ast::Expr::Await(await_expr) => {
+ forbidden.contains(&"await") || expr_has(&await_expr.arg, forbidden)
+ }
+ ast::Expr::Paren(paren) => expr_has(&paren.expr, forbidden),
+ ast::Expr::Unary(unary) => expr_has(&unary.arg, forbidden),
+ ast::Expr::Update(update) => expr_has(&update.arg, forbidden),
+ ast::Expr::Bin(binary) => {
+ expr_has(&binary.left, forbidden) || expr_has(&binary.right, forbidden)
+ }
+ ast::Expr::Assign(assign) => expr_has(&assign.right, forbidden),
+ ast::Expr::Cond(cond) => {
+ expr_has(&cond.test, forbidden)
+ || expr_has(&cond.cons, forbidden)
+ || expr_has(&cond.alt, forbidden)
+ }
+ ast::Expr::Seq(seq) => seq.exprs.iter().any(|expr| expr_has(expr, forbidden)),
+ ast::Expr::Member(member) => {
+ expr_has(&member.obj, forbidden)
+ || matches!(&member.prop, ast::MemberProp::Computed(c) if expr_has(&c.expr, forbidden))
+ }
+ ast::Expr::Call(call) => {
+ matches!(&call.callee, ast::Callee::Expr(expr) if expr_has(expr, forbidden))
+ || call.args.iter().any(|arg| expr_has(&arg.expr, forbidden))
+ }
+ ast::Expr::New(new_expr) => {
+ expr_has(&new_expr.callee, forbidden)
+ || new_expr
+ .args
+ .as_ref()
+ .is_some_and(|args| args.iter().any(|arg| expr_has(&arg.expr, forbidden)))
+ }
+ ast::Expr::Array(array) => array
+ .elems
+ .iter()
+ .flatten()
+ .any(|elem| expr_has(&elem.expr, forbidden)),
+ ast::Expr::Object(object) => object.props.iter().any(|prop| match prop {
+ ast::PropOrSpread::Spread(spread) => expr_has(&spread.expr, forbidden),
+ ast::PropOrSpread::Prop(prop) => match prop.as_ref() {
+ ast::Prop::KeyValue(kv) => {
+ prop_name_has(&kv.key, forbidden) || expr_has(&kv.value, forbidden)
+ }
+ ast::Prop::Assign(assign) => expr_has(&assign.value, forbidden),
+ ast::Prop::Getter(getter) => prop_name_has(&getter.key, forbidden),
+ ast::Prop::Setter(setter) => prop_name_has(&setter.key, forbidden),
+ ast::Prop::Method(method) => prop_name_has(&method.key, forbidden),
+ ast::Prop::Shorthand(id) => forbidden.contains(&id.sym.as_ref()),
+ },
+ }),
+ ast::Expr::TsAs(ts) => expr_has(&ts.expr, forbidden),
+ ast::Expr::TsTypeAssertion(ts) => expr_has(&ts.expr, forbidden),
+ ast::Expr::TsConstAssertion(ts) => expr_has(&ts.expr, forbidden),
+ ast::Expr::TsNonNull(ts) => expr_has(&ts.expr, forbidden),
+ _ => false,
+ }
+ }
+
+ fn_expr
+ .function
+ .params
+ .iter()
+ .any(|param| pat_has(¶m.pat, forbidden))
+}
diff --git a/crates/perry-hir/src/lower/expr_assign.rs b/crates/perry-hir/src/lower/expr_assign.rs
index a66d010223..b2c9799908 100644
--- a/crates/perry-hir/src/lower/expr_assign.rs
+++ b/crates/perry-hir/src/lower/expr_assign.rs
@@ -482,6 +482,12 @@ pub(crate) fn lower_ident_assignment(
Ok(*value)
} else {
if ctx.current_strict {
+ if matches!(name.as_str(), "undefined" | "NaN" | "Infinity") {
+ return Ok(Expr::Sequence(vec![
+ *value,
+ throw_type_error_const_assignment(&name),
+ ]));
+ }
// #5989: strict-mode assignment to an existing global
// builtin is a property write, not a ReferenceError. See
// `strict_global_assign_existing_or_throw` for the full
@@ -554,6 +560,25 @@ fn lower_assignment_target(
// Check if this is a static field assignment (e.g., Counter.count = 5)
if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() {
let obj_name = obj_ident.sym.to_string();
+ // Dynamic GeneratorFunction/AsyncGeneratorFunction results
+ // use a compact non-closure runtime representation, but still
+ // inherit Function.prototype's poisoned caller/arguments
+ // accessors. The local's inferred brand is authoritative here.
+ if let ast::MemberProp::Ident(prop_ident) = &member.prop {
+ let is_dynamic_generator = matches!(
+ ctx.lookup_local_type(&obj_name),
+ Some(Type::Named(name))
+ if matches!(name.as_str(), "GeneratorFunction" | "AsyncGeneratorFunction")
+ );
+ if is_dynamic_generator
+ && matches!(prop_ident.sym.as_ref(), "caller" | "arguments")
+ {
+ return Ok(Expr::Sequence(vec![
+ *value,
+ throw_restricted_function_property_assignment(),
+ ]));
+ }
+ }
// `f.caller = v` / `f.arguments = v` on a declared function —
// the poisoned setter-less accessor on Function.prototype
// throws (strict semantics; Perry-compiled code is strict).
diff --git a/crates/perry-hir/src/lower/expr_call/url_date_instance.rs b/crates/perry-hir/src/lower/expr_call/url_date_instance.rs
index 840c752f9c..fd842b7a68 100644
--- a/crates/perry-hir/src/lower/expr_call/url_date_instance.rs
+++ b/crates/perry-hir/src/lower/expr_call/url_date_instance.rs
@@ -92,6 +92,19 @@ pub(super) fn try_url_date_weakref_instance(
// `let u = new URL(...); u.toString()` (typed local)
if let ast::MemberProp::Ident(method_ident) = &member.prop {
let method_name = method_ident.sym.as_ref();
+ // `%Date.prototype%` is an ordinary object without [[DateValue]].
+ // Do not feed direct calls on it into the statically-specialized
+ // DateCell path; the reflective prototype thunk performs the
+ // required brand check and throws TypeError.
+ let receiver_is_date_prototype = matches!(
+ member.obj.as_ref(),
+ ast::Expr::Member(proto_member)
+ if matches!(proto_member.obj.as_ref(), ast::Expr::Ident(id) if id.sym.as_ref() == "Date")
+ && matches!(&proto_member.prop, ast::MemberProp::Ident(id) if id.sym.as_ref() == "prototype")
+ );
+ if receiver_is_date_prototype {
+ return Ok(Err(args));
+ }
if static_receiver_class(ctx, member.obj.as_ref()) == Some("URL") {
match method_name {
"toString" => {
@@ -288,11 +301,9 @@ pub(super) fn try_url_date_weakref_instance(
let date_expr = lower_expr(ctx, &member.obj)?;
return Ok(Ok(Expr::DateGetUtcMilliseconds(Box::new(date_expr))));
}
- // Other getters/methods
- "valueOf" => {
- let date_expr = lower_expr(ctx, &member.obj)?;
- return Ok(Ok(Expr::DateValueOf(Box::new(date_expr))));
- }
+ // Other getters/methods. `valueOf` deliberately remains a
+ // generic property call: an own replacement on a Date must
+ // take precedence over Date.prototype.valueOf.
"toDateString" => {
let date_expr = lower_expr(ctx, &member.obj)?;
return Ok(Ok(Expr::DateToDateString(Box::new(date_expr))));
diff --git a/crates/perry-hir/src/lower/expr_member/member_tail.rs b/crates/perry-hir/src/lower/expr_member/member_tail.rs
index 13d69c6ddd..74e8d1370e 100644
--- a/crates/perry-hir/src/lower/expr_member/member_tail.rs
+++ b/crates/perry-hir/src/lower/expr_member/member_tail.rs
@@ -27,6 +27,33 @@ pub(crate) fn lower_member_tail(
_ => lower_expr(ctx, &member.obj)?,
};
if let ast::MemberProp::Ident(prop_ident) = &member.prop {
+ // A function produced by the dynamic GeneratorFunction constructors
+ // inherits the caller/arguments poison accessors from
+ // %Function.prototype%. Its compact runtime representation is not a
+ // normal registered closure, so preserve the statically-known brand
+ // here instead of letting generic PropertyGet return undefined.
+ if matches!(prop_ident.sym.as_ref(), "caller" | "arguments") {
+ let is_dynamic_generator = match member.obj.as_ref() {
+ ast::Expr::Ident(obj_ident) => matches!(
+ ctx.lookup_local_type(obj_ident.sym.as_ref()),
+ Some(Type::Named(name))
+ if matches!(name.as_str(), "GeneratorFunction" | "AsyncGeneratorFunction")
+ ),
+ _ => false,
+ };
+ if is_dynamic_generator {
+ return Ok(Expr::Call {
+ callee: Box::new(Expr::ExternFuncRef {
+ name: "js_throw_restricted_function_property_assignment".to_string(),
+ param_types: vec![],
+ return_type: Type::Any,
+ }),
+ args: vec![],
+ type_args: vec![],
+ byte_offset: member.span.lo.0,
+ });
+ }
+ }
if let Some(value) = ws_ready_state_value(prop_ident.sym.as_ref()) {
if is_ws_ready_state_receiver(ctx, member.obj.as_ref(), &object_expr) {
return Ok(Expr::Number(value));
@@ -215,6 +242,12 @@ pub(crate) fn lower_member_tail(
},
ast::MemberProp::PrivateName(_) => None,
};
+ // Every constructor function inherits `.constructor` from
+ // Function.prototype. Collapsing `Error.constructor` (and
+ // the equivalent read on another built-in constructor) to
+ // `globalThis.constructor` loses the receiver and returns
+ // undefined instead of the Function constructor.
+ let outer_is_constructor_property = outer_static_member == Some("constructor");
// #4596 follow-up: `Array.isArray` / `Array.from` /
// `Array.of` read as VALUES need the reified Array
// constructor receiver so they resolve to the real native
@@ -377,6 +410,7 @@ pub(crate) fn lower_member_tail(
let receiver_is_regexp_ctor = property == "RegExp";
let receiver_is_function_ctor = property == "Function";
if !outer_is_prototype_or_proto
+ && !outer_is_constructor_property
&& !receiver_is_namespace_value
&& !receiver_is_regexp_ctor
&& !receiver_is_function_ctor
diff --git a/crates/perry-hir/src/lower/expr_new.rs b/crates/perry-hir/src/lower/expr_new.rs
index 4c9835e5d6..afed3542bc 100644
--- a/crates/perry-hir/src/lower/expr_new.rs
+++ b/crates/perry-hir/src/lower/expr_new.rs
@@ -225,6 +225,30 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R
// Try to extract class name from callee
match callee_expr {
ast::Expr::Ident(ident) => {
+ // Hidden dynamic-function constructors reached through
+ // `.constructor` are pre-classified by
+ // `fn_ctor_env`. Their call form already const-folds; construction
+ // must use the same kind-aware path (`new GeneratorFunction()`,
+ // `new AsyncFunction(...)`, and async generators) instead of the
+ // generic object-construction fallback.
+ if ctx.local_decl_scope_depth(ident.sym.as_ref()) == Some(0) {
+ if let Some(super::fn_ctor_env::FnCtorShape::DynCtor(kind)) =
+ ctx.fn_ctor_env.entries.get(ident.sym.as_str()).cloned()
+ {
+ let args = new_expr.args.as_deref().unwrap_or(&[]);
+ if let Some(folded) =
+ super::const_fold_fn::try_const_fold_function_construct_kind(
+ ctx,
+ args,
+ crate::eval_classifier::EvalSurface::NewFunction,
+ new_expr.span,
+ kind,
+ )?
+ {
+ return Ok(folded);
+ }
+ }
+ }
// The inner name of the class currently being lowered is a lexical
// binding that wins over same-named OUTER locals. A nearer method
// parameter/local still shadows it: `class C { static make(C) {
@@ -914,8 +938,25 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R
}
}
}
- if matches!(class_name.as_str(), "Symbol" | "BigInt" | "Math" | "JSON")
- && !shadowed_by_user_binding
+ if matches!(
+ class_name.as_str(),
+ "Symbol"
+ | "BigInt"
+ | "Math"
+ | "JSON"
+ | "Atomics"
+ | "Reflect"
+ | "global"
+ | "decodeURI"
+ | "decodeURIComponent"
+ | "encodeURI"
+ | "encodeURIComponent"
+ | "eval"
+ | "isFinite"
+ | "isNaN"
+ | "parseFloat"
+ | "parseInt"
+ ) && !shadowed_by_user_binding
{
let args = new_expr
.args
diff --git a/crates/perry-hir/src/lower/expr_new/helpers.rs b/crates/perry-hir/src/lower/expr_new/helpers.rs
index a6d160d3a6..f09e5e454d 100644
--- a/crates/perry-hir/src/lower/expr_new/helpers.rs
+++ b/crates/perry-hir/src/lower/expr_new/helpers.rs
@@ -124,7 +124,7 @@ pub(crate) fn nonconstructable_builtin_throw_expr(name: &str, mut args: Vec "js_throw_bigint_constructor_type_error",
"Math" => "js_throw_math_constructor_type_error",
"JSON" => "js_throw_json_constructor_type_error",
- _ => unreachable!(),
+ _ => "js_throw_not_a_constructor",
};
let throw_expr = Expr::Call {
callee: Box::new(Expr::ExternFuncRef {
diff --git a/crates/perry-hir/src/lower/lower_expr/arm_unary.rs b/crates/perry-hir/src/lower/lower_expr/arm_unary.rs
index d0af98105a..848592b200 100644
--- a/crates/perry-hir/src/lower/lower_expr/arm_unary.rs
+++ b/crates/perry-hir/src/lower/lower_expr/arm_unary.rs
@@ -523,6 +523,31 @@ pub(crate) fn lower_unary_expr(ctx: &mut LoweringContext, unary: &ast::UnaryExpr
let prop_name = prop.sym.as_ref();
let is_global =
ctx.lookup_local(obj_name).is_none() && ctx.lookup_func(obj_name).is_none();
+ // Global helper `.length` reads normally fold to a numeric
+ // constant. Under `delete`, preserve the reference so the
+ // configurable synthesized closure slot is actually removed.
+ if is_global
+ && prop_name == "length"
+ && matches!(
+ obj_name,
+ "decodeURI"
+ | "decodeURIComponent"
+ | "encodeURI"
+ | "encodeURIComponent"
+ | "eval"
+ | "isFinite"
+ | "isNaN"
+ | "parseFloat"
+ | "parseInt"
+ )
+ {
+ let object = lower_expr(ctx, member.obj.as_ref())?;
+ return Ok(Expr::Delete(Box::new(Expr::PropertyGet {
+ object: Box::new(object),
+ property: "length".to_string(),
+ byte_offset: member.span.lo.0,
+ })));
+ }
if is_global
&& obj_name == "Number"
&& matches!(
diff --git a/crates/perry-hir/src/lower/lower_module_fn.rs b/crates/perry-hir/src/lower/lower_module_fn.rs
index 10bdb7f8eb..fd076d4760 100644
--- a/crates/perry-hir/src/lower/lower_module_fn.rs
+++ b/crates/perry-hir/src/lower/lower_module_fn.rs
@@ -1225,6 +1225,35 @@ pub fn lower_module_full(
crate::dynamic_import::detect_top_level_await(&mut module);
let is_esm_entry =
!module.imports.is_empty() || !module.exports.is_empty() || module.has_top_level_await;
+ if ctx.is_entry_module && !is_esm_entry && module.references_global_this {
+ let script_var_decl_ids: HashSet<_> = ctx
+ .script_var_decl_names
+ .iter()
+ .filter_map(|name| ctx.lookup_local(name))
+ .collect();
+ // Script `var` bindings are properties of the global object. Insert
+ // the mirror immediately after each top-level initializer so code
+ // later in the same script observes the initialized value through
+ // `globalThis` (ES modules keep their lexical/module binding only).
+ let mut reflected = Vec::with_capacity(module.init.len());
+ for stmt in std::mem::take(&mut module.init) {
+ let global_var = match &stmt {
+ Stmt::Let { id, name, .. } if script_var_decl_ids.contains(id) => {
+ Some((*id, name.clone()))
+ }
+ _ => None,
+ };
+ reflected.push(stmt);
+ if let Some((id, name)) = global_var {
+ reflected.push(Stmt::Expr(Expr::PropertySet {
+ object: Box::new(Expr::GlobalThisExpr),
+ property: name,
+ value: Box::new(Expr::LocalGet(id)),
+ }));
+ }
+ }
+ module.init = reflected;
+ }
if ctx.is_entry_module && !is_esm_entry {
const RESTRICTED_GLOBAL_NAMES: [&str; 3] = ["undefined", "NaN", "Infinity"];
let restricted_scan_stmts: Vec = ast_module
diff --git a/crates/perry-hir/src/lower/stmt_loops.rs b/crates/perry-hir/src/lower/stmt_loops.rs
index 5e0602a4b4..34c1ec31cc 100644
--- a/crates/perry-hir/src/lower/stmt_loops.rs
+++ b/crates/perry-hir/src/lower/stmt_loops.rs
@@ -941,6 +941,12 @@ pub(super) fn lower_stmt_for_of_inner(
method: "iterator".to_string(),
args: vec![],
}
+ } else if for_of_stmt.is_await && is_generator_call && !callee_is_async_gen {
+ // A sync generator used by `for await` must be adapted through
+ // CreateAsyncFromSyncIterator. Awaiting only its raw `next()`
+ // result does not await `result.value`, and therefore neither
+ // preserves a rejected yielded promise nor closes the generator.
+ Expr::GetAsyncIterator(Box::new(iter_expr))
} else {
iter_expr
};
@@ -1042,8 +1048,7 @@ pub(super) fn lower_stmt_for_of_inner(
});
}
}
- // Lower user body statements. lower_stmt appends to module.init,
- // so we snapshot and drain to capture the body stmts.
+ // Lower user body statements. Snapshot module.init and drain body stmts.
// Handle both Block bodies (`for (...) { ... }`) AND single-statement
// bodies (`for (...) console.log(v);`). Pre-fix the brace-less
// form was silently dropped — `for (const v of gen()) doThing(v);`
@@ -1061,6 +1066,7 @@ pub(super) fn lower_stmt_for_of_inner(
|| is_filehandle_readlines_for_await
|| is_fs_dir_for_await
|| is_readline_interface_for_await
+ || (for_of_stmt.is_await && is_generator_call && !callee_is_async_gen)
{
insert_iterator_return_before_abrupts(&mut user_body, iter_id, needs_await);
}
diff --git a/crates/perry-hir/src/lower_decl/body_stmt.rs b/crates/perry-hir/src/lower_decl/body_stmt.rs
index d4a1169071..93ae4f1764 100644
--- a/crates/perry-hir/src/lower_decl/body_stmt.rs
+++ b/crates/perry-hir/src/lower_decl/body_stmt.rs
@@ -1248,6 +1248,11 @@ pub fn lower_body_stmt(ctx: &mut LoweringContext, stmt: &ast::Stmt) -> Result Result *mut Array
arr
}
+/// Descriptor tag bytes for [`js_value_from_const_descriptor`]. MUST match the
+/// serializer in `perry-codegen/src/expr/array_literal.rs`.
+const DESC_NUMBER: u8 = 0; // + 8 bytes little-endian f64
+const DESC_ARRAY: u8 = 1; // + 4 bytes little-endian u32 count, then `count` values
+const DESC_TRUE: u8 = 2;
+const DESC_FALSE: u8 = 3;
+const DESC_NULL: u8 = 4;
+const DESC_UNDEFINED: u8 = 5;
+
+/// #8583 follow-up: materialize a large, fully-CONSTANT array literal from a
+/// static rodata descriptor in ONE call, instead of the N per-subarray
+/// `js_array_from_values` allocations codegen otherwise emits. A minified bundle
+/// data table — a giant nested constant numeric array (the Claude Code bundle's
+/// `__33499`) — lowered to 11,104 allocations and a 245k-instruction body that
+/// made `rewrite-statepoints-for-gc` fan out; this collapses it to one call over
+/// a compact rodata blob.
+///
+/// Returns a FRESH, mutable value each call: JS array literals are mutable, so
+/// the descriptor is a template, never a shared constant. GC is suppressed for
+/// the whole build so the partially-built parent arrays held across nested child
+/// allocations cannot be collected or moved (mirrors `js_json_parse` and the
+/// lazy-array materializer). The blob is compiler-generated and trusted, but
+/// every read is bounds-checked so a malformed descriptor declines to
+/// `undefined` rather than reading out of bounds.
+#[no_mangle]
+pub extern "C" fn js_value_from_const_descriptor(ptr: *const u8, len: u32) -> f64 {
+ if ptr.is_null() || len == 0 {
+ return f64::from_bits(crate::value::JSValue::undefined().bits());
+ }
+ let _suppress = crate::gc::GcSuppressScope::new();
+ let bytes = unsafe { std::slice::from_raw_parts(ptr, len as usize) };
+ let mut pos = 0usize;
+ let bits = build_const_value(bytes, &mut pos);
+ f64::from_bits(bits)
+}
+
+/// Recursively build the JS value at `bytes[*pos]`, advancing `*pos`. Callers
+/// hold GC suppressed, so heap pointers materialized here stay live and pinned
+/// for the duration of the whole build.
+fn build_const_value(bytes: &[u8], pos: &mut usize) -> u64 {
+ use crate::value::JSValue;
+ let undefined = || JSValue::undefined().bits();
+ let Some(&tag) = bytes.get(*pos) else {
+ return undefined();
+ };
+ *pos += 1;
+ match tag {
+ DESC_NUMBER => {
+ if *pos + 8 > bytes.len() {
+ return undefined();
+ }
+ let mut b = [0u8; 8];
+ b.copy_from_slice(&bytes[*pos..*pos + 8]);
+ *pos += 8;
+ JSValue::number(f64::from_le_bytes(b)).bits()
+ }
+ DESC_ARRAY => {
+ if *pos + 4 > bytes.len() {
+ return undefined();
+ }
+ let mut c = [0u8; 4];
+ c.copy_from_slice(&bytes[*pos..*pos + 4]);
+ *pos += 4;
+ let count = u32::from_le_bytes(c);
+ let arr = js_array_alloc_literal(count);
+ // All-number rows keep the raw-f64 layout fast path; any pointer
+ // element (a nested array) is downgraded per-slot by
+ // `store_array_slot`, so gate the numeric mark on a pure-number row.
+ let mut all_number = count > 0;
+ for i in 0..count as usize {
+ if bytes.get(*pos).copied() != Some(DESC_NUMBER) {
+ all_number = false;
+ }
+ let elem = build_const_value(bytes, pos);
+ unsafe { crate::array::store_array_slot(arr, i, elem) };
+ }
+ if all_number {
+ crate::array::js_array_mark_numeric_f64_layout(arr);
+ }
+ JSValue::pointer(arr as *const u8).bits()
+ }
+ DESC_TRUE => JSValue::bool(true).bits(),
+ DESC_FALSE => JSValue::bool(false).bits(),
+ DESC_NULL => JSValue::null().bits(),
+ DESC_UNDEFINED => undefined(),
+ _ => undefined(),
+ }
+}
+
/// Issue #179 Phase 2: if `arr` points at a `LazyArrayHeader`
/// (`GcHeader::obj_type == GC_TYPE_LAZY_ARRAY`), force the lazy
/// value to materialize and return the real `ArrayHeader` pointer.
diff --git a/crates/perry-runtime/src/array/iter_object.rs b/crates/perry-runtime/src/array/iter_object.rs
index 7a41f76b48..201063f0c6 100644
--- a/crates/perry-runtime/src/array/iter_object.rs
+++ b/crates/perry-runtime/src/array/iter_object.rs
@@ -42,6 +42,10 @@ const KIND_ENTRIES: i32 = 2;
/// array iterator yields `value: undefined`), and `return()` terminates
/// the iterator. Produced only by `StatementSync.prototype.iterate()`.
const KIND_VALUES_NULL_DONE: i32 = 3;
+/// Values iterator over a live Arguments exotic object. Unlike an Array
+/// iterator this reads `length` and each indexed property from the Arguments
+/// object on every step, so mutations made before exhaustion are observable.
+const KIND_ARGUMENTS_VALUES: i32 = 4;
/// Clean a NaN-boxed array pointer to a raw `*mut ArrayHeader`, or null.
fn unbox_array_ptr(value: f64) -> *mut ArrayHeader {
@@ -52,21 +56,36 @@ fn unbox_array_ptr(value: f64) -> *mut ArrayHeader {
raw as *mut ArrayHeader
}
-unsafe fn alloc_iterator(arr_ptr: *mut ArrayHeader, kind: i32) -> f64 {
- let obj = js_object_alloc(ARRAY_ITERATOR_CLASS_ID, 3);
+unsafe fn alloc_iterator_backing(backing: f64, kind: i32) -> f64 {
+ let scope = crate::gc::RuntimeHandleScope::new();
+ // The iterator allocation and the lazy prototype bootstrap can both
+ // collect. Keep the incoming backing and the new iterator relocatable.
+ let backing_h = scope.root_nanbox_f64(backing);
+ let obj_h = scope.root_raw_mut_ptr(js_object_alloc(ARRAY_ITERATOR_CLASS_ID, 3));
// Field 0: backing array (NaN-boxed pointer so the GC scanner keeps it).
- let arr_nan = js_nanbox_pointer(arr_ptr as i64);
- js_object_set_field(obj, 0, JSValue::from_bits(arr_nan.to_bits()));
+ obj_h.with_mut_ptr(|obj| {
+ js_object_set_field(
+ obj,
+ 0,
+ JSValue::from_bits(backing_h.get_nanbox_f64().to_bits()),
+ )
+ });
// Field 1: cursor index, starts at 0.
- js_object_set_field(obj, 1, JSValue::number(0.0));
+ obj_h.with_mut_ptr(|obj| js_object_set_field(obj, 1, JSValue::number(0.0)));
// Field 2: iterator kind.
- js_object_set_field(obj, 2, JSValue::number(kind as f64));
+ obj_h.with_mut_ptr(|obj| js_object_set_field(obj, 2, JSValue::number(kind as f64)));
// Link `[[Prototype]]` to the shared `%ArrayIteratorPrototype%` singleton so
// `Object.getPrototypeOf(it)` and the inherited `.next` read resolve.
- crate::object::attach_iterator_prototype(obj, ARRAY_ITERATOR_CLASS_ID);
+ obj_h
+ .with_mut_ptr(|obj| crate::object::attach_iterator_prototype(obj, ARRAY_ITERATOR_CLASS_ID));
+ let (_, obj) = obj_h.across_mut::(|| ());
js_nanbox_pointer(obj as i64)
}
+unsafe fn alloc_iterator(arr_ptr: *mut ArrayHeader, kind: i32) -> f64 {
+ alloc_iterator_backing(js_nanbox_pointer(arr_ptr as i64), kind)
+}
+
/// `arr.values()` iterator — yields each element value.
pub fn array_values_iter(arr_f64: f64) -> f64 {
let arr_ptr = unbox_array_ptr(arr_f64);
@@ -124,6 +143,16 @@ pub fn array_entries_iter(arr_f64: f64) -> f64 {
unsafe { alloc_iterator(arr_ptr, KIND_ENTRIES) }
}
+/// `arguments[Symbol.iterator]()` — a live Array-style values iterator over an
+/// Arguments exotic object. Snapshotting to an Array here loses the specified
+/// expansion/truncation behavior before exhaustion.
+pub fn arguments_values_iter(obj: *const ObjectHeader) -> f64 {
+ if obj.is_null() || !crate::object::is_arguments_object(obj) {
+ return f64::from_bits(TAG_UNDEFINED);
+ }
+ unsafe { alloc_iterator_backing(js_nanbox_pointer(obj as i64), KIND_ARGUMENTS_VALUES) }
+}
+
// ---------------------------------------------------------------------------
// #2384: C-ABI entry points for codegen's `Expr::ArrayValues`/`ArrayKeys`/
// `ArrayEntries` fast path. These build a real `.next()`-bearing iterator
@@ -607,6 +636,11 @@ pub unsafe fn dispatch_array_iterator_method(
};
match method_name {
"next" => {
+ if let Some(result) =
+ crate::object::call_overridden_iterator_next(iter_obj(), ARRAY_ITERATOR_CLASS_ID)
+ {
+ return result;
+ }
if kind == KIND_VALUES_NULL_DONE {
let epoch_ptr = js_nanbox_get_pointer(f64::from_bits(
js_object_get_field(iter_obj(), 3).bits(),
@@ -632,15 +666,17 @@ pub unsafe fn dispatch_array_iterator_method(
}
return make_iter_result(done_value(), true);
}
- let arr_ptr = js_nanbox_get_pointer(backing_f64) as *const ArrayHeader;
+ let backing_ptr = js_nanbox_get_pointer(backing_f64);
// Field 1: current index.
let idx_field = js_object_get_field(iter_obj(), 1);
let idx = f64::from_bits(idx_field.bits()) as u32;
- let len = if arr_ptr.is_null() {
- 0u32
+ let len = if kind == KIND_ARGUMENTS_VALUES {
+ crate::object::arguments_object_length(backing_ptr as *const ObjectHeader)
+ } else if backing_ptr == 0 {
+ 0
} else {
- crate::array::js_array_length(arr_ptr)
+ crate::array::js_array_length(backing_ptr as *const ArrayHeader)
};
if idx >= len {
@@ -662,17 +698,21 @@ pub unsafe fn dispatch_array_iterator_method(
// field 0, which the collector DOES rewrite, instead of reusing
// the pre-store copy. `iter_obj()` re-reads the iterator's own
// address from its root for the same reason.
- let arr_ptr =
+ let backing_ptr =
js_nanbox_get_pointer(f64::from_bits(js_object_get_field(iter_obj(), 0).bits()))
- as *const ArrayHeader;
- let elem = if arr_ptr.is_null() {
+ as usize;
+ let elem = if kind == KIND_ARGUMENTS_VALUES {
+ crate::object::arguments_object_index_value(backing_ptr as *const ObjectHeader, idx)
+ } else if backing_ptr == 0 {
f64::from_bits(TAG_UNDEFINED)
} else {
- crate::array::js_array_get_f64(arr_ptr, idx)
+ crate::array::js_array_get_f64(backing_ptr as *const ArrayHeader, idx)
};
let value = match kind {
- KIND_VALUES | KIND_VALUES_NULL_DONE => JSValue::from_bits(elem.to_bits()),
+ KIND_VALUES | KIND_VALUES_NULL_DONE | KIND_ARGUMENTS_VALUES => {
+ JSValue::from_bits(elem.to_bits())
+ }
KIND_KEYS => JSValue::number(idx as f64),
KIND_ENTRIES => {
let pair = make_pair_array(idx, elem);
diff --git a/crates/perry-runtime/src/array/iterator.rs b/crates/perry-runtime/src/array/iterator.rs
index 6b4c51b099..296e9d883c 100644
--- a/crates/perry-runtime/src/array/iterator.rs
+++ b/crates/perry-runtime/src/array/iterator.rs
@@ -348,7 +348,16 @@ fn async_from_sync_continue(iter: f64, step_result: f64, close_on_rejection: boo
if close_on_rejection { 1.0 } else { 0.0 },
);
- let value_promise = crate::promise::js_promise_resolved(value);
+ let value_promise = match crate::promise::js_promise_resolved_catching(value) {
+ Ok(promise) => promise,
+ Err(reason) => {
+ if close_on_rejection {
+ async_from_sync_close(iter);
+ }
+ crate::promise::js_promise_reject(outer, reason);
+ return boxed_promise_value(outer);
+ }
+ };
crate::promise::js_promise_then(value_promise, on_fulfilled, on_rejected);
boxed_promise_value(outer)
}
diff --git a/crates/perry-runtime/src/array/mod.rs b/crates/perry-runtime/src/array/mod.rs
index 0decd10c9a..39004ced86 100644
--- a/crates/perry-runtime/src/array/mod.rs
+++ b/crates/perry-runtime/src/array/mod.rs
@@ -140,9 +140,9 @@ pub use self::iter_methods::{
js_validate_array_callback, js_validate_array_map_callback,
};
pub use self::iter_object::{
- array_entries_iter, array_keys_iter, array_values_iter, array_values_iter_null_done,
- dispatch_array_iterator_method, js_array_entries_iter_obj, js_array_keys_iter_obj,
- js_array_values_iter_obj, ARRAY_ITERATOR_CLASS_ID,
+ arguments_values_iter, array_entries_iter, array_keys_iter, array_values_iter,
+ array_values_iter_null_done, dispatch_array_iterator_method, js_array_entries_iter_obj,
+ js_array_keys_iter_obj, js_array_values_iter_obj, ARRAY_ITERATOR_CLASS_ID,
};
pub(crate) use self::iterator::is_builtin_iterator_class_id;
pub(crate) use self::iterator::iter_bt_dump;
diff --git a/crates/perry-runtime/src/array/push_pop.rs b/crates/perry-runtime/src/array/push_pop.rs
index 183714dde3..a86a4c6748 100644
--- a/crates/perry-runtime/src/array/push_pop.rs
+++ b/crates/perry-runtime/src/array/push_pop.rs
@@ -1,6 +1,5 @@
//! push / pop / shift / unshift / set_length / delete + grow primitive.
use super::*;
-use crate::arena::arena_alloc_gc;
use std::ptr;
/// `pop`/`shift`/`push`/`unshift` on a frozen array perform a `Set`/`Delete`
@@ -133,8 +132,38 @@ pub extern "C" fn js_array_grow(arr: *mut ArrayHeader, min_capacity: u32) -> *mu
let old_size = array_byte_size(old_capacity as usize);
let new_size = array_byte_size(new_capacity as usize);
- // Allocate new from arena and copy old data.
- let new_ptr = arena_alloc_gc(new_size, 8, crate::gc::GC_TYPE_ARRAY) as *mut ArrayHeader;
+ // A growth stub outlives the array operation: aliases can keep its
+ // address and `clean_arr_ptr` follows it on a later access. Therefore
+ // a non-moving source must not forward into the copying nursery. A
+ // minor does not trace a retained `GC_FLAG_FORWARDED` stub as a normal
+ // array object, so its payload forwarding word is outside ordinary
+ // layout and remembered-set scanning. It would neither move nor retain
+ // a young target; resetting from-space would leave the permanent old
+ // stub pointing at recycled bytes.
+ //
+ // For a young source, use the nursery only when the already-open block
+ // can satisfy the grow without collecting. If that allocation would
+ // collect, the source may be promoted while its handle is reloaded;
+ // birth the target old instead so the post-collection source cannot
+ // acquire the same old->young forwarding edge.
+ let old_header =
+ (arr as *mut u8).sub(crate::gc::GC_HEADER_SIZE) as *mut crate::gc::GcHeader;
+ let source_requires_old_target = (*old_header).gc_flags & crate::gc::GC_FLAG_TENURED != 0
+ || !matches!(
+ crate::arena::classify_heap_generation(arr as usize),
+ crate::arena::HeapGeneration::Nursery
+ );
+ let new_ptr = if source_requires_old_target {
+ crate::arena::arena_alloc_gc_old_born_tenured(new_size, 8, crate::gc::GC_TYPE_ARRAY)
+ } else {
+ let young =
+ crate::arena::arena_alloc_gc_no_collect(new_size, 8, crate::gc::GC_TYPE_ARRAY);
+ if young.is_null() {
+ crate::arena::arena_alloc_gc_old_born_tenured(new_size, 8, crate::gc::GC_TYPE_ARRAY)
+ } else {
+ young
+ }
+ } as *mut ArrayHeader;
let arr = arr_handle.get_raw_mut_ptr::();
// GC_STORE_AUDIT(BARRIERED): array growth copy transfers layout and replays write barriers below.
ptr::copy_nonoverlapping(arr as *const u8, new_ptr as *mut u8, old_size);
diff --git a/crates/perry-runtime/src/array/tests.rs b/crates/perry-runtime/src/array/tests.rs
index f77a7ce8ed..dffd1d87c0 100644
--- a/crates/perry-runtime/src/array/tests.rs
+++ b/crates/perry-runtime/src/array/tests.rs
@@ -590,6 +590,43 @@ fn stale_array_reference_survives_three_growths_and_forced_minor_gc() {
}
}
+#[test]
+fn growth_of_old_array_keeps_forwarding_target_out_of_copying_nursery() {
+ let _triggers = crate::gc::GcTriggerThresholdTestGuard::suppress_automatic_triggers();
+ let capacity = MIN_ARRAY_CAPACITY;
+ let initial = crate::arena::arena_alloc_gc_old_born_tenured(
+ array_byte_size(capacity as usize),
+ 8,
+ crate::gc::GC_TYPE_ARRAY,
+ ) as *mut ArrayHeader;
+
+ unsafe {
+ (*initial).length = 0;
+ (*initial).capacity = capacity;
+ let elements = (initial as *mut u8).add(std::mem::size_of::()) as *mut u64;
+ for i in 0..capacity as usize {
+ // GC_STORE_AUDIT(INIT): initialize unpublished fresh array storage
+ // with the non-pointer hole sentinel before exposing the array.
+ ptr::write(elements.add(i), crate::value::TAG_HOLE);
+ }
+ set_array_numeric_layout(initial, NumericArrayLayout::RawF64);
+ crate::gc::layout_init_pointer_free(initial as *mut u8);
+ }
+
+ let mut head = initial;
+ for i in 0..=capacity {
+ head = js_array_push_f64(head, i as f64);
+ }
+
+ assert_ne!(head, initial, "the capacity-crossing push must grow");
+ assert_eq!(clean_arr_ptr_mut(initial), head);
+ assert_eq!(
+ crate::arena::classify_heap_generation(head as usize),
+ crate::arena::HeapGeneration::Old,
+ "an old forwarding stub must not point into resetting copying-nursery space"
+ );
+}
+
#[test]
fn install_array_growth_forwarding_with_installs_stub_for_injected_header() {
// Actual low-address classification is covered by
diff --git a/crates/perry-runtime/src/buffer/dataview.rs b/crates/perry-runtime/src/buffer/dataview.rs
index 46241ce3d6..71727b2d24 100644
--- a/crates/perry-runtime/src/buffer/dataview.rs
+++ b/crates/perry-runtime/src/buffer/dataview.rs
@@ -286,7 +286,8 @@ pub fn js_data_view_set(
kind: DataViewKind,
little: bool,
) -> f64 {
- let buf = unbox_buffer_ptr(buf_f64.to_bits()) as *mut BufferHeader;
+ let scope = crate::gc::RuntimeHandleScope::new();
+ let buf_handle = scope.root_nanbox_f64(buf_f64);
let offset = to_byte_offset(offset_value);
if kind.is_bigint() {
// SetViewValue for a BigInt accessor: `ToBigInt(value)` (a Number throws
@@ -298,10 +299,12 @@ pub fn js_data_view_set(
} else {
raw.to_be_bytes()
};
+ let buf = unbox_buffer_ptr(buf_handle.get_nanbox_f64().to_bits()) as *mut BufferHeader;
unsafe { write_bytes(buf, offset, &b) };
return f64::from_bits(crate::value::TAG_UNDEFINED);
}
let n = to_number(value);
+ let buf = unbox_buffer_ptr(buf_handle.get_nanbox_f64().to_bits()) as *mut BufferHeader;
unsafe {
match kind {
DataViewKind::BigInt64 | DataViewKind::BigUint64 => unreachable!(),
@@ -486,46 +489,15 @@ fn wrap_to_u64(n: f64, bits: u32) -> u64 {
/// convertible and throw a `TypeError`; BigInt passes through, Boolean and
/// String coerce.
fn to_bigint_raw_or_throw(value: f64) -> u64 {
- use crate::value::JSValue;
- let jsval = JSValue::from_bits(value.to_bits());
- let bi: *const crate::bigint::BigIntHeader = if jsval.is_bigint() {
- jsval.as_bigint_ptr() as *const crate::bigint::BigIntHeader
- } else if jsval.is_bool() {
- crate::bigint::js_bigint_from_i64(if jsval.as_bool() { 1 } else { 0 })
- } else if jsval.is_any_string() {
- // StringToBigInt (a malformed numeric string throws SyntaxError).
- crate::bigint::js_bigint_from_f64(value)
- } else {
- throw_bigint_conversion_type_error(value);
- };
+ // Reuse the typed-array lane's complete ToBigInt implementation. In
+ // particular, object values must run @@toPrimitive/valueOf/toString and
+ // propagate abrupt completion before the DataView bounds check.
+ let coerced = crate::typedarray::bigint::to_bigint_for_store(value);
+ let bi = crate::value::JSValue::from_bits(coerced.to_bits()).as_bigint_ptr()
+ as *const crate::bigint::BigIntHeader;
let bi = crate::bigint::clean_bigint_ptr(bi);
if bi.is_null() {
return 0;
}
unsafe { (*bi).limbs[0] }
}
-
-/// Throw `TypeError: Cannot convert to a BigInt`, matching Node's
-/// `ToBigInt` rejection text for a DataView BigInt setter.
-#[cold]
-fn throw_bigint_conversion_type_error(value: f64) -> ! {
- use crate::value::JSValue;
- let jsval = JSValue::from_bits(value.to_bits());
- let label = if jsval.is_undefined() {
- "undefined".to_string()
- } else if jsval.is_null() {
- "null".to_string()
- } else if unsafe { crate::symbol::js_is_symbol(value) } != 0 {
- "a Symbol value".to_string()
- } else if jsval.is_int32() {
- jsval.as_int32().to_string()
- } else {
- format!("{value}")
- };
- let msg = format!("Cannot convert {label} to a BigInt");
- let err = crate::error::js_typeerror_new(crate::string::js_string_from_bytes(
- msg.as_ptr(),
- msg.len() as u32,
- ));
- crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64))
-}
diff --git a/crates/perry-runtime/src/buffer/from.rs b/crates/perry-runtime/src/buffer/from.rs
index 21fff8c32d..7ae68a0981 100644
--- a/crates/perry-runtime/src/buffer/from.rs
+++ b/crates/perry-runtime/src/buffer/from.rs
@@ -898,7 +898,7 @@ pub extern "C" fn js_data_view_new(value: f64, offset_value: f64, length_value:
throw_dataview_buffer_not_object();
}
let addr = v.as_pointer::() as usize;
- if addr == 0 || !is_registered_buffer(addr) {
+ if addr == 0 || !is_registered_buffer(addr) || !is_any_array_buffer(addr) {
throw_dataview_buffer_not_object();
}
// Steps 4-6: offset = ToIndex(byteOffset) (RangeError if negative). Runs
diff --git a/crates/perry-runtime/src/buffer/mod.rs b/crates/perry-runtime/src/buffer/mod.rs
index 6d67e40250..4b3076ae1e 100644
--- a/crates/perry-runtime/src/buffer/mod.rs
+++ b/crates/perry-runtime/src/buffer/mod.rs
@@ -71,8 +71,9 @@ pub(crate) use detach::{array_buffer_transfer, detach_array_buffer};
#[cfg(test)]
pub(crate) use own_props::test_buffer_own_props_owner_count;
pub use own_props::{
- buffer_get_own_prop, buffer_has_own_prop, buffer_own_prop_names, buffer_own_props_possible,
- buffer_set_own_prop, clear_buffer_own_props, scan_buffer_own_props_roots_mut,
+ buffer_define_own_data_prop, buffer_delete_own_prop, buffer_get_own_prop, buffer_has_own_prop,
+ buffer_own_prop_names, buffer_own_props_possible, buffer_set_own_prop, clear_buffer_own_props,
+ scan_buffer_own_props_roots_mut,
};
// ---- Re-exports: #8149 integer-indexed-exotic discrimination ----
diff --git a/crates/perry-runtime/src/buffer/own_props.rs b/crates/perry-runtime/src/buffer/own_props.rs
index 703f16208e..bf04295c8e 100644
--- a/crates/perry-runtime/src/buffer/own_props.rs
+++ b/crates/perry-runtime/src/buffer/own_props.rs
@@ -52,6 +52,32 @@ pub fn buffer_own_props_possible() -> bool {
/// Store `buf. = value`. Only reached for a registered buffer address.
pub fn buffer_set_own_prop(addr: usize, prop: &str, value: f64) {
+ if addr == 0 {
+ return;
+ }
+ if let Some(accessor) = crate::object::get_accessor_descriptor(addr, prop) {
+ if accessor.set != 0 {
+ unsafe {
+ crate::object::invoke_accessor_setter(
+ accessor.set,
+ crate::value::js_nanbox_pointer(addr as i64),
+ value,
+ );
+ }
+ }
+ return;
+ }
+ if crate::object::get_property_attrs(addr, prop).is_some_and(|attrs| !attrs.writable())
+ && buffer_get_own_prop(addr, prop).is_some()
+ {
+ return;
+ }
+ buffer_define_own_data_prop(addr, prop, value);
+}
+
+/// Descriptor installation bypasses ordinary [[Set]] interception after it
+/// has validated the redefinition and selected the new property kind.
+pub fn buffer_define_own_data_prop(addr: usize, prop: &str, value: f64) {
if addr == 0 {
return;
}
@@ -104,6 +130,28 @@ pub fn buffer_own_prop_names(addr: usize) -> Vec {
/// Whether the buffer carries any own dynamic prop under `prop`.
pub fn buffer_has_own_prop(addr: usize, prop: &str) -> bool {
buffer_get_own_prop(addr, prop).is_some()
+ || crate::object::get_accessor_descriptor(addr, prop).is_some()
+}
+
+/// Delete an ordinary named own property from a registered buffer/view.
+/// Returns whether the property was present.
+pub fn buffer_delete_own_prop(addr: usize, prop: &str) -> bool {
+ if addr == 0 || !buffer_own_props_possible() {
+ return false;
+ }
+ let Ok(mut props) = buffer_props().lock() else {
+ return false;
+ };
+ let Some(entries) = props.get_mut(&addr) else {
+ return false;
+ };
+ let removed = entries.remove(prop).is_some();
+ crate::object::clear_accessor_descriptor(addr, prop);
+ crate::object::clear_property_attrs(addr, prop);
+ if entries.is_empty() {
+ props.remove(&addr);
+ }
+ removed
}
/// GC: trace stored values in every phase (a stored closure is reachable ONLY
diff --git a/crates/perry-runtime/src/builtins/arithmetic.rs b/crates/perry-runtime/src/builtins/arithmetic.rs
index 3f11e3d02b..810ce20e5b 100644
--- a/crates/perry-runtime/src/builtins/arithmetic.rs
+++ b/crates/perry-runtime/src/builtins/arithmetic.rs
@@ -111,15 +111,6 @@ pub extern "C" fn js_loose_eq(a: JSValue, b: JSValue) -> JSValue {
if eq_is_object(a) && eq_is_object(b) {
return JSValue::bool(false);
}
- // Boxed primitives compare via their wrapped primitive value under
- // abstract equality (`new Number(5) == 5`, and sloppy primitive accessors
- // return boxed receivers).
- if let Some((_, payload)) = boxed_primitive_payload(f64::from_bits(a.bits())) {
- return js_loose_eq(JSValue::from_bits(payload.to_bits()), b);
- }
- if let Some((_, payload)) = boxed_primitive_payload(f64::from_bits(b.bits())) {
- return js_loose_eq(a, JSValue::from_bits(payload.to_bits()));
- }
// Object == primitive → ToPrimitive(object), then retry (ES2024 §7.2.15
// steps 10-11). Object-vs-object was settled above; symbols are primitives
// (`eq_is_object` excludes them) and correctly fall through to not-equal.
@@ -150,8 +141,8 @@ pub extern "C" fn js_loose_eq(a: JSValue, b: JSValue) -> JSValue {
);
}
// BigInt abstract equality (ES2024 §7.2.15). Neither side is
- // null/undefined here and boxed wrappers (incl. `Object(0n)`) have already
- // been unwrapped above.
+ // null/undefined here and boxed wrappers have already gone through the
+ // observable ToPrimitive operation above.
if a.is_bigint() || b.is_bigint() {
// BigInt == BigInt → compare by mathematical value.
if a.is_bigint() && b.is_bigint() {
diff --git a/crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs b/crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs
index c62f976c1c..5b760d0ea7 100644
--- a/crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs
+++ b/crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs
@@ -374,9 +374,16 @@ pub extern "C" fn js_boxed_bigint_new(value: f64) -> f64 {
#[no_mangle]
pub extern "C" fn js_boxed_symbol_new(value: f64) -> f64 {
- let obj = crate::object::js_object_alloc(CLASS_ID_BOXED_SYMBOL, 0);
- register_boxed_primitive_payload(obj, value);
- attach_boxed_primitive_prototype(obj, CLASS_ID_BOXED_SYMBOL);
+ let scope = crate::gc::RuntimeHandleScope::new();
+ let value = scope.root_nanbox_f64(value);
+ let obj = scope.root_raw_mut_ptr(crate::object::js_object_alloc(CLASS_ID_BOXED_SYMBOL, 0));
+ obj.with_mut_ptr::(|obj| {
+ register_boxed_primitive_payload(obj, value.get_nanbox_f64())
+ });
+ obj.with_mut_ptr::(|obj| {
+ attach_boxed_primitive_prototype(obj, CLASS_ID_BOXED_SYMBOL)
+ });
+ let (_, obj) = obj.across_mut::(|| ());
crate::value::js_nanbox_pointer(obj as i64)
}
diff --git a/crates/perry-runtime/src/builtins/numbers.rs b/crates/perry-runtime/src/builtins/numbers.rs
index 412f4cc49d..d5678dc6f8 100644
--- a/crates/perry-runtime/src/builtins/numbers.rs
+++ b/crates/perry-runtime/src/builtins/numbers.rs
@@ -21,7 +21,15 @@ pub extern "C" fn js_parse_int(str_ptr: *const StringHeader, radix: f64) -> f64
let data = (str_ptr as *const u8).add(std::mem::size_of::());
let bytes = std::slice::from_raw_parts(data, len);
- if let Ok(s) = std::str::from_utf8(bytes) {
+ // Perry stores lone UTF-16 surrogates as WTF-8. `parseInt` only
+ // inspects the leading digit run, so an invalid UTF-8 sequence after
+ // that run is a terminator, not a reason to reject the whole string.
+ // Decode the valid prefix in that case (`"1Z\uD800"`, radix 36, is
+ // still 71), matching the spec's longest-prefix rule.
+ let valid_len = std::str::from_utf8(bytes)
+ .map(|_| bytes.len())
+ .unwrap_or_else(|err| err.valid_up_to());
+ if let Ok(s) = std::str::from_utf8(&bytes[..valid_len]) {
// StrWhiteSpace per spec (NBSP/BOM in, NEL out) — not Rust's
// `trim_start`, whose White_Space set diverges from JS's.
let trimmed = s.trim_start_matches(crate::string::is_js_whitespace);
diff --git a/crates/perry-runtime/src/collection_iter_object.rs b/crates/perry-runtime/src/collection_iter_object.rs
index a90e58489a..839513b8cc 100644
--- a/crates/perry-runtime/src/collection_iter_object.rs
+++ b/crates/perry-runtime/src/collection_iter_object.rs
@@ -17,6 +17,7 @@
//! next to the array iterator one; `flat_clone.rs` detects the class id so
//! `[...m.entries()]` / `Array.from(s.values())` drive `.next()`.
+use crate::array::ArrayHeader;
use crate::map::MapHeader;
use crate::object::{js_object_alloc, js_object_get_field, js_object_set_field, ObjectHeader};
use crate::set::SetHeader;
@@ -58,26 +59,33 @@ fn iterator_class_id(addr: usize) -> Option {
}
unsafe fn alloc_iterator(class_id: u32, coll_nanboxed: f64, kind: i32) -> f64 {
- let obj = js_object_alloc(class_id, 5);
+ let scope = crate::gc::RuntimeHandleScope::new();
+ let coll_h = scope.root_nanbox_f64(coll_nanboxed);
+ let obj_h = scope.root_raw_mut_ptr(js_object_alloc(class_id, 5));
+ let obj = || obj_h.across_mut::(|| ()).1;
// Field 0: backing collection (NaN-boxed pointer so the GC scanner keeps it).
- js_object_set_field(obj, 0, JSValue::from_bits(coll_nanboxed.to_bits()));
+ js_object_set_field(
+ obj(),
+ 0,
+ JSValue::from_bits(coll_h.get_nanbox_f64().to_bits()),
+ );
// Field 1: cursor index (index just past the last-returned entry), starts at 0.
- js_object_set_field(obj, 1, JSValue::number(0.0));
+ js_object_set_field(obj(), 1, JSValue::number(0.0));
// Field 2: iterator kind.
- js_object_set_field(obj, 2, JSValue::number(kind as f64));
+ js_object_set_field(obj(), 2, JSValue::number(kind as f64));
// Field 3: collection size observed at the last `next()`. `-1` sentinel means
// "not started" (no entry returned yet). Used to detect a mid-iteration
// delete (which compacts the entries array, shifting live entries below the
// cursor) so the cursor can be re-derived from the last key (#6075).
- js_object_set_field(obj, 3, JSValue::number(-1.0));
+ js_object_set_field(obj(), 3, JSValue::number(-1.0));
// Field 4: the KEY of the last-returned entry (a Map key / Set value), used
// to re-derive the cursor after a delete-shift. Undefined until started.
- js_object_set_field(obj, 4, JSValue::undefined());
+ js_object_set_field(obj(), 4, JSValue::undefined());
// Link `[[Prototype]]` to the shared `%MapIteratorPrototype%` /
// `%SetIteratorPrototype%` singleton so `Object.getPrototypeOf(it)` and the
// inherited `.next` read resolve.
- crate::object::attach_iterator_prototype(obj, class_id);
- js_nanbox_pointer(obj as i64)
+ crate::object::attach_iterator_prototype(obj(), class_id);
+ js_nanbox_pointer(obj() as i64)
}
/// Build a fresh Map iterator object for `map` (raw pointer) of the given
@@ -178,11 +186,17 @@ use crate::iter_result::make_iter_result;
/// `[key, value]` pair array for Map entries / Set entries (`[v, v]`).
unsafe fn make_pair_array(a: f64, b: f64) -> f64 {
- let pair = crate::array::js_array_alloc(2);
- crate::array::store_array_slot(pair, 0, a.to_bits());
- crate::array::store_array_slot(pair, 1, b.to_bits());
- (*pair).length = 2;
- crate::array::rebuild_array_layout_exact(pair);
+ let scope = crate::gc::RuntimeHandleScope::new();
+ let a = scope.root_nanbox_f64(a);
+ let b = scope.root_nanbox_f64(b);
+ let pair = scope.root_raw_mut_ptr(crate::array::js_array_alloc(2));
+ pair.with_mut_ptr::(|pair| {
+ crate::array::store_array_slot(pair, 0, a.get_nanbox_u64());
+ crate::array::store_array_slot(pair, 1, b.get_nanbox_u64());
+ (*pair).length = 2;
+ crate::array::rebuild_array_layout_exact(pair);
+ });
+ let (_, pair) = pair.across_mut::(|| ());
js_nanbox_pointer(pair as i64)
}
@@ -219,50 +233,62 @@ fn next_read_index(cursor: u32, last_key_in_place: bool, find_last: impl FnOnce(
/// Dispatch `.next()` / `[Symbol.iterator]()` on a Map iterator object.
pub unsafe fn dispatch_map_iterator_method(iter_obj: *mut ObjectHeader, method_name: &str) -> f64 {
+ let scope = crate::gc::RuntimeHandleScope::new();
+ let iter_h = scope.root_nanbox_f64(js_nanbox_pointer(iter_obj as i64));
+ let iter_obj = || js_nanbox_get_pointer(iter_h.get_nanbox_f64()) as *mut ObjectHeader;
match method_name {
"next" => {
- let backing = f64::from_bits(js_object_get_field(iter_obj, 0).bits());
- let map = js_nanbox_get_pointer(backing) as *const MapHeader;
- let kind = f64::from_bits(js_object_get_field(iter_obj, 2).bits()) as i32;
- if map.is_null() {
+ if let Some(result) =
+ crate::object::call_overridden_iterator_next(iter_obj(), MAP_ITERATOR_CLASS_ID)
+ {
+ return result;
+ }
+ let backing = f64::from_bits(js_object_get_field(iter_obj(), 0).bits());
+ let map_h = scope.root_nanbox_f64(backing);
+ let map = || js_nanbox_get_pointer(map_h.get_nanbox_f64()) as *const MapHeader;
+ let kind = f64::from_bits(js_object_get_field(iter_obj(), 2).bits()) as i32;
+ if map().is_null() {
return make_iter_result(JSValue::undefined(), true);
}
- let cursor = f64::from_bits(js_object_get_field(iter_obj, 1).bits()) as u32;
- let last_key = js_object_get_field(iter_obj, 4);
- let size = crate::map::js_map_size(map);
+ let cursor = f64::from_bits(js_object_get_field(iter_obj(), 1).bits()) as u32;
+ let last_key = js_object_get_field(iter_obj(), 4);
+ let size = crate::map::js_map_size(map());
// Is the last-returned key still at cursor-1? (SameValueZero, so a
// NaN key matches itself.) If so, no delete shifted an entry at/below
// the cursor.
let in_place = cursor > 0 && {
- let prev = crate::map::js_map_entry_key_at(map, cursor - 1);
+ let prev = crate::map::js_map_entry_key_at(map(), cursor - 1);
crate::value::js_jsvalue_same_value_zero(prev, f64::from_bits(last_key.bits())) != 0
};
let idx = next_read_index(cursor, in_place, || {
- crate::map::find_key_index(map, f64::from_bits(last_key.bits()))
+ crate::map::find_key_index(map(), f64::from_bits(last_key.bits()))
});
if idx >= size {
- js_object_set_field(iter_obj, 1, JSValue::number(size as f64));
+ js_object_set_field(iter_obj(), 1, JSValue::number(size as f64));
+ // Once a collection iterator is exhausted it stays exhausted,
+ // even if entries are appended later.
+ js_object_set_field(iter_obj(), 0, JSValue::undefined());
return make_iter_result(JSValue::undefined(), true);
}
- let entry_key = crate::map::js_map_entry_key_at(map, idx);
+ let entry_key = crate::map::js_map_entry_key_at(map(), idx);
// Record state for the next re-derive BEFORE any allocation below.
- js_object_set_field(iter_obj, 1, JSValue::number((idx + 1) as f64));
- js_object_set_field(iter_obj, 4, JSValue::from_bits(entry_key.to_bits()));
+ js_object_set_field(iter_obj(), 1, JSValue::number((idx + 1) as f64));
+ js_object_set_field(iter_obj(), 4, JSValue::from_bits(entry_key.to_bits()));
let value = match kind {
KIND_KEYS => JSValue::from_bits(entry_key.to_bits()),
KIND_VALUES => {
- JSValue::from_bits(crate::map::js_map_entry_value_at(map, idx).to_bits())
+ JSValue::from_bits(crate::map::js_map_entry_value_at(map(), idx).to_bits())
}
_ => {
- let val = crate::map::js_map_entry_value_at(map, idx);
+ let val = crate::map::js_map_entry_value_at(map(), idx);
JSValue::from_bits(make_pair_array(entry_key, val).to_bits())
}
};
make_iter_result(value, false)
}
- "Symbol.iterator" | "@@iterator" => js_nanbox_pointer(iter_obj as i64),
+ "Symbol.iterator" | "@@iterator" => js_nanbox_pointer(iter_obj() as i64),
"return" | "throw" => make_iter_result(JSValue::undefined(), true),
_ => f64::from_bits(TAG_UNDEFINED),
}
@@ -270,32 +296,42 @@ pub unsafe fn dispatch_map_iterator_method(iter_obj: *mut ObjectHeader, method_n
/// Dispatch `.next()` / `[Symbol.iterator]()` on a Set iterator object.
pub unsafe fn dispatch_set_iterator_method(iter_obj: *mut ObjectHeader, method_name: &str) -> f64 {
+ let scope = crate::gc::RuntimeHandleScope::new();
+ let iter_h = scope.root_nanbox_f64(js_nanbox_pointer(iter_obj as i64));
+ let iter_obj = || js_nanbox_get_pointer(iter_h.get_nanbox_f64()) as *mut ObjectHeader;
match method_name {
"next" => {
- let backing = f64::from_bits(js_object_get_field(iter_obj, 0).bits());
- let set = js_nanbox_get_pointer(backing) as *const SetHeader;
- let kind = f64::from_bits(js_object_get_field(iter_obj, 2).bits()) as i32;
- if set.is_null() {
+ if let Some(result) =
+ crate::object::call_overridden_iterator_next(iter_obj(), SET_ITERATOR_CLASS_ID)
+ {
+ return result;
+ }
+ let backing = f64::from_bits(js_object_get_field(iter_obj(), 0).bits());
+ let set_h = scope.root_nanbox_f64(backing);
+ let set = || js_nanbox_get_pointer(set_h.get_nanbox_f64()) as *const SetHeader;
+ let kind = f64::from_bits(js_object_get_field(iter_obj(), 2).bits()) as i32;
+ if set().is_null() {
return make_iter_result(JSValue::undefined(), true);
}
- let cursor = f64::from_bits(js_object_get_field(iter_obj, 1).bits()) as u32;
- let last_val = js_object_get_field(iter_obj, 4);
- let size = crate::set::js_set_size(set);
+ let cursor = f64::from_bits(js_object_get_field(iter_obj(), 1).bits()) as u32;
+ let last_val = js_object_get_field(iter_obj(), 4);
+ let size = crate::set::js_set_size(set());
let in_place = cursor > 0 && {
- let prev = crate::set::js_set_value_at(set, cursor - 1);
+ let prev = crate::set::js_set_value_at(set(), cursor - 1);
crate::value::js_jsvalue_same_value_zero(prev, f64::from_bits(last_val.bits())) != 0
};
let idx = next_read_index(cursor, in_place, || {
- crate::set::find_value_index(set, f64::from_bits(last_val.bits()))
+ crate::set::find_value_index(set(), f64::from_bits(last_val.bits()))
});
if idx >= size {
- js_object_set_field(iter_obj, 1, JSValue::number(size as f64));
+ js_object_set_field(iter_obj(), 1, JSValue::number(size as f64));
+ js_object_set_field(iter_obj(), 0, JSValue::undefined());
return make_iter_result(JSValue::undefined(), true);
}
- let elem = crate::set::js_set_value_at(set, idx);
- js_object_set_field(iter_obj, 1, JSValue::number((idx + 1) as f64));
- js_object_set_field(iter_obj, 4, JSValue::from_bits(elem.to_bits()));
+ let elem = crate::set::js_set_value_at(set(), idx);
+ js_object_set_field(iter_obj(), 1, JSValue::number((idx + 1) as f64));
+ js_object_set_field(iter_obj(), 4, JSValue::from_bits(elem.to_bits()));
let value = match kind {
// For Sets, keys === values; entries yields [v, v] pairs.
@@ -304,7 +340,7 @@ pub unsafe fn dispatch_set_iterator_method(iter_obj: *mut ObjectHeader, method_n
};
make_iter_result(value, false)
}
- "Symbol.iterator" | "@@iterator" => js_nanbox_pointer(iter_obj as i64),
+ "Symbol.iterator" | "@@iterator" => js_nanbox_pointer(iter_obj() as i64),
"return" | "throw" => make_iter_result(JSValue::undefined(), true),
_ => f64::from_bits(TAG_UNDEFINED),
}
diff --git a/crates/perry-runtime/src/error.rs b/crates/perry-runtime/src/error.rs
index 516e691a8d..4550d0cc3c 100644
--- a/crates/perry-runtime/src/error.rs
+++ b/crates/perry-runtime/src/error.rs
@@ -822,6 +822,20 @@ pub(crate) unsafe fn js_error_has_own_property(error: *mut ErrorHeader, key: &st
}
}
+/// Clear an Error instance's spec-visible own-slot presence after a
+/// successful configurable-property deletion.
+pub(crate) unsafe fn js_error_delete_builtin_own_property(error: *mut ErrorHeader, key: &str) {
+ if error.is_null() {
+ return;
+ }
+ match key {
+ "message" => (*error).flags &= !ERROR_FLAG_HAS_MESSAGE,
+ "cause" => (*error).flags &= !ERROR_FLAG_HAS_CAUSE,
+ "errors" => (*error).flags &= !ERROR_FLAG_HAS_ERRORS,
+ _ => {}
+ }
+}
+
pub(crate) unsafe fn js_error_builtin_own_property_is_enumerable(
error: *mut ErrorHeader,
key: &str,
diff --git a/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs b/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs
index 258250bfa2..a2e82441d8 100644
--- a/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs
+++ b/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs
@@ -253,7 +253,7 @@ fn realm_owned_intrinsic_module_and_storage_roots_are_distinct() {
let a = a.join().expect("agent A panicked");
let b = b.join().expect("agent B panicked");
- assert_eq!(a.len(), 23, "the gate must cover every #8002/#8003 root");
+ assert_eq!(a.len(), 25, "the gate must cover every #8002/#8003 root");
assert_eq!(a.len(), b.len());
for ((a_name, a_slot, a_root), (b_name, b_slot, b_root)) in a.iter().zip(&b) {
assert_eq!(a_name, b_name, "snapshot wiring diverged between agents");
diff --git a/crates/perry-runtime/src/map.rs b/crates/perry-runtime/src/map.rs
index 856915bf0a..d49184a77f 100644
--- a/crates/perry-runtime/src/map.rs
+++ b/crates/perry-runtime/src/map.rs
@@ -2278,11 +2278,25 @@ pub extern "C" fn js_map_from_array(arr: *const crate::array::ArrayHeader) -> *m
pub extern "C" fn js_map_from_iterable(value: f64) -> *mut MapHeader {
use crate::collection_iter::{constructor_iter, ConstructorIter};
+ // The constructor returns before Get(map, "set") for a nullish iterable.
+ // In particular, a throwing accessor installed on Map.prototype.set must
+ // not affect `new Map()` / `new Map(null)`.
+ if crate::collection_iter::is_null_or_undefined(value) {
+ return js_map_alloc(4);
+ }
+
let scope = crate::gc::RuntimeHandleScope::new();
let value_handle = scope.root_nanbox_f64(value);
+ let map_handle = scope.root_raw_mut_ptr(js_map_alloc(4));
let adder = crate::collection_iter::require_callable(
- crate::collection_iter::builtin_prototype_method("Map", "set"),
+ map_handle.with_mut_ptr::(|map| {
+ crate::collection_iter::builtin_prototype_adder(
+ "Map",
+ "set",
+ crate::value::js_nanbox_pointer(map as i64),
+ )
+ }),
"Map.prototype.set",
);
let adder = crate::collection_iter::normalize_callable_value(adder);
@@ -2325,11 +2339,9 @@ pub extern "C" fn js_map_from_iterable(value: f64) -> *mut MapHeader {
}
match constructor_iter(value_handle.get_nanbox_f64()) {
- ConstructorIter::Empty => js_map_alloc(4),
+ ConstructorIter::Empty => map_handle.across_mut::(|| ()).1,
ConstructorIter::Array(arr_value) => {
let arr_handle = scope.root_nanbox_f64(arr_value);
- let map = js_map_alloc(4);
- let map_handle = scope.root_raw_mut_ptr(map);
let arr_ptr = crate::value::js_nanbox_get_pointer(arr_handle.get_nanbox_f64())
as *mut crate::array::ArrayHeader;
if !arr_ptr.is_null() {
@@ -2351,8 +2363,6 @@ pub extern "C" fn js_map_from_iterable(value: f64) -> *mut MapHeader {
}
ConstructorIter::Iterator(iter) => {
let iter_handle = scope.root_nanbox_f64(iter);
- let map = js_map_alloc(4);
- let map_handle = scope.root_raw_mut_ptr(map);
loop {
let iter = iter_handle.get_nanbox_f64();
let next = crate::collection_iter::iterator_next_value(iter);
diff --git a/crates/perry-runtime/src/object/arguments.rs b/crates/perry-runtime/src/object/arguments.rs
index d5b90ce334..58f716b8e6 100644
--- a/crates/perry-runtime/src/object/arguments.rs
+++ b/crates/perry-runtime/src/object/arguments.rs
@@ -138,6 +138,23 @@ extern "C" fn arguments_throw_type_error(_closure: *const crate::closure::Closur
fn thrower_closure_value() -> f64 {
let closure =
crate::closure::js_closure_alloc_singleton(arguments_throw_type_error as *const u8);
+ crate::closure::js_register_closure_arity(arguments_throw_type_error as *const u8, 0);
+ super::native_module::set_bound_native_closure_name(closure, "");
+ super::native_module::set_builtin_closure_length(closure as usize, 0);
+ super::native_module::set_builtin_closure_non_constructable(closure as usize);
+ let frozen = PropertyAttrs::new(false, false, false);
+ set_property_attrs(closure as usize, "name".to_string(), frozen);
+ set_property_attrs(closure as usize, "length".to_string(), frozen);
+ if !closure.is_null() {
+ unsafe {
+ if let Some(gc) = crate::value::addr_class::try_read_tracked_gc_header(closure as usize)
+ {
+ (*gc.as_ptr())._reserved |= crate::gc::OBJ_FLAG_FROZEN
+ | crate::gc::OBJ_FLAG_SEALED
+ | crate::gc::OBJ_FLAG_NO_EXTEND;
+ }
+ }
+ }
crate::value::js_nanbox_pointer(closure as i64)
}
@@ -559,6 +576,39 @@ pub(crate) unsafe fn arguments_object_to_vec(obj: *const ObjectHeader) -> Option
Some(out)
}
+/// Live `length` read used by the Arguments Array iterator.
+pub(crate) unsafe fn arguments_object_length(obj: *const ObjectHeader) -> u32 {
+ if !is_arguments_object(obj) {
+ return 0;
+ }
+ let scope = crate::gc::RuntimeHandleScope::new();
+ let obj_h = scope.root_raw_const_ptr(obj);
+ let key = intern_key("length");
+ let value = obj_h
+ .with_const_ptr(|obj| arguments_object_get_field(obj, key))
+ .map(|v| f64::from_bits(v.bits()))
+ .unwrap_or(0.0);
+ if value.is_finite() && value > 0.0 {
+ value.floor().min(u32::MAX as f64) as u32
+ } else {
+ 0
+ }
+}
+
+/// Live indexed read used by the Arguments Array iterator.
+pub(crate) unsafe fn arguments_object_index_value(obj: *const ObjectHeader, index: u32) -> f64 {
+ if !is_arguments_object(obj) {
+ return f64::from_bits(crate::value::TAG_UNDEFINED);
+ }
+ let scope = crate::gc::RuntimeHandleScope::new();
+ let obj_h = scope.root_raw_const_ptr(obj);
+ let key = intern_key(&index.to_string());
+ obj_h
+ .with_const_ptr(|obj| arguments_object_get_field(obj, key))
+ .map(|v| f64::from_bits(v.bits()))
+ .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED))
+}
+
pub(crate) unsafe fn arguments_object_to_array(
obj: *const ObjectHeader,
) -> Option<*mut ArrayHeader> {
diff --git a/crates/perry-runtime/src/object/async_generator_queue.rs b/crates/perry-runtime/src/object/async_generator_queue.rs
index dcccaba260..16c8b30a67 100644
--- a/crates/perry-runtime/src/object/async_generator_queue.rs
+++ b/crates/perry-runtime/src/object/async_generator_queue.rs
@@ -41,6 +41,9 @@ struct AsyncGeneratorRequest {
struct AsyncGeneratorQueueState {
active: bool,
drain_scheduled: bool,
+ started: bool,
+ completed: bool,
+ original_throw: *const ClosureHeader,
queue: VecDeque,
}
@@ -91,22 +94,27 @@ pub(crate) fn wrap_async_generator_instance(obj: *mut ObjectHeader) {
let next_h = scope.root_nanbox_f64(js_nanbox_pointer(next as i64));
let ret_h = scope.root_nanbox_f64(js_nanbox_pointer(ret as i64));
let throw_h = scope.root_nanbox_f64(js_nanbox_pointer(throw as i64));
+ let closure_now = |h: &crate::gc::RuntimeHandle<'_>| {
+ js_nanbox_get_pointer(h.get_nanbox_f64()) as *const ClosureHeader
+ };
let state_id = STATES.with(|states| {
let mut states = states.borrow_mut();
let id = states.len() + 1;
+ let original_throw = closure_now(&throw_h);
+ crate::gc::runtime_write_barrier_root_raw_ptr(original_throw);
states.push(AsyncGeneratorQueueState {
active: false,
drain_scheduled: false,
+ started: false,
+ completed: false,
+ original_throw,
queue: VecDeque::new(),
});
id
});
let obj_now = || js_nanbox_get_pointer(obj_h.get_nanbox_f64()) as *mut ObjectHeader;
- let closure_now = |h: &crate::gc::RuntimeHandle<'_>| {
- js_nanbox_get_pointer(h.get_nanbox_f64()) as *const ClosureHeader
- };
for (name, original_h, func) in [
(
@@ -136,6 +144,7 @@ pub(crate) fn scan_async_generator_queue_roots_mut(
) {
STATES.with(|states| {
for state in states.borrow_mut().iter_mut() {
+ visitor.visit_raw_const_ptr_slot(&mut state.original_throw);
for request in state.queue.iter_mut() {
visitor.visit_raw_const_ptr_slot(&mut request.original);
visitor.visit_nanbox_f64_slot(&mut request.arg);
@@ -362,16 +371,46 @@ fn dispatch_return_with_await(
arg: f64,
out: *mut Promise,
) {
- let arg_promise = crate::promise::js_promise_resolved(arg);
- let fulfill = make_return_step_wrapper(state_id, original, out, true);
- let reject = make_return_step_wrapper(state_id, original, out, false);
- js_promise_attach_settle_listener(arg_promise, fulfill, reject);
+ let scope = crate::gc::RuntimeHandleScope::new();
+ let original_h = scope.root_raw_const_ptr(original);
+ let out_h = scope.root_raw_mut_ptr(out);
+ let arg_h = scope.root_nanbox_f64(arg);
+ let arg_promise = match crate::promise::js_promise_resolved_catching(arg_h.get_nanbox_f64()) {
+ Ok(promise) => promise,
+ Err(reason) => {
+ let suspended_throw = STATES.with(|states| {
+ states
+ .borrow()
+ .get(state_id - 1)
+ .filter(|state| state.started && !state.completed)
+ .map(|state| state.original_throw)
+ });
+ if let Some(throw_original) = suspended_throw {
+ let throw_h = scope.root_raw_const_ptr(throw_original);
+ let result = throw_h.with_const_ptr(|throw| call_original(throw, reason));
+ out_h.with_mut_ptr(|out| after_queued_result(state_id, out, result));
+ } else {
+ out_h.with_mut_ptr(|out| {
+ finish_after_immediate_queued_result(state_id, out, false, reason)
+ });
+ }
+ return;
+ }
+ };
+ let arg_promise_h = scope.root_raw_mut_ptr(arg_promise);
+ let fulfill = make_return_step_wrapper(state_id, &original_h, &out_h, true);
+ let fulfill_h = scope.root_raw_mut_ptr(fulfill);
+ let reject = make_return_step_wrapper(state_id, &original_h, &out_h, false);
+ arg_promise_h.with_mut_ptr(|arg_promise| {
+ fulfill_h
+ .with_mut_ptr(|fulfill| js_promise_attach_settle_listener(arg_promise, fulfill, reject))
+ });
}
fn make_return_step_wrapper(
state_id: usize,
- original: *const ClosureHeader,
- out: *mut Promise,
+ original: &crate::gc::RuntimeHandle<'_>,
+ out: &crate::gc::RuntimeHandle<'_>,
is_fulfilled: bool,
) -> *mut ClosureHeader {
let func = if is_fulfilled {
@@ -381,8 +420,10 @@ fn make_return_step_wrapper(
};
let wrapper = js_closure_alloc(func, 3);
js_closure_set_capture_f64(wrapper, 0, state_id as f64);
- js_closure_set_capture_ptr(wrapper, 1, original as i64);
- js_closure_set_capture_ptr(wrapper, 2, out as i64);
+ original.with_const_ptr::(|original| {
+ js_closure_set_capture_ptr(wrapper, 1, original as i64)
+ });
+ out.with_mut_ptr::(|out| js_closure_set_capture_ptr(wrapper, 2, out as i64));
wrapper
}
@@ -459,6 +500,11 @@ fn after_initial_result(state_id: usize, result: f64) {
attach_pending_settle(state_id, promise, std::ptr::null_mut());
return;
}
+ if state == PromiseState::Fulfilled {
+ note_step_settlement(state_id, true, unsafe { (*promise).value });
+ } else if state == PromiseState::Rejected {
+ note_step_settlement(state_id, false, unsafe { (*promise).reason });
+ }
}
schedule_drain(state_id);
}
@@ -532,6 +578,7 @@ fn finish_after_pending_result(state_id: usize, out: *mut Promise, fulfilled: bo
// pending path is now always taken; before #6709 `.next()` resolved
// synchronously (busy-wait) and hit the immediate path, which already
// deferred the drain via `schedule_drain`.
+ note_step_settlement(state_id, fulfilled, value);
settle_out(out, fulfilled, value);
let has_queue = STATES.with(|states| {
states
@@ -552,6 +599,7 @@ fn finish_after_immediate_queued_result(
fulfilled: bool,
value: f64,
) {
+ note_step_settlement(state_id, fulfilled, value);
let has_queue = STATES.with(|states| {
states
.borrow()
@@ -566,6 +614,29 @@ fn finish_after_immediate_queued_result(
settle_out(out, fulfilled, value);
}
+fn note_step_settlement(state_id: usize, fulfilled: bool, value: f64) {
+ let done = if fulfilled {
+ let field = js_object_get_own_field_or_undef(value, b"done".as_ptr(), 4);
+ let jv = JSValue::from_bits(field.to_bits());
+ (!jv.is_undefined()).then(|| crate::value::js_is_truthy(field) != 0)
+ } else {
+ None
+ };
+ STATES.with(|states| {
+ let mut states = states.borrow_mut();
+ let Some(state) = states.get_mut(state_id - 1) else {
+ return;
+ };
+ if !fulfilled {
+ state.completed = true;
+ } else if let Some(true) = done {
+ state.completed = true;
+ } else if let Some(false) = done {
+ state.started = true;
+ }
+ });
+}
+
fn mark_inactive(state_id: usize) {
STATES.with(|states| {
if let Some(state) = states.borrow_mut().get_mut(state_id - 1) {
diff --git a/crates/perry-runtime/src/object/buffer_dispatch.rs b/crates/perry-runtime/src/object/buffer_dispatch.rs
index 4d569198e2..0383349399 100644
--- a/crates/perry-runtime/src/object/buffer_dispatch.rs
+++ b/crates/perry-runtime/src/object/buffer_dispatch.rs
@@ -516,15 +516,24 @@ pub unsafe fn dispatch_buffer_method(
crate::buffer::array_buffer_transfer(addr, args)
}
"slice" | "subarray" => {
- let len = (*buf_ptr).length as i32;
- let (start, end) = if crate::buffer::is_array_buffer(addr)
- || crate::buffer::is_shared_array_buffer(addr)
- {
+ let source_is_array_buffer = crate::buffer::is_array_buffer(addr);
+ let source_is_shared_array_buffer = crate::buffer::is_shared_array_buffer(addr);
+ let source_is_any_array_buffer =
+ source_is_array_buffer || source_is_shared_array_buffer;
+ let scope = crate::gc::RuntimeHandleScope::new();
+ let buffer = scope.root_raw_mut_ptr(buf_ptr);
+ let len =
+ buffer.with_mut_ptr::(|buf| (*buf).length as i32);
+ let (start, end) = if source_is_any_array_buffer {
+ // Instance-call lowering reaches this fused dispatch directly,
+ // bypassing the prototype thunk. The shared path therefore owns
+ // both index coercion and ArrayBufferSpeciesCreate ordering.
// A detached ArrayBuffer refuses slice with a TypeError
// (ES2024 DetachArrayBuffer; `transfer` is the only detach
// source in Perry).
- if crate::buffer::is_detached_buffer(addr)
- && !crate::buffer::is_shared_array_buffer(addr)
+ if buffer.with_mut_ptr::(|buf| {
+ crate::buffer::is_detached_buffer(buf as usize)
+ }) && !source_is_shared_array_buffer
{
crate::collection_iter::throw_type_error(
"Cannot perform ArrayBuffer.prototype.slice on a detached ArrayBuffer",
@@ -542,19 +551,40 @@ pub unsafe fn dispatch_buffer_method(
} else {
ab_slice_index(args[1])
};
+ // SpeciesConstructor is observed only after BOTH index
+ // coercions. It may run getters and collect, so the buffer is
+ // re-read from its handle again by the copy below.
+ if method_name == "slice" {
+ buffer.with_mut_ptr::(|buf| {
+ super::global_this::validate_array_buffer_species_constructor(buf as usize)
+ });
+ // ArrayBuffer.prototype.slice checks detachment again
+ // after the observable species lookup/creation sequence.
+ if !source_is_shared_array_buffer
+ && buffer.with_mut_ptr::(|buf| {
+ crate::buffer::is_detached_buffer(buf as usize)
+ })
+ {
+ crate::collection_iter::throw_type_error(
+ "Cannot perform ArrayBuffer.prototype.slice on a detached ArrayBuffer",
+ );
+ }
+ }
(s, e)
} else {
let s = arg_i32(0);
let e = if args.len() >= 2 { arg_i32(1) } else { len };
(s, e)
};
- let result = crate::buffer::js_buffer_slice(buf_ptr, start, end);
+ let result = buffer.with_mut_ptr::(|buf| {
+ crate::buffer::js_buffer_slice(buf, start, end)
+ });
// #2877: `ArrayBuffer.prototype.slice` returns a NEW ArrayBuffer
// (a copy), so mark the result so `ArrayBuffer.isView(slice)` is
// false and a subsequent `new Uint8Array(slice)` aliases it.
- if crate::buffer::is_array_buffer(addr) {
+ if source_is_array_buffer {
crate::buffer::mark_as_array_buffer(result as usize);
- } else if crate::buffer::is_shared_array_buffer(addr) {
+ } else if source_is_shared_array_buffer {
crate::buffer::mark_as_shared_array_buffer(result as usize);
}
f64::from_bits(JSValue::pointer(result as *mut u8).bits())
diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs
index 64dd9aefa5..c0387c3bbe 100644
--- a/crates/perry-runtime/src/object/class_registry.rs
+++ b/crates/perry-runtime/src/object/class_registry.rs
@@ -43,6 +43,7 @@ mod class_meta;
mod construct;
pub(crate) use construct::scan_current_new_target_root_mut;
mod dispatch;
+mod function_prototype;
mod gc_roots;
pub(crate) mod parent_static;
mod prototype_methods;
@@ -117,15 +118,18 @@ pub use prototype_methods::{
// ── construct.rs / vm_brand.rs ──────────────────────────────────────────────
pub(crate) use construct::{
- extends_target_must_throw, function_would_have_own_prototype, is_callable_function_value,
- js_value_is_constructor, lookup_prototype_method, nm_ctor_child_process, nm_ctor_cluster,
- nm_ctor_fs, nm_ctor_readline, nm_ctor_repl, nm_ctor_stream, nm_ctor_tls, nm_ctor_tty,
- nm_ctor_vm, nm_ctor_wasi, ordinary_function_prototype_value_for_read, promise_parent_in_chain,
+ extends_target_must_throw, is_callable_function_value, js_value_is_constructor,
+ lookup_prototype_method, nm_ctor_child_process, nm_ctor_cluster, nm_ctor_fs, nm_ctor_readline,
+ nm_ctor_repl, nm_ctor_stream, nm_ctor_tls, nm_ctor_tty, nm_ctor_vm, nm_ctor_wasi,
+ promise_parent_in_chain,
};
pub use construct::{
- js_ctor_return_override, js_function_prototype_value_for_read, js_new_function_construct,
- js_new_function_construct_apply, js_new_function_construct_with_new_target,
- js_new_target_value,
+ js_ctor_return_override, js_new_function_construct, js_new_function_construct_apply,
+ js_new_function_construct_with_new_target, js_new_target_value,
+};
+pub use function_prototype::js_function_prototype_value_for_read;
+pub(crate) use function_prototype::{
+ function_would_have_own_prototype, ordinary_function_prototype_value_for_read,
};
pub(crate) use vm_brand::brand_vm_script_instance;
diff --git a/crates/perry-runtime/src/object/class_registry/construct.rs b/crates/perry-runtime/src/object/class_registry/construct.rs
index e50e4f94f0..133aa98e7c 100644
--- a/crates/perry-runtime/src/object/class_registry/construct.rs
+++ b/crates/perry-runtime/src/object/class_registry/construct.rs
@@ -988,6 +988,9 @@ pub unsafe extern "C" fn js_new_function_construct(
class_cid, class_cid, func_value, args_ptr, args_len,
);
}
+ if extends_target_must_throw(func_value) {
+ super::super::object_ops::throw_object_type_error(b"is not a constructor");
+ }
if is_arrow_function_value(func_value) {
crate::fs::validate::throw_type_error_with_code(
"Arrow function is not a constructor",
@@ -1130,7 +1133,10 @@ pub unsafe extern "C" fn js_new_function_construct(
}
return inst_handle.get_nanbox_f64();
}
- nan_boxed
+ // Ordinary objects, symbols and every other non-callable heap value do not
+ // have [[Construct]]. The historical placeholder return made `new
+ // Reflect()`, `new Error.prototype()` and `new sym()` silently succeed.
+ super::super::object_ops::throw_object_type_error(b"is not a constructor")
}
/// `new (...spread)` — spread-bearing construction. Codegen builds a
@@ -1666,13 +1672,16 @@ pub unsafe extern "C" fn js_new_function_construct_with_new_target(
| "BigInt64Array"
| "BigUint64Array"
) {
- // Read `newTarget.prototype` (GetPrototypeFromConstructor) BEFORE
- // building the view: Node evaluates the proto access as part of
- // AllocateTypedArray, so a throwing `prototype` getter must surface
- // here even when later steps would also throw (test262
- // `throw-type-error-before-custom-proto-access` agreement).
- let proto_bits = new_target_custom_object_prototype(nt);
+ // Validate and initialize the typed-array contents before reading
+ // a custom newTarget prototype. In particular, a Number/BigInt
+ // element-type mismatch must throw TypeError without observing a
+ // poisoned `newTarget.prototype` getter.
+ let scope = crate::gc::RuntimeHandleScope::new();
+ let nt_h = scope.root_nanbox_f64(nt);
let result = js_new_function_construct(func_value, args_ptr, args_len);
+ let result_h = scope.root_heap_word_u64(result.to_bits());
+ let proto_bits = new_target_custom_object_prototype(nt_h.get_nanbox_f64());
+ let result = f64::from_bits(result_h.get_heap_word_u64());
if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(result) {
if let Some(proto_bits) = proto_bits {
super::super::prototype_chain::object_set_static_prototype(addr, proto_bits);
@@ -1697,7 +1706,7 @@ pub unsafe extern "C" fn js_new_function_construct_with_new_target(
let bits = result.to_bits();
let addr = if (bits >> 48) == 0x7FFD {
(bits & crate::value::POINTER_MASK) as usize
- } else if (bits >> 48) == 0 && bits >= 0x1000 {
+ } else if (bits >> 48) == 0 && crate::buffer::is_registered_buffer(bits as usize) {
// ArrayBuffer and SharedArrayBuffer are represented by a
// raw BufferHeader pointer rather than a NaN-boxed object.
bits as usize
@@ -1820,7 +1829,7 @@ pub(crate) fn is_callable_function_value(value: f64) -> bool {
unsafe { (*ptr).type_tag == crate::closure::CLOSURE_MAGIC }
}
-fn is_arrow_function_value(value: f64) -> bool {
+pub(super) fn is_arrow_function_value(value: f64) -> bool {
use crate::value::JSValue;
let jv = JSValue::from_bits(value.to_bits());
if !jv.is_pointer() {
@@ -1841,133 +1850,6 @@ fn is_arrow_function_value(value: f64) -> bool {
crate::closure::closure_is_arrow(ptr)
}
-/// Predicate-only sibling of `ordinary_function_prototype_value_for_read`:
-/// would this function have an own `.prototype` slot? Crucially does NOT
-/// materialize the prototype object — `fn.hasOwnProperty('prototype')` must
-/// not lock the slot's attributes before a later
-/// `Object.defineProperty(fn, "prototype", …)` (TypedArrayConstructors
-/// custom-proto tests).
-pub(crate) fn function_would_have_own_prototype(func_value: f64) -> bool {
- if !is_callable_function_value(func_value) || is_arrow_function_value(func_value) {
- return false;
- }
- if super::super::native_module::builtin_closure_is_non_constructable_value(func_value) {
- return false;
- }
- synthetic_class_id_for_function(func_value) != 0
-}
-
-pub(crate) fn ordinary_function_prototype_value_for_read(func_value: f64) -> Option {
- if !is_callable_function_value(func_value) || is_arrow_function_value(func_value) {
- return None;
- }
- // Bound-method / bound-function values (class method/getter/setter reads via
- // `C.prototype.m`, instance method reads, `fn.bind(...)`) are non-constructors
- // and have NO `prototype` own property (`C.prototype.m.prototype === undefined`,
- // `'prototype' in C.prototype.m === false`). (Test262 definition method/accessor
- // prop-desc.)
- //
- // #4973 / #3527 / #5268 exception: bound NATIVE-MODULE *class* exports
- // (`http.Server`, `fs.ReadStream`, `events.EventEmitter`, …) are
- // constructors in Node, and the util.inherits / `Object.create(Ctor.
- // prototype)` / `Object.setPrototypeOf(x, Ctor.prototype)` subclass
- // pattern reads their `.prototype` as a setPrototypeOf / Object.create
- // operand. Returning None here made that read `undefined`, and
- // `Object.create(undefined)` / `Object.setPrototypeOf(x, undefined)` then
- // threw "Object prototype may only be an Object or null" — the blocker hit
- // at Express init (`express/lib/request.js`:
- // `Object.create(http.IncomingMessage.prototype)`), graceful-fs's
- // `ReadStream.prototype = Object.create(fs$ReadStream.prototype)`, and
- // pino's `Object.setPrototypeOf(prototype, EventEmitter.prototype)`.
- //
- // A bound-native export is a constructor class when its method name uses
- // Node's constructor-cased convention (a leading uppercase ASCII letter,
- // e.g. `ReadStream`/`EventEmitter`/`Server`) AND it isn't explicitly
- // marked non-constructable (built-in prototype methods like
- // `String.prototype.charAt` carry that flag). Such exports are cached
- // singleton closures (NATIVE_CALLABLE_EXPORTS), so the synthetic-class
- // path below gives them a stable `.prototype` object. Non-constructor
- // bound methods (`fs.readFile`, `path.join`, …) keep `prototype ===
- // undefined`, matching Node's built-in non-constructor functions.
- {
- let jv = crate::value::JSValue::from_bits(func_value.to_bits());
- if jv.is_pointer() {
- let cptr = jv.as_pointer::();
- if !cptr.is_null()
- && is_valid_obj_ptr(cptr as *const u8)
- && crate::closure::closure_is_bound_method(cptr)
- {
- if super::super::native_module::builtin_closure_is_non_constructable_value(
- func_value,
- ) {
- return None;
- }
- let is_native_class_export = unsafe {
- super::super::native_module::bound_native_callable_module_and_method(func_value)
- }
- .map(|(_module, method)| {
- method
- .as_bytes()
- .first()
- .is_some_and(|b| b.is_ascii_uppercase())
- })
- .unwrap_or(false);
- if !is_native_class_export {
- return None;
- }
- }
- }
- }
- // Built-in methods (`String.prototype.charAt`, `Array.prototype.map`, …) are
- // not constructors and have NO `prototype` own property — `String.prototype.
- // charAt.prototype === undefined` (ECMA-262: built-in non-constructor
- // functions don't get the auto-created `.prototype`). Don't lazily synthesize
- // one for them.
- if super::super::native_module::builtin_closure_is_non_constructable_value(func_value) {
- return None;
- }
- let cid = synthetic_class_id_for_function(func_value);
- if cid == 0 {
- return None;
- }
- let proto = ensure_function_prototype_object(func_value, cid);
- if proto.is_null() {
- return None;
- }
- Some(crate::value::js_nanbox_pointer(proto as i64))
-}
-
-#[no_mangle]
-pub extern "C" fn js_function_prototype_value_for_read(func_value: f64) -> f64 {
- let undef = f64::from_bits(crate::value::TAG_UNDEFINED);
- let jv = crate::value::JSValue::from_bits(func_value.to_bits());
- if !jv.is_pointer() {
- return undef;
- }
- let ptr = jv.as_pointer() as *const crate::closure::ClosureHeader;
- if ptr.is_null() || !is_valid_obj_ptr(ptr as *const u8) {
- return undef;
- }
- unsafe {
- if (*ptr).type_tag != crate::closure::CLOSURE_MAGIC {
- return undef;
- }
- }
-
- let closure_addr = ptr as usize;
- if crate::closure::closure_is_key_deleted(closure_addr, "prototype") {
- return undef;
- }
- let dynamic = crate::closure::closure_get_dynamic_prop(closure_addr, "prototype");
- if dynamic.to_bits() != crate::value::TAG_UNDEFINED {
- return dynamic;
- }
- if let Some(proto) = generator_function_prototype_of(closure_addr) {
- return proto;
- }
- ordinary_function_prototype_value_for_read(func_value).unwrap_or(undef)
-}
-
/// Lookup helper: returns the registered prototype-method value for
/// `(class_id, name)`, or None if no assignment matched. Walks the
/// parent-class chain so methods registered on a base class are found
diff --git a/crates/perry-runtime/src/object/class_registry/construct/class_return.rs b/crates/perry-runtime/src/object/class_registry/construct/class_return.rs
index be5091de83..6f3077ae2b 100644
--- a/crates/perry-runtime/src/object/class_registry/construct/class_return.rs
+++ b/crates/perry-runtime/src/object/class_registry/construct/class_return.rs
@@ -39,7 +39,7 @@ fn constructor_return_overrides_this(value: f64) -> bool {
let bits = value.to_bits();
let raw_addr = if jv.is_pointer() {
(bits & crate::value::POINTER_MASK) as usize
- } else if (bits >> 48) == 0 && bits >= 0x1000 {
+ } else if (bits >> 48) == 0 {
bits as usize
} else {
0
@@ -72,13 +72,12 @@ fn constructor_return_overrides_this(value: f64) -> bool {
if !arr.is_null() {
return true;
}
- if !is_valid_obj_ptr(raw as *const u8) {
+ let Some(gc_header) = crate::value::addr_class::try_read_tracked_gc_header(raw as usize)
+ else {
return false;
- }
- let gc_header =
- (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader;
+ };
matches!(
- (*gc_header).obj_type,
+ (*gc_header.as_ptr()).obj_type,
// Per spec, a constructor returning ANY Object overrides the
// implicit `this`. Promises are objects — a user constructor like
// `function P(exec){ return new Promise(...) }` (the
diff --git a/crates/perry-runtime/src/object/class_registry/function_prototype.rs b/crates/perry-runtime/src/object/class_registry/function_prototype.rs
new file mode 100644
index 0000000000..da90ae67fd
--- /dev/null
+++ b/crates/perry-runtime/src/object/class_registry/function_prototype.rs
@@ -0,0 +1,104 @@
+use super::construct::{is_arrow_function_value, is_callable_function_value};
+use super::*;
+
+/// Does this function have an own `.prototype` slot? This intentionally does
+/// not materialize the prototype, so `hasOwnProperty` cannot freeze its attrs.
+pub(crate) fn function_would_have_own_prototype(func_value: f64) -> bool {
+ if !is_callable_function_value(func_value)
+ || is_arrow_function_value(func_value)
+ || is_plain_async_function_value(func_value)
+ || super::super::native_module::builtin_closure_is_non_constructable_value(func_value)
+ {
+ return false;
+ }
+ synthetic_class_id_for_function(func_value) != 0
+}
+
+pub(crate) fn ordinary_function_prototype_value_for_read(func_value: f64) -> Option {
+ if !is_callable_function_value(func_value)
+ || is_arrow_function_value(func_value)
+ || is_plain_async_function_value(func_value)
+ {
+ return None;
+ }
+ // Bound native class exports are constructors; ordinary bound methods are
+ // not. A stable synthetic class gives constructor exports a prototype.
+ let jv = crate::value::JSValue::from_bits(func_value.to_bits());
+ if jv.is_pointer() {
+ let cptr = jv.as_pointer::();
+ if !cptr.is_null()
+ && crate::value::addr_class::is_plausible_heap_addr(cptr as usize)
+ && crate::closure::closure_is_bound_method(cptr)
+ {
+ if super::super::native_module::builtin_closure_is_non_constructable_value(func_value) {
+ return None;
+ }
+ let is_native_class_export = unsafe {
+ super::super::native_module::bound_native_callable_module_and_method(func_value)
+ }
+ .is_some_and(|(_, method)| {
+ method
+ .as_bytes()
+ .first()
+ .is_some_and(|b| b.is_ascii_uppercase())
+ });
+ if !is_native_class_export {
+ return None;
+ }
+ }
+ }
+ if super::super::native_module::builtin_closure_is_non_constructable_value(func_value) {
+ return None;
+ }
+ let cid = synthetic_class_id_for_function(func_value);
+ if cid == 0 {
+ return None;
+ }
+ let proto = ensure_function_prototype_object(func_value, cid);
+ (!proto.is_null()).then(|| crate::value::js_nanbox_pointer(proto as i64))
+}
+
+fn is_plain_async_function_value(func_value: f64) -> bool {
+ let jv = crate::value::JSValue::from_bits(func_value.to_bits());
+ if !jv.is_pointer() {
+ return false;
+ }
+ let ptr = jv.as_pointer::();
+ if ptr.is_null() || !crate::value::addr_class::is_plausible_heap_addr(ptr as usize) {
+ return false;
+ }
+ let fp = crate::closure::get_valid_func_ptr(ptr);
+ !fp.is_null()
+ && crate::closure::is_registered_async_function(fp)
+ && !crate::closure::is_registered_generator_function(fp)
+}
+
+#[no_mangle]
+pub extern "C" fn js_function_prototype_value_for_read(func_value: f64) -> f64 {
+ let undef = f64::from_bits(crate::value::TAG_UNDEFINED);
+ let jv = crate::value::JSValue::from_bits(func_value.to_bits());
+ if !jv.is_pointer() {
+ return undef;
+ }
+ let ptr = jv.as_pointer() as *const crate::closure::ClosureHeader;
+ if ptr.is_null() || !crate::value::addr_class::is_plausible_heap_addr(ptr as usize) {
+ return undef;
+ }
+ unsafe {
+ if (*ptr).type_tag != crate::closure::CLOSURE_MAGIC {
+ return undef;
+ }
+ }
+ let closure_addr = ptr as usize;
+ if crate::closure::closure_is_key_deleted(closure_addr, "prototype") {
+ return undef;
+ }
+ let dynamic = crate::closure::closure_get_dynamic_prop(closure_addr, "prototype");
+ if dynamic.to_bits() != crate::value::TAG_UNDEFINED {
+ return dynamic;
+ }
+ if let Some(proto) = generator_function_prototype_of(closure_addr) {
+ return proto;
+ }
+ ordinary_function_prototype_value_for_read(func_value).unwrap_or(undef)
+}
diff --git a/crates/perry-runtime/src/object/class_registry/parent_static.rs b/crates/perry-runtime/src/object/class_registry/parent_static.rs
index 390de575ed..25276ec985 100644
--- a/crates/perry-runtime/src/object/class_registry/parent_static.rs
+++ b/crates/perry-runtime/src/object/class_registry/parent_static.rs
@@ -109,7 +109,7 @@ pub extern "C" fn js_register_class_parent_dynamic(class_id: u32, mut parent_val
match name {
"Request" => super::super::register_fetch_parent_kind(class_id, 1),
"Response" => super::super::register_fetch_parent_kind(class_id, 2),
- _ => {}
+ _ => super::super::data_view_registry::register_builtin_view_parent(class_id, name),
}
return;
}
@@ -651,7 +651,7 @@ pub unsafe extern "C" fn js_register_class_computed_method(
setters: HashMap::new(),
});
vtable.methods.insert(
- name,
+ name.clone(),
VTableMethodEntry {
func_ptr: func_ptr as usize,
param_count: param_count as u32,
@@ -662,6 +662,10 @@ pub unsafe extern "C" fn js_register_class_computed_method(
has_rest: has_rest != 0,
},
);
+ // Backfill when reflection already materialized `C.prototype`.
+ drop(registry);
+ let proto = class_decl_prototype_object(class_id);
+ super::state::install_class_decl_prototype_method_field(proto, class_id, &name);
}
VTABLE_GEN.fetch_add(1, Ordering::Release);
}
diff --git a/crates/perry-runtime/src/object/class_registry/state.rs b/crates/perry-runtime/src/object/class_registry/state.rs
index 6830919e13..01ace7af6e 100644
--- a/crates/perry-runtime/src/object/class_registry/state.rs
+++ b/crates/perry-runtime/src/object/class_registry/state.rs
@@ -580,13 +580,13 @@ pub(crate) fn class_decl_prototype_method_names(class_id: u32) -> Vec {
let mut names = Vec::new();
if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() {
if let Some(vtable) = registry.as_ref().and_then(|reg| reg.get(&class_id)) {
- names.extend(
- vtable
- .methods
- .keys()
- .filter(|name| *name != "constructor")
- .cloned(),
- );
+ // The real class constructor is stored in `Class::constructor`,
+ // not in the instance-method vtable. An entry named
+ // `"constructor"` here is therefore an ordinary method, most
+ // notably `class C { ["constructor"]() {} }`. It must replace the
+ // implicit `C.prototype.constructor` data property when the
+ // reflective prototype object is materialized.
+ names.extend(vtable.methods.keys().cloned());
}
}
names.sort();
@@ -594,14 +594,44 @@ pub(crate) fn class_decl_prototype_method_names(class_id: u32) -> Vec {
names
}
+pub(super) fn install_class_decl_prototype_method_field(
+ proto: *mut ObjectHeader,
+ class_id: u32,
+ name: &str,
+) {
+ if proto.is_null() {
+ return;
+ }
+ let scope = crate::gc::RuntimeHandleScope::new();
+ let proto_handle = scope.root_raw_mut_ptr(proto);
+ // Do not bind by reading the prototype object here. Its implicit
+ // `constructor` data property would shadow a computed method with that
+ // name and make the installation write the class constructor straight
+ // back. The canonical vtable value is the property value we need.
+ let method_handle =
+ scope.root_nanbox_f64(class_prototype_method_value_for_name(class_id, name));
+ let key_handle = scope.root_string_ptr(crate::string::js_string_from_bytes(
+ name.as_ptr(),
+ name.len() as u32,
+ ));
+ let method = method_handle.get_nanbox_f64();
+ proto_handle.with_mut_ptr::(|proto| {
+ key_handle.with_const_ptr::(|key| {
+ js_object_set_field_by_name(proto, key, method)
+ })
+ });
+ proto_handle.with_mut_ptr::(|proto| {
+ set_builtin_property_attrs(
+ proto as usize,
+ name.to_string(),
+ PropertyAttrs::new(true, false, true),
+ )
+ });
+}
+
fn install_class_decl_prototype_method_fields(proto: *mut ObjectHeader, class_id: u32) {
- let proto_value = crate::value::js_nanbox_pointer(proto as i64);
for name in class_decl_prototype_method_names(class_id) {
- let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32);
- let leaked: &'static [u8] = name.as_bytes().to_vec().leak();
- let method = js_class_method_bind(proto_value, leaked.as_ptr(), leaked.len());
- js_object_set_field_by_name(proto, key, method);
- set_builtin_property_attrs(proto as usize, name, PropertyAttrs::new(true, false, true));
+ install_class_decl_prototype_method_field(proto, class_id, &name);
}
}
diff --git a/crates/perry-runtime/src/object/collection_proto_thunks.rs b/crates/perry-runtime/src/object/collection_proto_thunks.rs
index 23f68c6720..f2fe0399d6 100644
--- a/crates/perry-runtime/src/object/collection_proto_thunks.rs
+++ b/crates/perry-runtime/src/object/collection_proto_thunks.rs
@@ -21,6 +21,19 @@ crate::perry_thread_local! {
static BUILTIN_SET_ADD_VALUE_BITS: std::cell::Cell = const { std::cell::Cell::new(0) };
}
+pub(crate) fn scan_builtin_collection_method_roots_mut(
+ visitor: &mut crate::gc::RuntimeRootVisitor<'_>,
+) {
+ for slot in [&BUILTIN_MAP_SET_VALUE_BITS, &BUILTIN_SET_ADD_VALUE_BITS] {
+ slot.with(|slot| {
+ let mut bits = slot.get();
+ if visitor.visit_heap_word_u64_slot(&mut bits) {
+ slot.set(bits);
+ }
+ });
+ }
+}
+
pub(crate) fn is_builtin_map_set_value(value: f64) -> bool {
is_remembered_builtin_collection_method(value, &BUILTIN_MAP_SET_VALUE_BITS)
}
@@ -164,6 +177,10 @@ pub(super) fn install_collection_proto_methods(
ipm(proto_obj, "has", set_proto_has_thunk as *const u8, 1);
ipm(proto_obj, "keys", set_proto_keys_thunk as *const u8, 0);
let values_value = ipm(proto_obj, "values", set_proto_values_thunk as *const u8, 0);
+ // ECMA-262 specifies these as the same function object, not merely
+ // two functions with equivalent behavior.
+ let keys_key = crate::string::js_string_from_bytes(b"keys".as_ptr(), 4);
+ super::js_object_set_field_by_name(proto_obj, keys_key, values_value);
install_collection_iterator_symbol(proto_obj, values_value);
remember_builtin_collection_method(
proto_obj,
diff --git a/crates/perry-runtime/src/object/data_view_registry.rs b/crates/perry-runtime/src/object/data_view_registry.rs
index 3eb76f6a04..d7f5bf433d 100644
--- a/crates/perry-runtime/src/object/data_view_registry.rs
+++ b/crates/perry-runtime/src/object/data_view_registry.rs
@@ -3,6 +3,8 @@ use super::*;
/// Global registry of class IDs that extend the built-in DataView class.
static EXTENDS_DATA_VIEW_REGISTRY: RwLock