From cd4d4180ff7286d03d1a190717c254579dfceb90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 23 Aug 2026 12:06:18 +0200 Subject: [PATCH 01/10] perf(codegen): materialize large constant array literals from a static descriptor + one bulk call (#8583 follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A minified bundle data table is a giant nested constant array literal. The default lowering builds it procedurally — one `js_array_from_values` per sub-array plus the inline element stores — so the Claude Code bundle's `__33499` (a constant numeric array-of-arrays) lowered to 11,104 allocations and a 245k- instruction body that made `rewrite-statepoints-for-gc` fan out. This adds a codegen path that recognizes a LARGE, fully-constant array literal (number/int/bool/null/undefined, recursively nested arrays) and instead: * serializes the constant tree into a compact tagged blob emitted as module- private rodata, and * emits ONE call to a new runtime helper `js_value_from_const_descriptor` that materializes the whole nested structure in a single pass. The runtime builds a FRESH, mutable array each call (JS array literals are mutable, so the descriptor is a template, never a shared constant), under `GcSuppressScope` so the partially-built parents held across nested child allocations cannot be collected or moved — the same discipline `js_json_parse` and the lazy-array materializer use. All-number rows keep the raw-f64 layout; any pointer element downgrades the row via `store_array_slot`. Gated on a 256-node minimum, so small literals keep the fast inline bump-alloc path (no regression). `PERRY_CONST_ARRAY_DESCRIPTOR=0` reverts to the procedural path (A/B bisection + escape hatch). On a 3,000-row nested-array synthetic: the 3,000+ `js_array_from_values` calls collapse to one `js_value_from_const_descriptor` + a rodata blob; the compile drops from not-finishing-in-2min to 1.46s; output is byte-identical to the procedural build across the moving-GC matrix, with mutation-after-materialize and bool/null rows verified. Claude-Session: https://claude.ai/code/session_01TwxRkALrR9HKSF1zKLSTAF --- .../perry-codegen/src/expr/array_literal.rs | 143 ++++++++++++ .../perry-codegen/src/runtime_decls/arrays.rs | 4 + crates/perry-runtime/src/array/alloc.rs | 89 ++++++++ .../tests/const_array_descriptor_8583.rs | 214 ++++++++++++++++++ 4 files changed, 450 insertions(+) create mode 100644 crates/perry/tests/const_array_descriptor_8583.rs diff --git a/crates/perry-codegen/src/expr/array_literal.rs b/crates/perry-codegen/src/expr/array_literal.rs index 25e7bf6b21..dc1098333c 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,133 @@ 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; + } + 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/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-runtime/src/array/alloc.rs b/crates/perry-runtime/src/array/alloc.rs index d1bd72fc48..058da970ba 100644 --- a/crates/perry-runtime/src/array/alloc.rs +++ b/crates/perry-runtime/src/array/alloc.rs @@ -444,6 +444,95 @@ pub extern "C" fn js_array_from_values(values: *const f64, n: u32) -> *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/tests/const_array_descriptor_8583.rs b/crates/perry/tests/const_array_descriptor_8583.rs new file mode 100644 index 0000000000..bdcb859b29 --- /dev/null +++ b/crates/perry/tests/const_array_descriptor_8583.rs @@ -0,0 +1,214 @@ +//! #8583 follow-up — large constant array literals materialize from a static +//! rodata descriptor + ONE bulk call, and the result is GC-correct, fresh, and +//! byte-identical to the procedural construction path. +//! +//! A minified bundle data table is a giant nested constant array literal. The +//! default lowering builds it with N per-subarray `js_array_from_values` +//! allocations and a huge procedural body (the `__33499` fan-out). The +//! descriptor path serializes the constant tree into a rodata blob and calls +//! `js_value_from_const_descriptor` once to materialize a FRESH, mutable array. +//! +//! Two checks, no node oracle: +//! * the optimization actually fired — the emitted IR calls +//! `js_value_from_const_descriptor` and does NOT build the table with a +//! per-subarray `js_array_from_values` (so this test can't silently become +//! a tautology if the path stops matching); +//! * `PERRY_CONST_ARRAY_DESCRIPTOR=1` (default) vs `=0` (procedural) produce +//! byte-identical output under every moving-collector arm — a mis-rooted +//! value in the suppressed-GC bulk build would diverge (or crash) in the +//! descriptor arm only. Mutation-after-materialization is exercised so a +//! wrongly-shared constant would surface as cross-instance aliasing. + +use std::path::PathBuf; +use std::process::Command; + +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +/// A large, fully-constant nested array (300 numeric rows > the 256-node gate), +/// one boolean/null row to cover non-numeric constants, then: a fresh second +/// materialization, a mutation of the first (freshness/mutability), and a +/// deterministic checksum. Output is hand-verifiable and identical whichever +/// construction path built the table. +fn source() -> String { + let mut rows = String::new(); + for i in 0..300 { + if i > 0 { + rows.push(','); + } + rows.push_str(&format!("[{},{},{}]", i % 128, (i * 7) % 128, (i * 13) % 128)); + } + // One non-numeric row so bool/null tags are exercised in the descriptor. + rows.push_str(",[true,null,false]"); + format!( + r#" +function table() {{ return [{rows}]; }} +const t = table(); +const t2 = table(); +t[0].push(999); +let sum = 0; +for (let i = 0; i < 300; i++) {{ sum = (sum + t[i][0] + t[i][1] + t[i][2]) | 0; }} +const row = t[300]; +console.log( + "rows:" + t.length + + " s:" + sum + + " mut:" + t[0].length + + " fresh:" + t2[0].length + + " id:" + (t === t2) + + " b:" + row[0] + " n:" + row[1] + " f:" + row[2] +); +"# + ) +} + +const GC_ENV_OVERRIDES: &[&str] = &[ + "PERRY_GEN_GC", + "PERRY_GC_SCAVENGE", + "PERRY_GC_SCAVENGE_NURSERY_MB", + "PERRY_GC_MOVING_SAFEPOINT", + "PERRY_GC_MOVING_LOOP_POLLS", + "PERRY_GC_FORCE_EVACUATE", + "PERRY_CONSERVATIVE_STACK_SCAN", + "PERRY_WRITE_BARRIERS", + "PERRY_GC_INCREMENTAL", + "PERRY_GC_HEAP_LIMIT", + "PERRY_CONST_ARRAY_DESCRIPTOR", +]; + +fn compile(dir: &std::path::Path, descriptor: bool) -> PathBuf { + let entry = dir.join("main.ts"); + let output = dir.join(format!("bin_desc_{descriptor}")); + std::fs::write(&entry, source()).expect("write entry"); + let mut cmd = Command::new(perry_bin()); + cmd.current_dir(dir) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .arg("--no-cache"); + for key in GC_ENV_OVERRIDES { + cmd.env_remove(key); + } + if !descriptor { + cmd.env("PERRY_CONST_ARRAY_DESCRIPTOR", "0"); + } + let out = cmd.output().expect("run perry compile"); + assert!( + out.status.success(), + "perry compile (descriptor={descriptor}) failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr), + ); + output +} + +fn run_arms(binary: &std::path::Path, dir: &std::path::Path, label: &str) -> String { + let mut arms: Vec> = vec![vec![]]; + for mb in ["1", "2", "4"] { + arms.push(vec![("PERRY_GC_SCAVENGE_NURSERY_MB", mb)]); + } + arms.push(vec![("PERRY_GEN_GC", "0")]); + + let mut first: Option = None; + for arm in &arms { + let mut cmd = Command::new(binary); + cmd.current_dir(dir); + for key in GC_ENV_OVERRIDES { + cmd.env_remove(key); + } + for (k, v) in arm { + cmd.env(k, v); + } + let run = cmd.output().expect("run compiled binary"); + let arm_label = if arm.is_empty() { + format!("{label}/default") + } else { + format!("{label}/{}={}", arm[0].0, arm[0].1) + }; + assert!( + run.status.success(), + "[{arm_label}] compiled binary failed (exit {:?})\nstderr:\n{}", + run.status.code(), + String::from_utf8_lossy(&run.stderr), + ); + let stdout = String::from_utf8_lossy(&run.stdout).into_owned(); + match &first { + None => first = Some(stdout), + Some(f) => assert_eq!( + &stdout, f, + "[{arm_label}] output differs between collector arms — a value \ + materialized in the suppressed-GC bulk build was mis-rooted" + ), + } + } + first.expect("at least one arm ran") +} + +#[test] +fn const_array_descriptor_fires_in_ir() { + let dir = tempfile::tempdir().expect("tempdir"); + let entry = dir.path().join("main.ts"); + let ll_dir = dir.path().join("ll"); + std::fs::create_dir_all(&ll_dir).unwrap(); + std::fs::write(&entry, source()).expect("write entry"); + let out = Command::new(perry_bin()) + .current_dir(dir.path()) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(dir.path().join("unused")) + .arg("--no-cache") + .arg("--no-link") + .env("PERRY_SAVE_LL", &ll_dir) + .env_remove("PERRY_CONST_ARRAY_DESCRIPTOR") + .output() + .expect("run perry compile --no-link"); + assert!(out.status.success(), "compile failed: {}", String::from_utf8_lossy(&out.stderr)); + + let ir: String = std::fs::read_dir(&ll_dir) + .unwrap() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("ll")) + .map(|e| std::fs::read_to_string(e.path()).unwrap_or_default()) + .collect(); + assert!( + ir.contains("js_value_from_const_descriptor"), + "the large constant table should materialize via js_value_from_const_descriptor" + ); + // The descriptor path must replace the per-subarray builder for this table: + // no `js_array_from_values` CALL should remain (the always-present `declare` + // line is filtered out). + assert!( + !ir.contains("call i64 @js_array_from_values("), + "no per-subarray js_array_from_values call should remain for the const table" + ); +} + +#[test] +fn const_array_descriptor_matches_procedural_under_moving_gc() { + let dir = tempfile::tempdir().expect("tempdir"); + let descriptor_bin = compile(dir.path(), true); + let procedural_bin = compile(dir.path(), false); + + let descriptor_out = run_arms(&descriptor_bin, dir.path(), "descriptor"); + let procedural_out = run_arms(&procedural_bin, dir.path(), "procedural"); + + // Structural correctness (robust to the exact checksum, which the + // differential below pins anyway): 301 rows; the two materializations are + // DISTINCT instances (id:false); mutating t[0] (3 -> 4 after push) did not + // touch the fresh t2[0] (still 3) — proving each call yields a fresh mutable + // array, not a shared constant; and the non-numeric row round-trips. + assert!( + descriptor_out.starts_with("rows:301 s:54810 mut:4 fresh:3 id:false"), + "unexpected descriptor output: {descriptor_out:?}" + ); + assert!( + descriptor_out.trim_end().ends_with("b:true n:null f:false"), + "non-numeric constants must round-trip: {descriptor_out:?}" + ); + assert_eq!( + descriptor_out, procedural_out, + "descriptor materialization diverged from the procedural construction path" + ); +} From f48cfdf8f47ac6d614d28365ca724f5ebad55f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 23 Aug 2026 12:07:47 +0200 Subject: [PATCH 02/10] docs(changelog): fragment for #8647 Claude-Session: https://claude.ai/code/session_01TwxRkALrR9HKSF1zKLSTAF --- changelog.d/8647-const-array-descriptor.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/8647-const-array-descriptor.md 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. From 29949babc319861c298870bd69a1c19ad0b7318f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 23 Aug 2026 11:48:44 +0200 Subject: [PATCH 03/10] fix(runtime): complete computed property name reflection --- .../object/class_registry/parent_static.rs | 6 +- .../src/object/class_registry/state.rs | 56 ++++++++++++++----- crates/perry-runtime/src/symbol/iterator.rs | 23 +++++++- crates/perry-runtime/src/value/dyn_index.rs | 15 +++-- ...test_issue_5894_computed_property_names.ts | 46 +++++++++++++++ 5 files changed, 126 insertions(+), 20 deletions(-) create mode 100644 test-files/test_issue_5894_computed_property_names.ts 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..59a00c8de3 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static.rs @@ -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/symbol/iterator.rs b/crates/perry-runtime/src/symbol/iterator.rs index 54809d809f..a68c014808 100644 --- a/crates/perry-runtime/src/symbol/iterator.rs +++ b/crates/perry-runtime/src/symbol/iterator.rs @@ -56,13 +56,34 @@ pub unsafe extern "C" fn js_object_get_own_property_symbols(obj_f64: f64) -> i64 if obj_key == 0 { return crate::array::js_array_alloc(0) as i64; } + // A declared class prototype is a materialized ObjectHeader, while its + // computed Symbol methods/accessors live in the class registry. Seed the + // ordinary-object enumeration with those own keys so + // `Object.getOwnPropertySymbols(C.prototype)` sees `[sym]() {}` exactly as + // direct `C.prototype[sym]` dispatch does. A later assignment to the same + // symbol is deduplicated below; class elements precede such assignments in + // property-creation order. + let mut entries: Vec<(usize, u64)> = crate::object::class_id_for_decl_prototype_object(obj_key) + .map(|class_id| { + crate::object::class_own_symbol_member_keys(class_id, false) + .into_iter() + .map(|sym_key| (sym_key, 0)) + .collect() + }) + .unwrap_or_default(); + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); - let mut entries = guard + let stored_entries = guard .as_ref() .and_then(|m| m.get(&obj_key)) .cloned() .unwrap_or_default(); drop(guard); + for entry in stored_entries { + if !entries.iter().any(|(sym_key, _)| *sym_key == entry.0) { + entries.push(entry); + } + } // `entries` is the full own-symbol-key list in property-CREATION order: // data entries hold their value, accessor properties hold an // order-preserving placeholder written by `set_symbol_accessor_property` diff --git a/crates/perry-runtime/src/value/dyn_index.rs b/crates/perry-runtime/src/value/dyn_index.rs index 491311bdc2..4e3d6bc215 100644 --- a/crates/perry-runtime/src/value/dyn_index.rs +++ b/crates/perry-runtime/src/value/dyn_index.rs @@ -409,11 +409,16 @@ pub extern "C" fn js_dyn_index_get(value: f64, index: f64) -> f64 { ); } } - let idx_i32 = if index.is_nan() || index.is_infinite() { - return f64::from_bits(TAG_UNDEFINED); - } else { - index as i32 - }; + // NaN and +/-Infinity are not array indices, but they are still ordinary + // property keys (`"NaN"`, `"Infinity"`, `"-Infinity"`) on Objects and + // Arrays. Delegate this cold case to the polymorphic key path, which runs + // ToPropertyKey and already distinguishes ordinary from integer-indexed + // exotic receivers. The old early return made a computed definition such + // as `{ [Infinity]: value }` unreadable through `obj[Infinity]`. + if index.is_nan() || index.is_infinite() { + return crate::object::js_object_get_index_polymorphic(raw_ptr as i64, index); + } + let idx_i32 = index as i32; if idx_i32 >= 0 { if let Some(value) = unsafe { crate::object::arguments_object_get_index( diff --git a/test-files/test_issue_5894_computed_property_names.ts b/test-files/test_issue_5894_computed_property_names.ts new file mode 100644 index 0000000000..4db31bd3c2 --- /dev/null +++ b/test-files/test_issue_5894_computed_property_names.ts @@ -0,0 +1,46 @@ +// Issue #5894: computed property keys must stay visible through the same +// reflective surfaces as non-computed properties. + +const sym1 = Symbol("one"); +const sym2 = Symbol("two"); + +class C { + ["constructor"](): number { + return 1; + } + + [sym1](): string { + return "first"; + } + + [((value: symbol): symbol => value)(sym2)](): string { + return "second"; + } +} + +const instance = new C(); +const prototypeSymbols = Object.getOwnPropertySymbols(C.prototype); +console.log(C === C.prototype.constructor); +console.log( + Object.getOwnPropertyDescriptor(C.prototype, "constructor")?.value === C, +); +console.log(instance.constructor()); +console.log(instance[sym1]()); +console.log(instance[sym2]()); +console.log(prototypeSymbols.length); +console.log(prototypeSymbols[0] === sym1); +console.log(prototypeSymbols[1] === sym2); + +const numericKeys = { + [1.2]: "finite", + [-0]: "zero", + [Infinity]: "positive infinity", + [-Infinity]: "negative infinity", + [NaN]: "not a number", +}; + +console.log(numericKeys[1.2]); +console.log(numericKeys[-0]); +console.log(numericKeys[Infinity]); +console.log(numericKeys[-Infinity]); +console.log(numericKeys[NaN]); From b285d75576eaa7834cf5eeec32a8eb5c3568c807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 23 Aug 2026 12:57:57 +0200 Subject: [PATCH 04/10] perf(codegen): restrict the const-array descriptor to NESTED literals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A flat constant scalar array (e.g. `[0; 2050]`) is already a single `js_array_alloc_literal` + inline stores — not the per-subarray fan-out the descriptor targets — and its inline path carries the precise per-slot write barriers a later push/store depends on (large_object_barriers). Gate the descriptor path on the literal containing at least one nested array element, so only genuine nested data tables (the __33499 shape) take it; flat arrays keep their existing path. Verified: the nested 3,000-row synthetic still collapses to one js_value_from_const_descriptor call, and large_object_barriers passes. Claude-Session: https://claude.ai/code/session_01TwxRkALrR9HKSF1zKLSTAF --- crates/perry-codegen/src/expr/array_literal.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/perry-codegen/src/expr/array_literal.rs b/crates/perry-codegen/src/expr/array_literal.rs index dc1098333c..fa49bafa53 100644 --- a/crates/perry-codegen/src/expr/array_literal.rs +++ b/crates/perry-codegen/src/expr/array_literal.rs @@ -359,6 +359,14 @@ fn try_lower_const_array_descriptor(ctx: &mut FnCtx<'_>, elements: &[Expr]) -> O 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; From 6cd25e13c50d07c54d73ad888f08ed4860cf358d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 23 Aug 2026 16:46:21 +0200 Subject: [PATCH 05/10] fix(runtime): keep old array growth targets out of nursery --- crates/perry-runtime/src/array/push_pop.rs | 35 ++++++++++++++++++-- crates/perry-runtime/src/array/tests.rs | 37 ++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) 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 From acf6e722e35f93faad0dfe379e3f880526f20903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 23 Aug 2026 17:18:19 +0200 Subject: [PATCH 06/10] docs(changelog): note array growth generation fix --- changelog.d/8651-array-growth-generation.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 changelog.d/8651-array-growth-generation.md 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`. From 87df08b9f3419b76f099359f97651844d8570c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 23 Aug 2026 18:58:53 +0200 Subject: [PATCH 07/10] fix(codegen): guard block-creating lowerings against diverged (terminated) blocks (#8583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a sub-expression provably diverges — a throwing operand (e.g. a captured TDZ access or const-reassignment) emits `js_throw_error_with_code` + `unreachable` — the current block is terminated. `LlBlock` silently drops any instruction emitted after a terminator (block.rs), so the setup instructions for the surrounding operation are discarded; but block-creating lowerings still emit fresh blocks that reference those dropped `%rN` registers, which the dialect builder rejects with "register %rN used but never defined" (dialect/mod.rs). The whole surrounding operation is unreachable on that path, so the fix is to emit nothing once the block is terminated. Two sites hit this in the Claude Code 2.1.112 bundle (both dead code after a proven-throwing operand): `lower_index_set_fast` (`a[i] = v`, closure `__44845`) and `emit_persistent_shadow_root_barrier` (a pointer root store, closure `__44449`). Each now returns early when `ctx.block().is_terminated()`. Also adds a `PERRY_DIALECT_DUMP=` diagnostic: on a dialect construction failure, `render_units_from_frozen` names the offending function and writes its full IR (typed insts rendered via `render_into`) — the failing unit never parses, so the normal `PERRY_SAVE_LL` post-parse dump cannot capture it. This is how the two sites above were located. Validated end-to-end: with these guards, the cli.js bundle codegens ALL 84 units with zero "used but never defined" errors (it previously failed at unit 25); the remaining blocker to a final binary is unrelated (host disk). Claude-Session: https://claude.ai/code/session_01TwxRkALrR9HKSF1zKLSTAF --- crates/perry-codegen/src/expr/index.rs | 13 +++++ crates/perry-codegen/src/expr/shadow_slot.rs | 10 ++++ crates/perry-codegen/src/native_emit.rs | 54 +++++++++++++++++--- 3 files changed, 70 insertions(+), 7 deletions(-) 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/native_emit.rs b/crates/perry-codegen/src/native_emit.rs index b21d815308..d5edc5d6db 100644 --- a/crates/perry-codegen/src/native_emit.rs +++ b/crates/perry-codegen/src/native_emit.rs @@ -255,20 +255,60 @@ 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 From 1645fb9597629baf566fa8ce8c7f4b8815d8bd49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 23 Aug 2026 18:59:32 +0200 Subject: [PATCH 08/10] docs(changelog): fragment for #8652 Claude-Session: https://claude.ai/code/session_01TwxRkALrR9HKSF1zKLSTAF --- changelog.d/8652-diverged-block-guards.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/8652-diverged-block-guards.md 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). From 5abb1225c1012f082c4f6b628800a2c74e45598e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 23 Aug 2026 13:10:26 +0200 Subject: [PATCH 09/10] fix: complete test262 built-ins misc tail semantics --- crates/perry-codegen/src/codegen/helpers.rs | 13 + crates/perry-codegen/src/expr/unary.rs | 2 +- .../runtime_decls/stdlib_ffi/language_core.rs | 1 + .../src/runtime_decls/strings.rs | 1 + crates/perry-codegen/src/type_analysis.rs | 4 +- crates/perry-hir/src/lower/const_fold_fn.rs | 77 ++--- .../lower/const_fold_fn/param_early_error.rs | 116 ++++++++ crates/perry-hir/src/lower/expr_assign.rs | 25 ++ .../src/lower/expr_call/url_date_instance.rs | 21 +- .../src/lower/expr_member/member_tail.rs | 34 +++ crates/perry-hir/src/lower/expr_new.rs | 45 ++- .../perry-hir/src/lower/expr_new/helpers.rs | 2 +- .../src/lower/lower_expr/arm_unary.rs | 25 ++ crates/perry-hir/src/lower/lower_module_fn.rs | 29 ++ crates/perry-hir/src/lower/stmt_loops.rs | 10 +- crates/perry-hir/src/lower_decl/body_stmt.rs | 6 + crates/perry-runtime/src/array/iter_object.rs | 72 +++-- crates/perry-runtime/src/array/iterator.rs | 11 +- crates/perry-runtime/src/array/mod.rs | 6 +- crates/perry-runtime/src/buffer/dataview.rs | 48 +--- crates/perry-runtime/src/buffer/from.rs | 2 +- crates/perry-runtime/src/buffer/mod.rs | 5 +- crates/perry-runtime/src/buffer/own_props.rs | 48 ++++ .../perry-runtime/src/builtins/arithmetic.rs | 13 +- .../builtins/formatting/boxed_primitives.rs | 13 +- crates/perry-runtime/src/builtins/numbers.rs | 10 +- .../src/collection_iter_object.rs | 122 +++++--- crates/perry-runtime/src/error.rs | 14 + .../src/gc/tests/lazy_intrinsic_towers.rs | 2 +- crates/perry-runtime/src/map.rs | 22 +- crates/perry-runtime/src/object/arguments.rs | 50 ++++ .../src/object/async_generator_queue.rs | 93 ++++++- .../src/object/buffer_dispatch.rs | 48 +++- .../src/object/class_registry.rs | 18 +- .../src/object/class_registry/construct.rs | 154 ++-------- .../class_registry/construct/class_return.rs | 11 +- .../class_registry/function_prototype.rs | 104 +++++++ .../object/class_registry/parent_static.rs | 2 +- .../src/object/collection_proto_thunks.rs | 17 ++ .../src/object/data_view_registry.rs | 47 ++++ .../src/object/date_proto_thunks.rs | 17 ++ .../perry-runtime/src/object/delete_rest.rs | 22 ++ .../perry-runtime/src/object/descriptors.rs | 52 +++- .../object/field_get_set/buffer_own_prop.rs | 7 + .../field_get_set/get_field_by_name_tail.rs | 26 +- .../perry-runtime/src/object/global_this.rs | 33 ++- .../src/object/global_this/ctor_thunks.rs | 6 + .../src/object/global_this/generator.rs | 144 +++++++++- .../src/object/global_this/install_static.rs | 13 + .../src/object/global_this/populate.rs | 12 + .../src/object/global_this/proto_methods.rs | 8 +- .../src/object/global_this/typed_array.rs | 145 +++++++++- .../src/object/iterator_prototypes.rs | 59 ++++ crates/perry-runtime/src/object/mod.rs | 16 +- .../native_call_method/primitive_methods.rs | 16 +- .../src/object/object_ops/define_property.rs | 262 ++++++++++++++++++ .../src/object/object_ops/prototype.rs | 14 + .../src/object/polymorphic_index.rs | 6 + .../src/object/primitive_proto_thunks.rs | 22 +- .../src/object/prototype_chain.rs | 4 + .../perry-runtime/src/promise/async_step.rs | 54 +++- crates/perry-runtime/src/promise/mod.rs | 4 +- crates/perry-runtime/src/proxy.rs | 71 +++++ crates/perry-runtime/src/proxy/put_value.rs | 10 + crates/perry-runtime/src/set.rs | 17 +- .../perry-runtime/src/string/iter_object.rs | 56 ++-- crates/perry-runtime/src/symbol.rs | 9 +- crates/perry-runtime/src/symbol/iterator.rs | 6 +- crates/perry-runtime/src/symbol/properties.rs | 14 + crates/perry-runtime/src/typedarray/bigint.rs | 8 +- .../perry-runtime/src/typedarray/construct.rs | 64 +++-- crates/perry-runtime/src/typedarray_props.rs | 27 +- crates/perry-runtime/src/value/dyn_index.rs | 68 ++++- .../perry-runtime/src/value/dynamic_arith.rs | 12 + crates/perry-runtime/src/value/mod.rs | 6 +- crates/perry-runtime/src/value/to_string.rs | 27 +- crates/perry-transform/src/generator/lower.rs | 2 +- scripts/gc_runtime_root_holders.json | 16 +- scripts/raw_handle_debt_baseline.txt | 2 +- scripts/raw_handle_debt_files.txt | 4 +- 80 files changed, 2199 insertions(+), 505 deletions(-) create mode 100644 crates/perry-hir/src/lower/const_fold_fn/param_early_error.rs create mode 100644 crates/perry-runtime/src/object/class_registry/function_prototype.rs 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/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/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 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/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..01c05a9a3c 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; } 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>> = RwLock::new(None); +static EXTENDS_TYPED_ARRAY_REGISTRY: RwLock>> = + RwLock::new(None); /// Mark a user-defined class as extending the built-in DataView class. #[no_mangle] @@ -39,3 +41,48 @@ pub(crate) fn extends_builtin_data_view(class_id: u32) -> bool { } false } + +#[no_mangle] +pub extern "C" fn js_register_class_extends_typed_array(class_id: u32) { + let mut registry = EXTENDS_TYPED_ARRAY_REGISTRY.write().unwrap(); + registry + .get_or_insert_with(std::collections::HashSet::new) + .insert(class_id); +} + +pub(crate) fn register_builtin_view_parent(class_id: u32, parent_name: &str) { + if parent_name == "DataView" { + js_register_class_extends_data_view(class_id); + } else if crate::typedarray::kind_for_name(parent_name).is_some() { + js_register_class_extends_typed_array(class_id); + } +} + +pub(crate) fn extends_builtin_typed_array(class_id: u32) -> bool { + let registry = EXTENDS_TYPED_ARRAY_REGISTRY.read().unwrap(); + let Some(registered) = registry.as_ref() else { + return false; + }; + if registered.contains(&class_id) { + return true; + } + let mut current = class_id; + let parent_reg = super::CLASS_REGISTRY.read().unwrap(); + if let Some(parents) = parent_reg.as_ref() { + // A valid parent chain cannot visit more entries than the registry + // contains. This follows arbitrarily deep user hierarchies while still + // terminating if malformed registry data contains a cycle. + for _ in 0..=parents.len() { + match parents.get(¤t).copied() { + Some(parent) if parent != 0 => { + if registered.contains(&parent) { + return true; + } + current = parent; + } + _ => break, + } + } + } + false +} diff --git a/crates/perry-runtime/src/object/date_proto_thunks.rs b/crates/perry-runtime/src/object/date_proto_thunks.rs index c58a56c389..9c45dd9b23 100644 --- a/crates/perry-runtime/src/object/date_proto_thunks.rs +++ b/crates/perry-runtime/src/object/date_proto_thunks.rs @@ -96,6 +96,17 @@ extern "C" fn date_to_utc_string(_closure: *const crate::closure::ClosureHeader) crate::value::js_nanbox_string(s as i64) } +#[cfg(feature = "temporal")] +extern "C" fn date_to_temporal_instant(_closure: *const crate::closure::ClosureHeader) -> f64 { + let timestamp = require_date_timestamp(); + crate::temporal::instant::from_epoch_milliseconds_static(&[timestamp]) +} + +#[cfg(not(feature = "temporal"))] +extern "C" fn date_to_temporal_instant(_closure: *const crate::closure::ClosureHeader) -> f64 { + f64::from_bits(crate::value::TAG_UNDEFINED) +} + /// `Date.prototype.toJSON` reflective thunk. Unlike the getters this is a /// *generic* method — it is not brand-checked to Date. Per ECMA-262 /// (`thisTimeValue` is NOT used): `ToObject(this)`, then `ToPrimitive(this, @@ -406,6 +417,12 @@ pub(crate) fn install_date_proto_getters(proto_obj: *mut ObjectHeader) { 0, ); super::global_this::install_proto_method(proto_obj, "toJSON", date_to_json as *const u8, 1); + super::global_this::install_proto_method( + proto_obj, + "toTemporalInstant", + date_to_temporal_instant as *const u8, + 0, + ); let utc = super::global_this::install_proto_method( proto_obj, "toUTCString", diff --git a/crates/perry-runtime/src/object/delete_rest.rs b/crates/perry-runtime/src/object/delete_rest.rs index a44acfd53b..4b7015e949 100644 --- a/crates/perry-runtime/src/object/delete_rest.rs +++ b/crates/perry-runtime/src/object/delete_rest.rs @@ -97,6 +97,22 @@ pub extern "C" fn js_object_delete_field( key, ); } + // ArrayBuffer / SharedArrayBuffer / DataView are registered + // BufferHeaders with ordinary named expandos. They must not fall + // through to the ObjectHeader keys-array walk. + if crate::buffer::is_registered_buffer(obj as usize) { + if let Some(name) = super::has_own_helpers::str_from_string_header(key) { + if let Some(attrs) = get_property_attrs(obj as usize, name) { + if !attrs.configurable() { + return 0; + } + } + crate::buffer::buffer_delete_own_prop(obj as usize, name); + super::clear_accessor_descriptor(obj as usize, name); + super::clear_property_attrs(obj as usize, name); + } + return 1; + } if let Some(result) = super::arguments_object_before_delete(obj, key) { return result; } @@ -115,6 +131,12 @@ pub extern "C" fn js_object_delete_field( return 0; } super::exotic_expando::value_remove(kind, obj as usize, name); + if kind == ExoticKind::Error { + crate::error::js_error_delete_builtin_own_property( + obj as *mut crate::error::ErrorHeader, + name, + ); + } super::clear_accessor_descriptor(obj as usize, name); super::clear_property_attrs(obj as usize, name); } diff --git a/crates/perry-runtime/src/object/descriptors.rs b/crates/perry-runtime/src/object/descriptors.rs index b75fbe0173..79d16cd3f1 100644 --- a/crates/perry-runtime/src/object/descriptors.rs +++ b/crates/perry-runtime/src/object/descriptors.rs @@ -275,6 +275,47 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu ); } + if obj_jv.is_pointer() { + let addr = crate::value::js_nanbox_get_pointer(obj_value) as usize; + if crate::buffer::is_registered_buffer(addr) { + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(obj_value); + let Some(name) = metadata_key_to_string(key_value) else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + let addr = crate::value::js_nanbox_get_pointer(receiver.get_nanbox_f64()) as usize; + if let Some(accessor) = get_accessor_descriptor(addr, &name) { + let attrs = get_property_attrs(addr, &name) + .unwrap_or_else(|| PropertyAttrs::new(false, false, false)); + return build_accessor_descriptor( + f64::from_bits(if accessor.get == 0 { + crate::value::TAG_UNDEFINED + } else { + accessor.get + }), + f64::from_bits(if accessor.set == 0 { + crate::value::TAG_UNDEFINED + } else { + accessor.set + }), + attrs.enumerable(), + attrs.configurable(), + ); + } + let Some(value) = crate::buffer::buffer_get_own_prop(addr, &name) else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + let attrs = get_property_attrs(addr, &name) + .unwrap_or_else(|| PropertyAttrs::new(true, true, true)); + return build_data_descriptor( + value, + attrs.writable(), + attrs.enumerable(), + attrs.configurable(), + ); + } + } + // Date / RegExp / Error exotic instances: own properties live in the // expando side tables (plus a few builtin own slots), never in an // `ObjectHeader` — the ordinary path below would bit-cast the cell. @@ -1014,12 +1055,14 @@ pub(crate) unsafe fn build_data_descriptor( const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; let bf = |b: bool| f64::from_bits(if b { TAG_TRUE } else { TAG_FALSE }); + let scope = crate::gc::RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(value); let packed = b"value\0writable\0enumerable\0configurable"; let desc = js_object_alloc_with_shape(0x0D_E5_C0, 4, packed.as_ptr(), packed.len() as u32); let header_size = std::mem::size_of::(); let fields = (desc as *mut u8).add(header_size) as *mut f64; // GC_STORE_AUDIT(INIT): descriptor object is freshly allocated; layout is rebuilt before publication. - *fields = value; + *fields = value.get_nanbox_f64(); *fields.add(1) = bf(writable); *fields.add(2) = bf(enumerable); *fields.add(3) = bf(configurable); @@ -1036,13 +1079,16 @@ pub(crate) unsafe fn build_accessor_descriptor( const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; let bf = |b: bool| f64::from_bits(if b { TAG_TRUE } else { TAG_FALSE }); + let scope = crate::gc::RuntimeHandleScope::new(); + let get = scope.root_nanbox_f64(get); + let set = scope.root_nanbox_f64(set); let packed = b"get\0set\0enumerable\0configurable"; let desc = js_object_alloc_with_shape(0x0D_E5_C1, 4, packed.as_ptr(), packed.len() as u32); let header_size = std::mem::size_of::(); let fields = (desc as *mut u8).add(header_size) as *mut f64; // GC_STORE_AUDIT(INIT): descriptor object is freshly allocated; layout is rebuilt before publication. - *fields = get; - *fields.add(1) = set; + *fields = get.get_nanbox_f64(); + *fields.add(1) = set.get_nanbox_f64(); *fields.add(2) = bf(enumerable); *fields.add(3) = bf(configurable); super::rebuild_object_field_layout(desc, 4); diff --git a/crates/perry-runtime/src/object/field_get_set/buffer_own_prop.rs b/crates/perry-runtime/src/object/field_get_set/buffer_own_prop.rs index b5fb088114..934d1ab998 100644 --- a/crates/perry-runtime/src/object/field_get_set/buffer_own_prop.rs +++ b/crates/perry-runtime/src/object/field_get_set/buffer_own_prop.rs @@ -25,6 +25,13 @@ pub(super) fn buffer_own_prop_or_method( key_bytes: &[u8], ) -> Option { let name = std::str::from_utf8(key_bytes).ok()?; + if let Some(accessor) = super::get_accessor_descriptor(obj as usize, name) { + if accessor.get == 0 { + return Some(JSValue::undefined()); + } + let receiver = crate::value::js_nanbox_pointer(obj as i64); + return Some(unsafe { super::invoke_accessor_getter(accessor.get, receiver) }); + } if let Some(v) = crate::buffer::buffer_get_own_prop(obj as usize, name) { return Some(JSValue::from_bits(v.to_bits())); } diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs index 9ab7ca06ce..da053cb3aa 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs @@ -593,6 +593,12 @@ pub(crate) fn get_field_by_name_object_tail( if val.to_bits() != crate::value::TAG_UNDEFINED { return JSValue::from_bits(val.to_bits()); } + if matches!(name_str, "caller" | "arguments") { + crate::fs::validate::throw_type_error_with_code( + "Restricted function property access", + "ERR_INVALID_ARG_TYPE", + ); + } // #3664: `g.constructor` for a generator/async-generator // function resolves through its [[Prototype]] (`%Generator%`) // to `%GeneratorFunction%` / `%AsyncGeneratorFunction%`. @@ -604,6 +610,11 @@ pub(crate) fn get_field_by_name_object_tail( { return JSValue::from_bits(ctor.to_bits()); } + let ctor = + super::super::js_get_global_this_builtin_value(b"Function".as_ptr(), 8); + if !JSValue::from_bits(ctor.to_bits()).is_undefined() { + return JSValue::from_bits(ctor.to_bits()); + } } // #3664: `g.prototype` for a generator/async-generator // function is a lazily-created object whose [[Prototype]] is @@ -704,12 +715,6 @@ pub(crate) fn get_field_by_name_object_tail( let v = crate::error::js_error_get_cause(err_ptr); return JSValue::from_bits(v.to_bits()); } - b"toString" => { - let this_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - let result = js_class_method_bind(this_f64, b"toString".as_ptr(), 8); - return JSValue::from_bits(result.to_bits()); - } b"constructor" => { let name = crate::error::error_kind_constructor_name((*err_ptr).error_kind); let name = name.as_bytes(); @@ -1209,8 +1214,11 @@ pub(crate) fn get_field_by_name_object_tail( let key_ptr = (key as *const u8).add(std::mem::size_of::()); let key_len = (*key).byte_len as usize; let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + // `next` is an ordinary prototype method. Do not bind it to the + // iterator at property-read time: `iterator.next.call(other)` must + // receive `other` and perform the spec brand check. The remaining + // legacy synthetic methods still use the bound-method path. let bind_name: Option<&'static [u8]> = match key_bytes { - b"next" => Some(b"next"), b"return" => Some(b"return"), b"throw" => Some(b"throw"), b"@@iterator" => Some(b"@@iterator"), @@ -1222,7 +1230,9 @@ pub(crate) fn get_field_by_name_object_tail( let result = js_class_method_bind(this_f64, name.as_ptr(), name.len()); return JSValue::from_bits(result.to_bits()); } - return JSValue::undefined(); + if key_bytes != b"next" { + return JSValue::undefined(); + } } // Issue #649: native-module sub-namespace property access. diff --git a/crates/perry-runtime/src/object/global_this.rs b/crates/perry-runtime/src/object/global_this.rs index 5228586c4c..8dd1613f0a 100644 --- a/crates/perry-runtime/src/object/global_this.rs +++ b/crates/perry-runtime/src/object/global_this.rs @@ -93,19 +93,20 @@ pub(crate) use builtin_thunks::{ }; pub use ctor_thunks::js_webcrypto_illegal_constructor; pub(crate) use ctor_thunks::{ - builtin_prototype_value, cryptokey_algorithm_getter_thunk, cryptokey_extractable_getter_thunk, - cryptokey_type_getter_thunk, cryptokey_usages_getter_thunk, error_constructor_call_thunk, - eval_error_constructor_call_thunk, global_this_crypto_getter_thunk, - global_this_url_pattern_call_thunk, is_array_prototype_method_value, - is_function_prototype_object_value, map_constructor_call_thunk, normalize_eval_this_body, - promise_constructor_call_thunk, range_error_constructor_call_thunk, - reference_error_constructor_call_thunk, regexp_constructor_call_thunk, - set_constructor_call_thunk, subtle_crypto_method_value, syntax_error_constructor_call_thunk, - type_error_constructor_call_thunk, typed_array_constructor_call_thunk, - uri_error_constructor_call_thunk, weak_map_constructor_call_thunk, - weak_ref_constructor_call_thunk, weak_set_constructor_call_thunk, - webcrypto_get_random_values_thunk, webcrypto_illegal_constructor_thunk, webcrypto_method_value, - webcrypto_random_uuid_thunk, webcrypto_subtle_getter_thunk, + builtin_prototype_value, construct_only_builtin_call_thunk, cryptokey_algorithm_getter_thunk, + cryptokey_extractable_getter_thunk, cryptokey_type_getter_thunk, cryptokey_usages_getter_thunk, + error_constructor_call_thunk, eval_error_constructor_call_thunk, + global_this_crypto_getter_thunk, global_this_url_pattern_call_thunk, + is_array_prototype_method_value, is_function_prototype_object_value, + map_constructor_call_thunk, normalize_eval_this_body, promise_constructor_call_thunk, + range_error_constructor_call_thunk, reference_error_constructor_call_thunk, + regexp_constructor_call_thunk, set_constructor_call_thunk, subtle_crypto_method_value, + syntax_error_constructor_call_thunk, type_error_constructor_call_thunk, + typed_array_constructor_call_thunk, uri_error_constructor_call_thunk, + weak_map_constructor_call_thunk, weak_ref_constructor_call_thunk, + weak_set_constructor_call_thunk, webcrypto_get_random_values_thunk, + webcrypto_illegal_constructor_thunk, webcrypto_method_value, webcrypto_random_uuid_thunk, + webcrypto_subtle_getter_thunk, }; #[cfg(feature = "temporal")] pub(crate) use fetch_globals::temporal_subclass_super; @@ -120,9 +121,12 @@ pub use fetch_globals::{ js_fetch_or_value_super, js_get_global_this, js_global_or_console_property_by_name, js_module_top_this, js_request_subclass_init, js_response_subclass_init, }; +#[cfg(test)] +pub(crate) use generator::append_async_function_root_snapshot; pub(crate) use generator::{ ensure_generator_intrinsics, generator_function_constructor_of, generator_function_proto_of, generator_function_prototype_of, set_intrinsic_data_prop, set_intrinsic_to_string_tag, + wire_async_function_intrinsic_parents, }; pub use generator::{js_generator_attach_closure_prototype, js_generator_attach_prototype}; pub use install_static::js_promise_static_function_value; @@ -149,8 +153,9 @@ pub(crate) use proto_methods::{ install_error_prototype_data_properties, populate_builtin_prototype_methods, }; pub(crate) use typed_array::{ - array_buffer_byte_length_getter_thunk, array_buffer_is_view_thunk, + array_buffer_byte_length_getter_thunk, array_buffer_is_view_thunk, array_buffer_slice_thunk, ensure_typed_array_intrinsic, install_function_has_instance_symbol, shared_array_buffer_byte_length_getter_thunk, shared_array_buffer_slice_thunk, typed_array_constructor_this_kind, typed_array_intrinsic_proto_ptr, + validate_array_buffer_species_constructor, }; diff --git a/crates/perry-runtime/src/object/global_this/ctor_thunks.rs b/crates/perry-runtime/src/object/global_this/ctor_thunks.rs index 60430e6619..0cc46e03b5 100644 --- a/crates/perry-runtime/src/object/global_this/ctor_thunks.rs +++ b/crates/perry-runtime/src/object/global_this/ctor_thunks.rs @@ -24,6 +24,12 @@ pub(crate) extern "C" fn typed_array_constructor_call_thunk( super::super::object_ops::throw_object_type_error(b"Constructor %TypedArray% requires 'new'") } +pub(crate) extern "C" fn construct_only_builtin_call_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + super::super::object_ops::throw_object_type_error(b"Constructor requires 'new'") +} + /// `RegExp(pattern, flags)` called WITHOUT `new` — unlike Map/Set below, /// RegExp IS callable: ECMA-262 22.2.4 makes the call form construct exactly /// like `new RegExp(pattern, flags)`, with one identity shortcut — `RegExp(re)` diff --git a/crates/perry-runtime/src/object/global_this/generator.rs b/crates/perry-runtime/src/object/global_this/generator.rs index 28026aff9a..4ca1337ff4 100644 --- a/crates/perry-runtime/src/object/global_this/generator.rs +++ b/crates/perry-runtime/src/object/global_this/generator.rs @@ -1,5 +1,25 @@ use super::*; +#[cfg(test)] +pub(crate) fn append_async_function_root_snapshot(roots: &mut Vec<(&'static str, usize, u64)>) { + for (name, slot) in [ + ( + "ASYNC_FUNCTION_INTRINSIC_PTR", + &crate::object::ASYNC_FUNCTION_INTRINSIC_PTR, + ), + ( + "ASYNC_FUNCTION_INTRINSIC_PROTO_PTR", + &crate::object::ASYNC_FUNCTION_INTRINSIC_PROTO_PTR, + ), + ] { + roots.push(( + name, + slot.test_slot_addr(), + slot.load(Ordering::Acquire) as u64, + )); + } +} + /// Distinguishes plain vs async generator closures for the intrinsic-tower /// lookups. #[derive(Clone, Copy, PartialEq, Eq)] @@ -30,6 +50,18 @@ fn closure_generator_kind(closure_ptr: usize) -> Option { } } +/// Plain async closures have their own hidden `%AsyncFunction%` intrinsic +/// tower. Async generators are intentionally excluded: they use the separate +/// async-generator tower classified above. +fn is_plain_async_function(closure_ptr: usize) -> bool { + let closure = closure_ptr as *const crate::closure::ClosureHeader; + let func_ptr = crate::closure::get_valid_func_ptr(closure); + !func_ptr.is_null() + && crate::closure::is_registered_async_function(func_ptr) + && !crate::closure::is_registered_generator_function(func_ptr) + && !crate::closure::is_registered_async_generator_function(func_ptr) +} + fn intrinsic_pointer_value(slot: i64) -> Option { if slot != 0 { Some(crate::value::js_nanbox_pointer(slot)) @@ -43,6 +75,12 @@ fn intrinsic_pointer_value(slot: i64) -> Option { /// `None` for non-generator closures so the caller keeps its existing /// `closure_static_prototype` / null resolution. (#3664) pub(crate) fn generator_function_proto_of(closure_ptr: usize) -> Option { + if is_plain_async_function(closure_ptr) { + ensure_generator_intrinsics(); + return intrinsic_pointer_value( + crate::object::ASYNC_FUNCTION_INTRINSIC_PROTO_PTR.load(Ordering::Acquire), + ); + } let kind = closure_generator_kind(closure_ptr)?; // The towers are normally built in `populate_global_this_builtins`, but a // program that reflects on a generator without ever touching `globalThis` @@ -57,6 +95,65 @@ pub(crate) fn generator_function_proto_of(closure_ptr: usize) -> Option { intrinsic_pointer_value(slot) } +/// Build the hidden `%AsyncFunction%` constructor and its ordinary, +/// non-callable `.prototype` object. The parent links are attached separately +/// once the global `Function` constructor has been populated. +fn build_async_function_tower() { + let _no_move = crate::gc::GcSuppressScope::new(); + let noop = global_this_builtin_noop_thunk as *const u8; + let ctor = crate::closure::js_closure_alloc(noop, 0); + let proto = js_object_alloc(0, 0); + if ctor.is_null() || proto.is_null() { + return; + } + let configurable = super::super::PropertyAttrs::new(false, false, true); + let fixed = super::super::PropertyAttrs::new(false, false, false); + + crate::closure::js_register_closure_arity(noop, 1); + super::super::native_module::set_bound_native_closure_name(ctor, "AsyncFunction"); + super::super::native_module::set_builtin_closure_length(ctor as usize, 1); + super::super::set_builtin_property_attrs(ctor as usize, "name".to_string(), configurable); + super::super::set_builtin_property_attrs(ctor as usize, "length".to_string(), configurable); + set_intrinsic_data_prop( + ctor as *mut ObjectHeader, + "prototype", + crate::value::js_nanbox_pointer(proto as i64), + fixed, + ); + set_intrinsic_data_prop( + proto, + "constructor", + crate::value::js_nanbox_pointer(ctor as i64), + configurable, + ); + set_intrinsic_to_string_tag(proto, "AsyncFunction"); + + crate::object::ASYNC_FUNCTION_INTRINSIC_PTR.store(ctor as i64, Ordering::Release); + crate::object::ASYNC_FUNCTION_INTRINSIC_PROTO_PTR.store(proto as i64, Ordering::Release); +} + +/// Complete `%AsyncFunction%.__proto__ = Function` and +/// `%AsyncFunction.prototype%.__proto__ = Function.prototype` after the +/// global constructor table exists. +pub(crate) fn wire_async_function_intrinsic_parents() { + let ctor = crate::object::ASYNC_FUNCTION_INTRINSIC_PTR.load(Ordering::Acquire); + let proto = crate::object::ASYNC_FUNCTION_INTRINSIC_PROTO_PTR.load(Ordering::Acquire); + if ctor == 0 || proto == 0 { + return; + } + let function_ctor = js_get_global_this_builtin_value(b"Function".as_ptr(), 8); + if crate::value::JSValue::from_bits(function_ctor.to_bits()).is_pointer() { + crate::closure::closure_set_static_prototype(ctor as usize, function_ctor.to_bits()); + } + let function_proto = builtin_prototype_value("Function"); + if crate::value::JSValue::from_bits(function_proto.to_bits()).is_pointer() { + super::super::prototype_chain::object_set_static_prototype( + proto as usize, + function_proto.to_bits(), + ); + } +} + /// `g.constructor` for a generator-function closure `g` → `%GeneratorFunction%` /// / `%AsyncGeneratorFunction%`. `None` for non-generator closures. (#3664) pub(crate) fn generator_function_constructor_of(closure_ptr: usize) -> Option { @@ -79,10 +176,16 @@ pub(crate) fn generator_function_constructor_of(closure_ptr: usize) -> Option Option { - let kind = closure_generator_kind(closure_ptr)?; + let scope = crate::gc::RuntimeHandleScope::new(); + let closure_h = scope.root_raw_const_ptr(closure_ptr as *const crate::closure::ClosureHeader); + let kind = closure_h.with_const_ptr::(|closure| { + closure_generator_kind(closure as usize) + })?; // A previously-created (or user-assigned) `prototype` wins — preserves // identity and lets `g.prototype = X` overrides stick. - let existing = crate::closure::closure_get_dynamic_prop(closure_ptr, "prototype"); + let existing = closure_h.with_const_ptr::(|closure| { + crate::closure::closure_get_dynamic_prop(closure as usize, "prototype") + }); if existing.to_bits() != crate::value::TAG_UNDEFINED { return Some(f64::from_bits(existing.to_bits())); } @@ -99,7 +202,6 @@ pub(crate) fn generator_function_prototype_of(closure_ptr: usize) -> Option if obj.is_null() { return None; } - let scope = crate::gc::RuntimeHandleScope::new(); let obj_h = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(obj as i64)); let gen_proto = generator_prototype_ptr(matches!(kind, GeneratorKind::Async)); @@ -111,12 +213,15 @@ pub(crate) fn generator_function_prototype_of(closure_ptr: usize) -> Option ); } let obj_value = obj_h.get_nanbox_f64(); - crate::closure::closure_set_dynamic_prop(closure_ptr, "prototype", obj_value); - super::super::set_builtin_property_attrs( - closure_ptr, - "prototype".to_string(), - super::super::PropertyAttrs::new(true, false, false), - ); + closure_h.with_const_ptr::(|closure| { + let closure = closure as usize; + crate::closure::closure_set_dynamic_prop(closure, "prototype", obj_value); + super::super::set_builtin_property_attrs( + closure, + "prototype".to_string(), + super::super::PropertyAttrs::new(true, false, false), + ); + }); Some(obj_h.get_nanbox_f64()) } @@ -340,6 +445,11 @@ fn install_proto_symbol_self_method( crate::value::js_nanbox_pointer(closure as i64), ); } + crate::symbol::set_symbol_property_attrs( + proto as usize, + sym as usize, + super::super::PropertyAttrs::new(true, false, true), + ); } /// Stamp `NO_THIS_REBIND_FLAG` onto the `next`/`return`/`throw` step-closure @@ -707,7 +817,18 @@ fn build_generator_tower( generator_proto_iterator_thunk as *const u8, ) }; - install_proto_symbol_self_method(gen_proto, symbol_name, display_name, thunk); + if is_async { + // `%AsyncGenerator.prototype%` inherits from a distinct + // `%AsyncIteratorPrototype%`; reflection reaches that parent with two + // `Object.getPrototypeOf` calls from an async generator function's + // `.prototype`. + let async_iterator_proto = js_object_alloc(0, 0); + install_proto_symbol_self_method(async_iterator_proto, symbol_name, display_name, thunk); + let parent_bits = crate::value::js_nanbox_pointer(async_iterator_proto as i64).to_bits(); + super::super::prototype_chain::object_set_static_prototype(gen_proto as usize, parent_bits); + } else { + install_proto_symbol_self_method(gen_proto, symbol_name, display_name, thunk); + } set_intrinsic_to_string_tag(gen_proto, inst_tag); ctor_slot.store(ctor as i64, Ordering::Release); @@ -718,6 +839,9 @@ fn build_generator_tower( /// Build both generator intrinsic towers. Idempotent within the current /// agent; called during its global bootstrap or by the lazy accessors. (#3664) pub(crate) fn ensure_generator_intrinsics() { + if crate::object::ASYNC_FUNCTION_INTRINSIC_PTR.load(Ordering::Acquire) == 0 { + build_async_function_tower(); + } if crate::object::GENERATOR_FUNCTION_INTRINSIC_PTR.load(Ordering::Acquire) == 0 { build_generator_tower( false, diff --git a/crates/perry-runtime/src/object/global_this/install_static.rs b/crates/perry-runtime/src/object/global_this/install_static.rs index 2c2cf65811..0e8479b50c 100644 --- a/crates/perry-runtime/src/object/global_this/install_static.rs +++ b/crates/perry-runtime/src/object/global_this/install_static.rs @@ -570,6 +570,19 @@ pub(crate) fn install_builtin_constructor_statics( "Symbol" => { install_constructor_static(ctor, "for", symbol_for_thunk as *const u8, 1, false); install_constructor_static(ctor, "keyFor", symbol_key_for_thunk as *const u8, 1, false); + for name in ["iterator", "asyncIterator"] { + let symbol = crate::symbol::well_known_symbol(name); + crate::closure::closure_set_dynamic_prop( + ctor as usize, + name, + crate::value::js_nanbox_pointer(symbol as i64), + ); + super::super::set_builtin_property_attrs( + ctor as usize, + name.to_string(), + super::super::PropertyAttrs::new(false, false, false), + ); + } } "String" => { // #4627: reify the variadic `String.fromCharCode` / `fromCodePoint` diff --git a/crates/perry-runtime/src/object/global_this/populate.rs b/crates/perry-runtime/src/object/global_this/populate.rs index 37fdfb2b48..919bae51c5 100644 --- a/crates/perry-runtime/src/object/global_this/populate.rs +++ b/crates/perry-runtime/src/object/global_this/populate.rs @@ -106,6 +106,11 @@ pub(crate) fn populate_global_this_builtins(singleton_at_entry: *mut ObjectHeade let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); let value = crate::value::js_nanbox_pointer(singleton() as i64); js_object_set_field_by_name(singleton(), key, value); + super::super::set_builtin_property_attrs( + singleton() as usize, + "globalThis".to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); } { // #4511: Node exposes the global object as `global` too @@ -195,6 +200,9 @@ pub(crate) fn populate_global_this_builtins(singleton_at_entry: *mut ObjectHeade "WeakSet" => weak_set_constructor_call_thunk as *const u8, "WeakRef" => weak_ref_constructor_call_thunk as *const u8, "Promise" => promise_constructor_call_thunk as *const u8, + "ArrayBuffer" | "SharedArrayBuffer" | "DataView" => { + construct_only_builtin_call_thunk as *const u8 + } _ => global_this_builtin_noop_thunk as *const u8, }; let closure_ptr = crate::closure::js_closure_alloc(func_ptr, 0); @@ -486,6 +494,10 @@ pub(crate) fn populate_global_this_builtins(singleton_at_entry: *mut ObjectHeade super::super::PropertyAttrs::new(true, false, true), ); } + // The hidden `%AsyncFunction%` tower is allocated before the constructor + // loop, but its two parents are the `Function` values installed by that + // loop. Complete those links now that both are available. + wire_async_function_intrinsic_parents(); // Callable global functions: ClosureHeader-backed values with real // dispatch so direct property reads and rebound calls match bare calls. for name in GLOBAL_THIS_BUILTIN_FUNCTIONS.iter().copied() { diff --git a/crates/perry-runtime/src/object/global_this/proto_methods.rs b/crates/perry-runtime/src/object/global_this/proto_methods.rs index 4e09f6adb6..d63cf18478 100644 --- a/crates/perry-runtime/src/object/global_this/proto_methods.rs +++ b/crates/perry-runtime/src/object/global_this/proto_methods.rs @@ -246,7 +246,7 @@ pub(crate) fn populate_builtin_prototype_methods(builtin_name: &str, proto_obj: install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); } "ArrayBuffer" => { - install_noop_proto_methods(proto_obj, &[("slice", 2)]); + install_proto_method(proto_obj, "slice", array_buffer_slice_thunk as *const u8, 2); unsafe { crate::closure::js_register_closure_arity( array_buffer_byte_length_getter_thunk as *const u8, @@ -509,12 +509,16 @@ pub(crate) fn populate_builtin_prototype_methods(builtin_name: &str, proto_obj: // #4100: mirror `Symbol` — brand-checking `toString`(radix)/`valueOf` // re-dispatched to the canonical BigInt logic (`(5n).toString(2)` // → `"101"`). After OBJECT_PROTO_METHODS so the brand `valueOf` wins. - install_proto_method( + let to_string = install_proto_method( proto_obj, "toString", primitive_proto_thunks::bigint_proto_to_string_thunk as *const u8, 1, ); + super::super::native_module::set_builtin_closure_length( + crate::value::js_nanbox_get_pointer(to_string) as usize, + 0, + ); install_proto_method( proto_obj, "valueOf", diff --git a/crates/perry-runtime/src/object/global_this/typed_array.rs b/crates/perry-runtime/src/object/global_this/typed_array.rs index 252bca14b3..d980e62f56 100644 --- a/crates/perry-runtime/src/object/global_this/typed_array.rs +++ b/crates/perry-runtime/src/object/global_this/typed_array.rs @@ -95,6 +95,105 @@ pub(crate) extern "C" fn shared_array_buffer_slice_thunk( } } +pub(crate) extern "C" fn array_buffer_slice_thunk( + _closure: *const crate::closure::ClosureHeader, + start: f64, + end: f64, +) -> f64 { + match array_buffer_receiver_addr() { + Some(addr) => unsafe { + let args = [start, end]; + super::super::buffer_dispatch::dispatch_buffer_method(addr, "slice", args.as_ptr(), 2) + }, + None => super::super::object_ops::throw_object_type_error( + b"Method ArrayBuffer.prototype.slice called on incompatible receiver", + ), + } +} + +pub(crate) unsafe fn validate_array_buffer_species_constructor(addr: usize) { + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(addr as i64)); + let receiver_value = || receiver.get_nanbox_f64(); + let receiver_addr = || crate::value::js_nanbox_get_pointer(receiver_value()) as usize; + + // SpeciesConstructor starts with ordinary Get(O, "constructor"). Buffer + // own data/accessor properties live in side tables; absent own state falls + // through the receiver's recorded custom prototype chain before using the + // intrinsic constructor. + let value = if let Some(accessor) = + super::super::get_accessor_descriptor(receiver_addr(), "constructor") + { + if accessor.get == 0 { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + f64::from_bits( + super::super::invoke_accessor_getter(accessor.get, receiver_value()).bits(), + ) + } + } else if let Some(value) = crate::buffer::buffer_get_own_prop(receiver_addr(), "constructor") { + value + } else { + let inherited = + if super::super::prototype_chain::object_static_prototype(receiver_addr()).is_some() { + let key = crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); + super::super::prototype_chain::resolve_inherited_field(receiver_addr(), key) + .map(|value| f64::from_bits(value.bits())) + } else { + None + }; + inherited.unwrap_or_else(|| { + let name: &[u8] = if crate::buffer::is_shared_array_buffer(receiver_addr()) { + b"SharedArrayBuffer" + } else { + b"ArrayBuffer" + }; + super::js_get_global_this_builtin_value(name.as_ptr(), name.len()) + }) + }; + let value = JSValue::from_bits(value.to_bits()); + // `null` is NaN-boxed in the pointer-shaped value family, but it is not + // an Object and therefore cannot supply @@species. + if !value.is_undefined() + && (value.is_null() + || (!value.is_pointer() + && !super::super::js_value_is_constructor(f64::from_bits(value.bits()))) + // Symbols use the pointer-shaped NaN-box family, but remain + // primitives for SpeciesConstructor's Type(C) check. + || crate::symbol::js_is_symbol(f64::from_bits(value.bits())) != 0) + { + super::super::object_ops::throw_object_type_error( + b"ArrayBuffer species constructor is not an object", + ); + } + if value.is_undefined() { + return; + } + + // Complete SpeciesConstructor validation: Get(C, @@species), accept + // undefined/null as the default constructor, otherwise require an actual + // constructable value. The constructor is rooted because a species getter + // can execute arbitrary user code and collect. + let constructor = scope.root_nanbox_f64(f64::from_bits(value.bits())); + let species = crate::symbol::well_known_symbol("species"); + if species.is_null() { + return; + } + let species_value = crate::symbol::js_object_get_symbol_property( + constructor.get_nanbox_f64(), + f64::from_bits(JSValue::pointer(species as *const u8).bits()), + ); + let species_value = JSValue::from_bits(species_value.to_bits()); + if species_value.is_undefined() || species_value.is_null() { + return; + } + if !super::super::js_value_is_constructor(f64::from_bits(species_value.bits())) { + super::super::object_ops::throw_object_type_error( + b"ArrayBuffer species is not a constructor", + ); + } +} + pub(crate) extern "C" fn array_buffer_is_view_thunk( _closure: *const crate::closure::ClosureHeader, value: f64, @@ -111,6 +210,7 @@ pub(crate) extern "C" fn array_buffer_is_view_thunk( && !crate::buffer::is_any_array_buffer(addr) && (crate::buffer::is_uint8array_buffer(addr) || crate::buffer::is_data_view(addr))) || jsvalue_extends_data_view(value) + || jsvalue_extends_typed_array(value) || crate::typedarray::lookup_typed_array_kind(addr).is_some(); f64::from_bits(crate::value::JSValue::bool(is_view).bits()) } @@ -121,12 +221,13 @@ fn jsvalue_extends_data_view(value: f64) -> bool { return false; } let ptr = v.as_pointer::(); - if ptr.is_null() || !crate::object::is_valid_obj_ptr(ptr) { + let Some(gc_header) = + (unsafe { crate::value::addr_class::try_read_tracked_gc_header(ptr as usize) }) + else { return false; - } + }; unsafe { - let gc_header = ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type != crate::gc::GC_TYPE_OBJECT { + if (*gc_header.as_ptr()).obj_type != crate::gc::GC_TYPE_OBJECT { return false; } let obj = ptr as *const ObjectHeader; @@ -135,6 +236,26 @@ fn jsvalue_extends_data_view(value: f64) -> bool { } } +fn jsvalue_extends_typed_array(value: f64) -> bool { + let v = JSValue::from_bits(value.to_bits()); + if !v.is_pointer() { + return false; + } + let ptr = v.as_pointer::(); + let Some(gc_header) = + (unsafe { crate::value::addr_class::try_read_tracked_gc_header(ptr as usize) }) + else { + return false; + }; + unsafe { + if (*gc_header.as_ptr()).obj_type != crate::gc::GC_TYPE_OBJECT { + return false; + } + let class_id = (*(ptr as *const ObjectHeader)).class_id; + class_id != 0 && crate::object::extends_builtin_typed_array(class_id) + } +} + /// Resolve the `IMPLICIT_THIS` receiver to a `(typed-array ptr, kind)` if it /// is a typed array, else `None`. Backs the `%TypedArray%.prototype` accessor /// getters installed for reflection (#2060) — these fire when user code does @@ -396,6 +517,11 @@ fn install_typed_array_iterator_symbol(proto_obj: *mut ObjectHeader) { iter_value, f64::from_bits(values.bits()), ); + crate::symbol::set_symbol_property_attrs( + proto_obj as usize, + iter as usize, + crate::object::PropertyAttrs::new(true, false, true), + ); } } } @@ -462,6 +588,17 @@ pub(crate) fn ensure_typed_array_intrinsic( "prototype".to_string(), super::super::PropertyAttrs::new(false, false, false), ); + let constructor_key = crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); + js_object_set_field_by_name( + proto, + constructor_key, + crate::value::js_nanbox_pointer(ctor as i64), + ); + super::super::set_builtin_property_attrs( + proto as usize, + "constructor".to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); // #2060: the four reflectable `length`/`byteLength`/`byteOffset`/`buffer` // accessor descriptors are own properties of `%TypedArray%.prototype` per // spec, NOT of the per-kind proto. Pre-#2145 they were installed on each diff --git a/crates/perry-runtime/src/object/iterator_prototypes.rs b/crates/perry-runtime/src/object/iterator_prototypes.rs index e52ca22366..dd1e82195e 100644 --- a/crates/perry-runtime/src/object/iterator_prototypes.rs +++ b/crates/perry-runtime/src/object/iterator_prototypes.rs @@ -235,6 +235,11 @@ fn install_symbol_iterator(shared: *mut ObjectHeader) { crate::value::js_nanbox_pointer(closure as i64), ); } + crate::symbol::set_symbol_property_attrs( + shared as usize, + sym as usize, + PropertyAttrs::new(true, false, true), + ); } /// Allocate one family prototype with an own `next` method (spec descriptor), @@ -308,3 +313,57 @@ pub(crate) fn attach_iterator_prototype(obj_ptr: *mut ObjectHeader, class_id: u3 } chain_to(obj_ptr, proto_ptr as *mut ObjectHeader); } + +/// Invoke a user replacement of a built-in iterator prototype's `next` +/// method. Returns `None` while the canonical native thunk is installed. +pub(crate) unsafe fn call_overridden_iterator_next( + iter_obj: *mut ObjectHeader, + class_id: u32, +) -> Option { + let scope = crate::gc::RuntimeHandleScope::new(); + let iter = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(iter_obj as i64)); + let previous = scope.root_nanbox_f64(super::js_implicit_this_get()); + ensure_iterator_prototypes(); + let (slot, canonical): (&crate::object::RealmAtomicI64, *const u8) = match class_id { + crate::array::ARRAY_ITERATOR_CLASS_ID => ( + &ARRAY_ITERATOR_PROTOTYPE_PTR, + array_iterator_next_thunk as *const u8, + ), + crate::collection_iter_object::MAP_ITERATOR_CLASS_ID => ( + &MAP_ITERATOR_PROTOTYPE_PTR, + map_iterator_next_thunk as *const u8, + ), + crate::collection_iter_object::SET_ITERATOR_CLASS_ID => ( + &SET_ITERATOR_PROTOTYPE_PTR, + set_iterator_next_thunk as *const u8, + ), + crate::string::STRING_ITERATOR_CLASS_ID => ( + &STRING_ITERATOR_PROTOTYPE_PTR, + string_iterator_next_thunk as *const u8, + ), + _ => return None, + }; + // Building the tower above may collect. Reload its realm-owned root only + // after the build rather than retaining a pre-build raw address. + let proto = scope.root_raw_const_ptr(slot.load(Ordering::Acquire) as *const ObjectHeader); + if proto.with_const_ptr::(|proto| proto.is_null()) { + return None; + } + let key = scope.root_raw_const_ptr(crate::string::js_string_from_bytes(b"next".as_ptr(), 4)); + let method = proto.with_const_ptr::(|proto| { + key.with_const_ptr::(|key| { + f64::from_bits(super::js_object_get_field_by_name(proto, key).bits()) + }) + }); + let method_ptr = + crate::value::js_nanbox_get_pointer(method) as *const crate::closure::ClosureHeader; + if !method_ptr.is_null() && crate::closure::get_valid_func_ptr(method_ptr) == canonical { + return None; + } + + let method = scope.root_nanbox_f64(method); + super::js_implicit_this_set(iter.get_nanbox_f64()); + let result = crate::closure::js_native_call_value(method.get_nanbox_f64(), std::ptr::null(), 0); + super::js_implicit_this_set(previous.get_nanbox_f64()); + Some(result) +} diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index e28f2a0b0d..28b2d034d8 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -185,7 +185,7 @@ pub(crate) use class_gc_roots::{ }; pub use class_registry::*; pub(crate) use collection_proto_thunks::{is_builtin_map_set_value, is_builtin_set_add_value}; -pub(crate) use data_view_registry::extends_builtin_data_view; +pub(crate) use data_view_registry::{extends_builtin_data_view, extends_builtin_typed_array}; pub use delete_rest::*; pub use descriptors::*; pub use exotic_expando::scan_exotic_expando_roots_mut; @@ -195,7 +195,9 @@ pub use global_this::*; pub(crate) use global_this_tables::*; pub use groupby::*; pub use instanceof::*; -pub(crate) use iterator_prototypes::{attach_iterator_prototype, iterator_prototype_for_class_id}; +pub(crate) use iterator_prototypes::{ + attach_iterator_prototype, call_overridden_iterator_next, iterator_prototype_for_class_id, +}; pub use namespace_create::*; pub use native_call_method::*; pub use native_module::*; @@ -341,6 +343,8 @@ crate::perry_thread_local! { static OS_CONSTANTS_DLOPEN_CACHE_SLOT: AtomicU64 = const { AtomicU64::new(0) }; static TYPED_ARRAY_INTRINSIC_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; static TYPED_ARRAY_INTRINSIC_PROTO_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; + static ASYNC_FUNCTION_INTRINSIC_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; + static ASYNC_FUNCTION_INTRINSIC_PROTO_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; static GENERATOR_FUNCTION_INTRINSIC_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; static GENERATOR_INTRINSIC_PROTO_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; static GENERATOR_PROTOTYPE_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; @@ -367,6 +371,10 @@ pub(crate) static TYPED_ARRAY_INTRINSIC_PTR: RealmAtomicI64 = RealmAtomicI64::new(&TYPED_ARRAY_INTRINSIC_PTR_SLOT); pub(crate) static TYPED_ARRAY_INTRINSIC_PROTO_PTR: RealmAtomicI64 = RealmAtomicI64::new(&TYPED_ARRAY_INTRINSIC_PROTO_PTR_SLOT); +pub(crate) static ASYNC_FUNCTION_INTRINSIC_PTR: RealmAtomicI64 = + RealmAtomicI64::new(&ASYNC_FUNCTION_INTRINSIC_PTR_SLOT); +pub(crate) static ASYNC_FUNCTION_INTRINSIC_PROTO_PTR: RealmAtomicI64 = + RealmAtomicI64::new(&ASYNC_FUNCTION_INTRINSIC_PROTO_PTR_SLOT); pub(crate) static GENERATOR_FUNCTION_INTRINSIC_PTR: RealmAtomicI64 = RealmAtomicI64::new(&GENERATOR_FUNCTION_INTRINSIC_PTR_SLOT); pub(crate) static GENERATOR_INTRINSIC_PROTO_PTR: RealmAtomicI64 = @@ -1186,6 +1194,8 @@ pub fn scan_object_cache_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<' for slot in [ &TYPED_ARRAY_INTRINSIC_PTR, &TYPED_ARRAY_INTRINSIC_PROTO_PTR, + &ASYNC_FUNCTION_INTRINSIC_PTR, + &ASYNC_FUNCTION_INTRINSIC_PROTO_PTR, &GENERATOR_FUNCTION_INTRINSIC_PTR, &GENERATOR_INTRINSIC_PROTO_PTR, &GENERATOR_PROTOTYPE_PTR, @@ -1200,6 +1210,7 @@ pub fn scan_object_cache_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<' }); } async_generator_queue::scan_async_generator_queue_roots_mut(visitor); + collection_proto_thunks::scan_builtin_collection_method_roots_mut(visitor); // Shared `%IteratorPrototype%`-style singletons for Array/Map/Set/String // iterator objects. Each iterator instance's `[[Prototype]]` points here, so // these must stay live for the lifetime of any iterator. @@ -1565,6 +1576,7 @@ pub(crate) fn test_realm_owned_root_snapshot() -> Vec<(&'static str, usize, u64) slot.load(Ordering::Acquire) as u64, )); } + global_this::append_async_function_root_snapshot(&mut roots); roots } diff --git a/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs index d9b39c6a5a..0a082e013d 100644 --- a/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs @@ -146,16 +146,10 @@ pub(super) unsafe fn dispatch_primitive( let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); } - let raw = crate::value::js_nanbox_get_pointer(object) as *const u8; - if !raw.is_null() && crate::object::is_valid_obj_ptr(raw) { - unsafe { - let gc = raw.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc).obj_type == crate::gc::GC_TYPE_ERROR { - let s = crate::error::js_error_to_string(raw as *mut crate::error::ErrorHeader); - return Some(f64::from_bits(JSValue::string_ptr(s).bits())); - } - } - } + // Error instances continue through ordinary method lookup. Their + // prototype's `toString` is replaceable, so hard-wiring the native + // formatter here would ignore `Error.prototype.toString = + // Object.prototype.toString`. } // Primitive-wrapper prototypes (`Number.prototype`, `Boolean.prototype`, @@ -213,7 +207,7 @@ pub(super) unsafe fn dispatch_primitive( (own.to_bits() & crate::value::POINTER_MASK) as usize, ) { - return None; + return super::call_primitive_closure_value(object, own_jsv, args_ptr, args_len); } } match method_name { diff --git a/crates/perry-runtime/src/object/object_ops/define_property.rs b/crates/perry-runtime/src/object/object_ops/define_property.rs index e247e53f98..8c5375d8c0 100644 --- a/crates/perry-runtime/src/object/object_ops/define_property.rs +++ b/crates/perry-runtime/src/object/object_ops/define_property.rs @@ -474,6 +474,268 @@ pub extern "C" fn js_object_define_property( let descriptor_value = desc_handle.get_nanbox_f64(); let key_value = key_handle.get_nanbox_f64(); + // ArrayBuffer/SharedArrayBuffer/DataView use `BufferHeader` storage, + // but are ordinary extensible objects for named properties. Keep their + // data descriptors in the existing buffer expando table instead of + // falling through and interpreting the header as an `ObjectHeader`. + let buffer_addr = crate::value::JSValue::from_bits(obj_value.to_bits()) + .is_pointer() + .then(|| crate::value::js_nanbox_get_pointer(obj_value) as usize) + .filter(|addr| crate::buffer::is_registered_buffer(*addr)); + if buffer_addr.is_some() { + let current_obj = || f64::from_bits(obj_value_handle.get_heap_word_u64()); + let current_addr = || crate::value::js_nanbox_get_pointer(current_obj()) as usize; + let current_desc = || desc_handle.get_nanbox_f64(); + let current_key = || key_handle.get_nanbox_f64(); + + // Symbol keys stay Symbols; string coercion would create an + // unrelated `"Symbol(...)"` expando that symbol lookup cannot see. + if crate::symbol::js_is_symbol(current_key()) != 0 { + let current_owner = || crate::symbol::obj_key_from_f64(current_obj()); + let current_sym = || crate::symbol::sym_key_from_f64(current_key()); + let existing_accessor_bits = + crate::symbol::symbol_accessor_descriptor_bits(current_owner(), current_sym()); + let existing_data_bits = existing_accessor_bits + .is_none() + .then(|| { + crate::symbol::symbol_property_root_bits(current_owner(), current_sym()) + }) + .flatten(); + let existing_get = + scope.root_nanbox_u64(existing_accessor_bits.map(|(get, _)| get).unwrap_or(0)); + let existing_set = + scope.root_nanbox_u64(existing_accessor_bits.map(|(_, set)| set).unwrap_or(0)); + let existing_data = scope + .root_nanbox_u64(existing_data_bits.unwrap_or(crate::value::TAG_UNDEFINED)); + let existed = existing_accessor_bits.is_some() || existing_data_bits.is_some(); + // A symbol installed by ordinary assignment has no explicit + // attrs side-table entry and therefore has the ordinary + // writable/enumerable/configurable defaults. + let existing_attrs = existed.then(|| { + crate::symbol::get_symbol_property_attrs(current_owner(), current_sym()) + .unwrap_or(PropertyAttrs::new(true, true, true)) + }); + if let Some(attrs) = existing_attrs { + if !attrs.configurable() { + validate_nonconfigurable_redefine( + "symbol", + attrs, + existing_accessor_bits.map(|_| super::super::AccessorDescriptor { + get: existing_get.get_nanbox_u64(), + set: existing_set.get_nanbox_u64(), + }), + existing_data.get_nanbox_f64(), + current_desc(), + desc_view.as_ref(), + ); + } + } + + let has_get = desc_has_field(current_desc(), b"get"); + let has_set = desc_has_field(current_desc(), b"set"); + let has_value = desc_has_field(current_desc(), b"value"); + let has_writable = desc_has_field(current_desc(), b"writable"); + if has_get || has_set { + let get = scope.root_nanbox_u64(if has_get { + let field = desc_read_field(current_desc(), b"get"); + (!field.is_undefined()) + .then(|| { + crate::closure::clone_closure_rebind_this( + field.bits(), + current_obj(), + ) + }) + .unwrap_or(0) + } else { + existing_get.get_nanbox_u64() + }); + let set = if has_set { + let field = desc_read_field(current_desc(), b"set"); + (!field.is_undefined()) + .then(|| { + crate::closure::clone_closure_rebind_this( + field.bits(), + current_obj(), + ) + }) + .unwrap_or(0) + } else { + existing_set.get_nanbox_u64() + }; + crate::symbol::set_symbol_accessor_property( + current_obj(), + current_key(), + get.get_nanbox_u64(), + set, + ); + } else if has_value || has_writable || !existed { + let value = if has_value { + f64::from_bits(desc_read_field(current_desc(), b"value").bits()) + } else if existing_accessor_bits.is_some() || existing_data_bits.is_none() { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + existing_data.get_nanbox_f64() + }; + crate::symbol::define_symbol_data_property(current_obj(), current_key(), value); + } + let read_flag = |name: &[u8]| -> Option { + desc_has_field(current_desc(), name).then(|| { + crate::value::js_is_truthy(f64::from_bits( + desc_read_field(current_desc(), name).bits(), + )) != 0 + }) + }; + crate::symbol::set_symbol_property_attrs( + current_owner(), + current_sym(), + PropertyAttrs::new( + if has_get || has_set { + false + } else { + read_flag(b"writable").unwrap_or_else(|| { + existing_attrs + .map(|attrs| attrs.writable()) + .unwrap_or(false) + }) + }, + read_flag(b"enumerable").unwrap_or_else(|| { + existing_attrs + .map(|attrs| attrs.enumerable()) + .unwrap_or(false) + }), + read_flag(b"configurable").unwrap_or_else(|| { + existing_attrs + .map(|attrs| attrs.configurable()) + .unwrap_or(false) + }), + ), + ); + return current_obj(); + } + + if let Some(name) = super::super::metadata_key_to_string(current_key()) { + // Key coercion and descriptor getters may collect. Every side + // table access below therefore re-derives the buffer owner from + // the rooted receiver instead of retaining `buffer_addr`. + let addr = current_addr(); + let existing_accessor = super::super::get_accessor_descriptor(addr, &name); + let existing_data = crate::buffer::buffer_get_own_prop(addr, &name); + let existing_get = scope + .root_nanbox_u64(existing_accessor.map(|accessor| accessor.get).unwrap_or(0)); + let existing_set = scope + .root_nanbox_u64(existing_accessor.map(|accessor| accessor.set).unwrap_or(0)); + let existing_data_value = scope.root_nanbox_f64( + existing_data.unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)), + ); + let existing_attrs = + (existing_accessor.is_some() || existing_data.is_some()).then(|| { + super::super::get_property_attrs(addr, &name) + .unwrap_or(PropertyAttrs::new(true, true, true)) + }); + if let Some(attrs) = existing_attrs { + if !attrs.configurable() { + validate_nonconfigurable_redefine( + &name, + attrs, + existing_accessor, + existing_data_value.get_nanbox_f64(), + current_desc(), + desc_view.as_ref(), + ); + } + } + + let has_get = desc_has_field(current_desc(), b"get"); + let has_set = desc_has_field(current_desc(), b"set"); + let has_value = desc_has_field(current_desc(), b"value"); + let has_writable = desc_has_field(current_desc(), b"writable"); + if has_get || has_set { + let get_field = + scope.root_nanbox_u64(desc_read_field(current_desc(), b"get").bits()); + let set_field = + scope.root_nanbox_u64(desc_read_field(current_desc(), b"set").bits()); + let get_bits = scope.root_nanbox_u64( + if has_get && get_field.get_nanbox_u64() != crate::value::TAG_UNDEFINED { + crate::closure::clone_closure_rebind_this( + get_field.get_nanbox_u64(), + current_obj(), + ) + } else if !has_get { + existing_get.get_nanbox_u64() + } else { + 0 + }, + ); + let set_bits = + if has_set && set_field.get_nanbox_u64() != crate::value::TAG_UNDEFINED { + crate::closure::clone_closure_rebind_this( + set_field.get_nanbox_u64(), + current_obj(), + ) + } else if !has_set { + existing_set.get_nanbox_u64() + } else { + 0 + }; + let addr = current_addr(); + super::super::set_accessor_descriptor( + addr, + name.clone(), + super::super::AccessorDescriptor { + get: get_bits.get_nanbox_u64(), + set: set_bits, + }, + ); + // Keep an order/enumeration placeholder; accessor-aware + // reads ignore its undefined payload. + crate::buffer::buffer_define_own_data_prop( + addr, + &name, + f64::from_bits(crate::value::TAG_UNDEFINED), + ); + } else if has_value || has_writable || existing_attrs.is_none() { + // Data descriptor (or a brand-new generic descriptor). + let addr = current_addr(); + super::super::clear_accessor_descriptor(addr, &name); + let value = if has_value { + f64::from_bits(desc_read_field(current_desc(), b"value").bits()) + } else if existing_accessor.is_some() || existing_data.is_none() { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + existing_data_value.get_nanbox_f64() + }; + crate::buffer::buffer_define_own_data_prop(current_addr(), &name, value); + } + + let read_flag = |field: &[u8]| -> Option { + desc_has_field(current_desc(), field).then(|| { + crate::value::js_is_truthy(f64::from_bits( + desc_read_field(current_desc(), field).bits(), + )) != 0 + }) + }; + let attrs = PropertyAttrs::new( + read_flag(b"writable").unwrap_or_else(|| { + existing_attrs + .map(|attrs| attrs.writable()) + .unwrap_or(false) + }), + read_flag(b"enumerable").unwrap_or_else(|| { + existing_attrs + .map(|attrs| attrs.enumerable()) + .unwrap_or(false) + }), + read_flag(b"configurable").unwrap_or_else(|| { + existing_attrs + .map(|attrs| attrs.configurable()) + .unwrap_or(false) + }), + ); + super::super::set_property_attrs(current_addr(), name, attrs); + } + return current_obj(); + } + // Date / RegExp / Error instances are exotic cells, not // `ObjectHeader`s — the ordinary define path below would bit-cast // them and corrupt memory. Route through the expando-aware diff --git a/crates/perry-runtime/src/object/object_ops/prototype.rs b/crates/perry-runtime/src/object/object_ops/prototype.rs index 7a0f09f89c..3ed6d5fdee 100644 --- a/crates/perry-runtime/src/object/object_ops/prototype.rs +++ b/crates/perry-runtime/src/object/object_ops/prototype.rs @@ -87,6 +87,20 @@ pub extern "C" fn js_object_create(proto_value: f64) -> f64 { return f64::from_bits((obj as u64) | POINTER_TAG); } + // Integer-indexed exotic objects are valid prototypes even though their + // TypedArrayHeader cannot be modeled as an ObjectHeader-backed synthetic + // class prototype. Preserve the exact object identity in the ordinary + // per-instance prototype side table so its [[Set]] intercepts canonical + // numeric keys on descendants. + if crate::typedarray_props::typed_array_addr_from_value(proto_value).is_some() { + let obj = js_object_alloc(0, 0); + crate::object::prototype_chain::object_set_static_prototype( + obj as usize, + proto_value.to_bits(), + ); + return f64::from_bits((obj as u64) | POINTER_TAG); + } + let mut class_id: u32 = 0; let proto_bits = proto_value.to_bits(); if (proto_bits & 0xFFFF_0000_0000_0000) == POINTER_TAG { diff --git a/crates/perry-runtime/src/object/polymorphic_index.rs b/crates/perry-runtime/src/object/polymorphic_index.rs index 2f483c6f76..ff14715c1d 100644 --- a/crates/perry-runtime/src/object/polymorphic_index.rs +++ b/crates/perry-runtime/src/object/polymorphic_index.rs @@ -177,6 +177,12 @@ pub extern "C" fn js_object_get_index_polymorphic(obj_handle: i64, idx: f64) -> if raw < 0x1000 { return f64::from_bits(crate::value::TAG_UNDEFINED); } + // Symbols share GC_TYPE_STRING storage for tracing, but they are primitive + // values, not String exotic objects. A numeric property access boxes the + // Symbol transiently and therefore observes no indexed property. + if crate::symbol::is_registered_symbol(raw as usize) { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } // #5525 fast path: cached typed-array kind lookup + inline load, ahead of // the thread-local `typed_array_get_numeric_index` registry dispatch. // `typed_array_fast_index_get` returns `Some(value)` for an in-bounds read diff --git a/crates/perry-runtime/src/object/primitive_proto_thunks.rs b/crates/perry-runtime/src/object/primitive_proto_thunks.rs index a39746e24b..280b0c552c 100644 --- a/crates/perry-runtime/src/object/primitive_proto_thunks.rs +++ b/crates/perry-runtime/src/object/primitive_proto_thunks.rs @@ -87,12 +87,19 @@ pub(super) fn install_primitive_proto_methods( ); } "BigInt" => { - ipm( + let to_string = ipm( proto_obj, "toString", bigint_proto_to_string_thunk as *const u8, 1, ); + // The optional radix does not contribute to the observable + // function length, but the native thunk still needs one ABI slot + // so a missing argument arrives as undefined rather than garbage. + super::native_module::set_builtin_closure_length( + crate::value::js_nanbox_get_pointer(to_string) as usize, + 0, + ); ipm( proto_obj, "valueOf", @@ -133,11 +140,14 @@ pub(crate) fn primitive_proto_method_value(builtin_name: &str, method_name: &str ("BigInt", "valueOf") => (bigint_proto_value_of_thunk as *const u8, 0), _ => return None, }; - Some(primitive_proto_method_closure_value( - method_name, - func_ptr, - arity, - )) + let value = primitive_proto_method_closure_value(method_name, func_ptr, arity); + if builtin_name == "BigInt" && method_name == "toString" { + super::native_module::set_builtin_closure_length( + crate::value::js_nanbox_get_pointer(value) as usize, + 0, + ); + } + Some(value) } fn primitive_proto_method_closure_value(method_name: &str, func_ptr: *const u8, arity: u32) -> f64 { diff --git a/crates/perry-runtime/src/object/prototype_chain.rs b/crates/perry-runtime/src/object/prototype_chain.rs index 63b0b37244..253d9e486f 100644 --- a/crates/perry-runtime/src/object/prototype_chain.rs +++ b/crates/perry-runtime/src/object/prototype_chain.rs @@ -139,6 +139,10 @@ fn get_object_prototypes() -> &'static Mutex> { /// always on exactly one of the two storages. pub(crate) unsafe fn meta_capable_object(obj_ptr: usize) -> Option<*mut crate::ObjectHeader> { if !crate::value::addr_class::is_above_handle_band(obj_ptr) + // ArrayBuffer / SharedArrayBuffer / DataView use BufferHeader storage. + // Some of those headers pass the legacy ObjectHeader validity probe, + // but they do not have an ObjectMeta slot at the ObjectHeader offset. + || crate::buffer::is_registered_buffer(obj_ptr) || !crate::object::is_valid_obj_ptr(obj_ptr as *const u8) { return None; diff --git a/crates/perry-runtime/src/promise/async_step.rs b/crates/perry-runtime/src/promise/async_step.rs index 5175cd8295..510cf31bee 100644 --- a/crates/perry-runtime/src/promise/async_step.rs +++ b/crates/perry-runtime/src/promise/async_step.rs @@ -184,6 +184,9 @@ pub extern "C" fn js_promise_resolved(value: f64) -> *mut Promise { } return promise; } + let scope = crate::gc::RuntimeHandleScope::new(); + let value_h = scope.root_nanbox_f64(value); + // Issue #2823: `Promise.resolve(p)` MUST return `p` itself when `p` is // already a native Promise (constructor === Promise). The spec defines // Promise.resolve to short-circuit and return the argument unchanged in @@ -192,10 +195,36 @@ pub extern "C" fn js_promise_resolved(value: f64) -> *mut Promise { // `Promise` instances, so a GC_TYPE_PROMISE value always satisfies the // "constructor is Promise" check. Return the existing pointer directly // instead of allocating a fresh wrapper and chaining to it. - if js_value_is_promise(value) != 0 { - let inner = crate::value::js_nanbox_get_pointer(value) as *mut Promise; + if js_value_is_promise(value_h.get_nanbox_f64()) != 0 { + let inner = crate::value::js_nanbox_get_pointer(value_h.get_nanbox_f64()) as *mut Promise; if !inner.is_null() { - return inner; + // PromiseResolve(%Promise%, x) performs Get(x, "constructor") + // before its identity short-cut. An own accessor can throw, and + // an own non-intrinsic constructor prevents returning x itself. + let addr = inner as usize; + let same_intrinsic_constructor = if super::promise_has_own_constructor(addr) { + let ctor = unsafe { + crate::object::exotic_expando::exotic_get_own_property( + addr, + crate::object::exotic_expando::ExoticKind::Promise, + "constructor", + value_h.get_nanbox_f64(), + ) + } + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); + let ctor_h = scope.root_nanbox_f64(ctor); + let intrinsic = crate::object::js_get_global_this_builtin_value( + b"Promise".as_ptr(), + b"Promise".len(), + ); + ctor_h.get_nanbox_f64().to_bits() == intrinsic.to_bits() + } else { + true + }; + if same_intrinsic_constructor { + return crate::value::js_nanbox_get_pointer(value_h.get_nanbox_f64()) + as *mut Promise; + } } } let promise = js_promise_new(); @@ -213,10 +242,27 @@ pub extern "C" fn js_promise_resolved(value: f64) -> *mut Promise { // steady state is untouched; only real thenables (drizzle's `QueryPromise`, // object literals with `then`) defer by one microtask — which the await // loop drains, leaving the resolved value identical. - super::assimilate::promise_resolve_assimilating(promise, value); + super::assimilate::promise_resolve_assimilating(promise, value_h.get_nanbox_f64()); promise } +/// Run the spec PromiseResolve path behind an exception boundary. Async +/// iterator/generator algorithms turn an abrupt constructor getter into a +/// rejected result promise instead of throwing synchronously to their caller. +pub fn js_promise_resolved_catching(value: f64) -> Result<*mut Promise, f64> { + let trap_buf = crate::exception::js_try_push(); + let jumped = unsafe { crate::ffi::setjmp::setjmp(trap_buf as *mut std::os::raw::c_int) }; + let result = if jumped == 0 { + Ok(js_promise_resolved(value)) + } else { + let reason = crate::exception::js_get_exception(); + crate::exception::js_clear_exception(); + Err(reason) + }; + crate::exception::js_try_end(); + result +} + /// Fused fast path for `Promise.resolve(value).then(cb_f, cb_e)` — /// the steady-state shape of the async-to-generator transform's /// per-`await` lowering. The naive sequence is: diff --git a/crates/perry-runtime/src/promise/mod.rs b/crates/perry-runtime/src/promise/mod.rs index 8d10844efe..6782f06af6 100644 --- a/crates/perry-runtime/src/promise/mod.rs +++ b/crates/perry-runtime/src/promise/mod.rs @@ -39,8 +39,8 @@ pub(crate) mod then_probe; pub use async_step::{ js_array_from_async, js_async_first_call, js_async_step_chain, js_async_step_done, - js_get_current_step_closure, js_promise_resolved, js_promise_resolved_then, - scan_async_step_thunk_cache, scan_async_step_thunk_cache_mut, + js_get_current_step_closure, js_promise_resolved, js_promise_resolved_catching, + js_promise_resolved_then, scan_async_step_thunk_cache, scan_async_step_thunk_cache_mut, }; pub use checked_dispatch::{ js_promise_catch_checked, js_promise_closure_arg, js_promise_finally_checked, diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index 465bdd9aec..4f580fa8d3 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -756,6 +756,20 @@ fn reflect_value_is_object(value: f64) -> bool { return true; } let bits = value.to_bits(); + // BufferHeader-backed values are objects even when they are not GC heap + // allocations. In particular, SharedArrayBuffer uses a process-global, + // never-moved backing from `alloc_zeroed`; depending on the surrounding + // allocation history its address can fail the generic heap-address test + // below. Consult the authoritative registries before that heuristic so + // receiver-aware Set reaches CreateDataProperty and records named + // expandos such as an own `constructor`. + let raw = extract_pointer(bits) as usize; + if raw != 0 + && (crate::buffer::is_registered_buffer(raw) + || crate::typedarray::lookup_typed_array_kind(raw).is_some()) + { + return true; + } let top16 = bits >> 48; if top16 == (POINTER_TAG >> 48) { let lower48 = bits & POINTER_MASK; @@ -1554,6 +1568,63 @@ fn call_setter_with_receiver(setter_bits: u64, receiver: f64, value: f64) -> boo true } +/// Ordinary named-property Set for the BufferHeader-backed objects that are +/// not integer-indexed exotics (ArrayBuffer, SharedArrayBuffer, DataView). +/// +/// These values have no ObjectHeader/GcHeader. The generic Set tail eventually +/// asks whether the receiver is a GC heap object before CreateDataProperty; +/// a process-global SharedArrayBuffer can therefore be rejected even though +/// the buffer registries authoritatively classify it as an Object. Walk the +/// descriptors normally, but materialize the final own data property in the +/// buffer expando table instead of the ObjectHeader store. +fn set_nonindexed_buffer_named_self(target: f64, key: f64, value: f64) -> Option { + if unsafe { crate::symbol::js_is_symbol(key) } != 0 { + return None; + } + let raw = raw_ptr_from_value(target)?; + if !crate::buffer::is_non_indexed_buffer_view(raw) { + return None; + } + let name = key_to_rust_string(key)?; + + // Existing own accessor/data descriptors take the same precedence as in + // OrdinarySetWithOwnDescriptor. Buffer expandos live in side tables, so + // inspect those tables without treating the BufferHeader as ObjectHeader. + if let Some(accessor) = crate::object::get_accessor_descriptor(raw, &name) { + return Some(call_setter_with_receiver(accessor.set, target, value)); + } + if crate::buffer::buffer_has_own_prop(raw, &name) { + if crate::object::get_property_attrs(raw, &name).is_some_and(|attrs| !attrs.writable()) { + return Some(false); + } + crate::buffer::buffer_set_own_prop(raw, &name, value); + return Some(true); + } + + // A descriptor on the prototype can reject the write or invoke a setter. + // A writable inherited data descriptor (including the standard + // `.constructor`) creates an own property on the original receiver. + let mut current = prototype_of_for_set(target); + for _ in 0..64 { + let Some(proto) = current else { + break; + }; + if let Some(desc) = own_set_descriptor(proto, key) { + match desc { + OwnSetDescriptor::Data { writable: false } => return Some(false), + OwnSetDescriptor::Data { writable: true } => break, + OwnSetDescriptor::Accessor { setter_bits } => { + return Some(call_setter_with_receiver(setter_bits, target, value)); + } + } + } + current = prototype_of_for_set(proto); + } + + crate::buffer::buffer_set_own_prop(raw, &name, value); + Some(true) +} + /// #5129: build a fresh data property descriptor /// `{ value, writable: true, enumerable: true, configurable: true }` /// (the CreateDataProperty shape) for defining a property on a Proxy receiver diff --git a/crates/perry-runtime/src/proxy/put_value.rs b/crates/perry-runtime/src/proxy/put_value.rs index 932d227f37..a0ce216b1d 100644 --- a/crates/perry-runtime/src/proxy/put_value.rs +++ b/crates/perry-runtime/src/proxy/put_value.rs @@ -185,6 +185,16 @@ pub extern "C" fn js_put_value_set( if set_integer_indexed_exotic(target, property_key, value) { return value; } + if target.to_bits() == receiver.to_bits() { + if let Some(stored) = set_nonindexed_buffer_named_self(target, property_key, value) { + if !stored && strict != 0 { + let key_name = + key_to_rust_string(property_key).unwrap_or_else(|| "property".to_string()); + crate::error::throw_immutable_write(0, &key_name); + } + return value; + } + } // Integer-Indexed exotic objects: a key that is *not* a CanonicalNumeric // index does OrdinarySet, creating/looking-up a normal own property on // the typed array (ECMA-262 §10.4.5.5). The generic diff --git a/crates/perry-runtime/src/set.rs b/crates/perry-runtime/src/set.rs index 868e81b3a6..8d381f900c 100644 --- a/crates/perry-runtime/src/set.rs +++ b/crates/perry-runtime/src/set.rs @@ -1495,18 +1495,27 @@ pub extern "C" fn js_set_from_array(arr: *const crate::array::ArrayHeader) -> *m pub extern "C" fn js_set_from_iterable(value: f64) -> *mut SetHeader { use crate::collection_iter::{constructor_iter, ConstructorIter}; + if crate::collection_iter::is_null_or_undefined(value) { + return js_set_alloc(4); + } + let scope = crate::gc::RuntimeHandleScope::new(); let value_handle = scope.root_nanbox_f64(value); + let set = js_set_alloc(4); + let set_handle = scope.root_raw_mut_ptr(set); let adder = crate::collection_iter::require_callable( - crate::collection_iter::builtin_prototype_method("Set", "add"), + set_handle.with_mut_ptr::(|set| { + crate::collection_iter::builtin_prototype_adder( + "Set", + "add", + crate::value::js_nanbox_pointer(set as i64), + ) + }), "Set.prototype.add", ); let adder = crate::collection_iter::normalize_callable_value(adder); let adder_handle = scope.root_nanbox_f64(adder); - let set = js_set_alloc(4); - let set_handle = scope.root_raw_mut_ptr(set); - let add_value = |element: f64, iter_to_close: Option| { let args = [element]; let adder = adder_handle.get_nanbox_f64(); diff --git a/crates/perry-runtime/src/string/iter_object.rs b/crates/perry-runtime/src/string/iter_object.rs index 197321b6c2..8916059879 100644 --- a/crates/perry-runtime/src/string/iter_object.rs +++ b/crates/perry-runtime/src/string/iter_object.rs @@ -36,16 +36,25 @@ use crate::StringHeader; pub const STRING_ITERATOR_CLASS_ID: u32 = 0xFFFF_0009; unsafe fn alloc_iterator(cp_array: *mut ArrayHeader) -> f64 { - let obj = js_object_alloc(STRING_ITERATOR_CLASS_ID, 2); + let scope = crate::gc::RuntimeHandleScope::new(); + let cp_h = scope.root_raw_mut_ptr(cp_array); + let obj_h = scope.root_raw_mut_ptr(js_object_alloc(STRING_ITERATOR_CLASS_ID, 2)); // Field 0: backing codepoint array (NaN-boxed pointer for the GC scanner). - js_object_set_field( - obj, - 0, - JSValue::from_bits(js_nanbox_pointer(cp_array as i64).to_bits()), - ); + obj_h.with_mut_ptr(|obj| { + cp_h.with_mut_ptr::(|cp| { + js_object_set_field( + obj, + 0, + JSValue::from_bits(js_nanbox_pointer(cp as i64).to_bits()), + ) + }) + }); // Field 1: cursor index, starts at 0. - js_object_set_field(obj, 1, JSValue::number(0.0)); - crate::object::attach_iterator_prototype(obj, STRING_ITERATOR_CLASS_ID); + obj_h.with_mut_ptr(|obj| js_object_set_field(obj, 1, JSValue::number(0.0))); + obj_h.with_mut_ptr(|obj| { + crate::object::attach_iterator_prototype(obj, STRING_ITERATOR_CLASS_ID) + }); + let (_, obj) = obj_h.across_mut::(|| ()); js_nanbox_pointer(obj as i64) } @@ -56,7 +65,11 @@ pub fn string_values_iter(s: *const StringHeader) -> f64 { return f64::from_bits(TAG_UNDEFINED); } unsafe { - let cp_array = crate::array::js_array_from_string_codepoints(s); + // Use the bounded WTF-8 iterator shared with string spread. A JS + // string may contain lone surrogates, so `str::chars()`-based + // materialization would reject the entire payload and produce an + // empty iterator. + let cp_array = crate::string::js_string_to_char_array(s as i64) as *mut ArrayHeader; alloc_iterator(cp_array) } } @@ -71,24 +84,33 @@ pub unsafe fn dispatch_string_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 arr = js_nanbox_get_pointer(backing) as *const ArrayHeader; - let idx = f64::from_bits(js_object_get_field(iter_obj, 1).bits()) as u32; - let len = if arr.is_null() { + if let Some(result) = + crate::object::call_overridden_iterator_next(iter_obj(), STRING_ITERATOR_CLASS_ID) + { + return result; + } + let backing = f64::from_bits(js_object_get_field(iter_obj(), 0).bits()); + let arr_h = scope.root_nanbox_f64(backing); + let arr = || js_nanbox_get_pointer(arr_h.get_nanbox_f64()) as *const ArrayHeader; + let idx = f64::from_bits(js_object_get_field(iter_obj(), 1).bits()) as u32; + let len = if arr().is_null() { 0 } else { - crate::array::js_array_length(arr) + crate::array::js_array_length(arr()) }; if idx >= len { return make_iter_result(JSValue::undefined(), true); } - js_object_set_field(iter_obj, 1, JSValue::number((idx + 1) as f64)); - let elem = crate::array::js_array_get_f64(arr, idx); + js_object_set_field(iter_obj(), 1, JSValue::number((idx + 1) as f64)); + let elem = crate::array::js_array_get_f64(arr(), idx); make_iter_result(JSValue::from_bits(elem.to_bits()), 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/symbol.rs b/crates/perry-runtime/src/symbol.rs index d184854302..dffcb0ddd2 100644 --- a/crates/perry-runtime/src/symbol.rs +++ b/crates/perry-runtime/src/symbol.rs @@ -42,10 +42,11 @@ pub use constructors::{ // Symbol-keyed property side-table operations. pub(crate) use properties::{ class_static_symbol_keys_for_class, clone_symbol_entries_for_obj_ptr, - get_symbol_property_attrs, inspect_custom_symbol_ptr, js_object_define_symbol_accessor, - js_object_delete_symbol_property, js_object_has_own_symbol_property, - reflect_symbol_getter_closure_bits, set_symbol_property_attrs, symbol_accessor_descriptor_bits, - symbol_property_is_enumerable, symbol_property_is_non_writable, symbol_property_root_bits, + define_symbol_data_property, get_symbol_property_attrs, inspect_custom_symbol_ptr, + js_object_define_symbol_accessor, js_object_delete_symbol_property, + js_object_has_own_symbol_property, reflect_symbol_getter_closure_bits, + set_symbol_property_attrs, symbol_accessor_descriptor_bits, symbol_property_is_enumerable, + symbol_property_is_non_writable, symbol_property_root_bits, }; pub use properties::{ class_static_symbol_lookup, js_class_register_static_symbol, js_object_has_own_symbol, diff --git a/crates/perry-runtime/src/symbol/iterator.rs b/crates/perry-runtime/src/symbol/iterator.rs index 54809d809f..2daa02c30c 100644 --- a/crates/perry-runtime/src/symbol/iterator.rs +++ b/crates/perry-runtime/src/symbol/iterator.rs @@ -243,11 +243,7 @@ pub extern "C" fn js_get_iterator(val_f64: f64) -> f64 { if jsv.is_pointer() { let ptr = jsv.as_pointer::(); if crate::object::is_arguments_object(ptr) { - if let Some(arr) = unsafe { crate::object::arguments_object_to_array(ptr) } { - let arr_f64 = - f64::from_bits(crate::value::JSValue::pointer(arr as *const u8).bits()); - return crate::array::array_values_iter(arr_f64); - } + return crate::array::arguments_values_iter(ptr); } } } diff --git a/crates/perry-runtime/src/symbol/properties.rs b/crates/perry-runtime/src/symbol/properties.rs index 32c967cb58..b2dbfac3b1 100644 --- a/crates/perry-runtime/src/symbol/properties.rs +++ b/crates/perry-runtime/src/symbol/properties.rs @@ -58,6 +58,20 @@ pub(crate) fn symbol_property_root_bits(owner: usize, sym_key: usize) -> Option< }) } +/// Install a symbol-keyed data descriptor after the caller has completed +/// `ValidateAndApplyPropertyDescriptor`. Unlike ordinary `[[Set]]`, this must +/// not be intercepted by the property's previous writable flag or setter. +pub(crate) unsafe fn define_symbol_data_property(obj_f64: f64, sym_f64: f64, value_f64: f64) { + let obj_key = obj_key_from_f64(obj_f64); + let sym_key = sym_key_from_f64(sym_f64); + if obj_key == 0 || sym_key == 0 { + return; + } + super::note_symbol_key_installed(sym_key); + accessors::clear_symbol_accessor_property(obj_key, sym_key); + store_object_symbol_property_root(obj_key, sym_key, value_f64.to_bits()); +} + pub(crate) fn get_symbol_property_attrs( owner: usize, sym_key: usize, diff --git a/crates/perry-runtime/src/typedarray/bigint.rs b/crates/perry-runtime/src/typedarray/bigint.rs index 751418bbd5..e46ad1cef4 100644 --- a/crates/perry-runtime/src/typedarray/bigint.rs +++ b/crates/perry-runtime/src/typedarray/bigint.rs @@ -11,8 +11,7 @@ //! for the construction / `set()` paths. use super::{ - jsvalue_to_f64, store_at, throw_type_error, typed_array_alloc, TypedArrayHeader, KIND_BIGINT64, - KIND_BIGUINT64, + store_at, throw_type_error, typed_array_alloc, TypedArrayHeader, KIND_BIGINT64, KIND_BIGUINT64, }; pub(crate) fn is_bigint_kind(kind: u8) -> bool { @@ -90,7 +89,10 @@ pub(crate) fn coerce_for_kind(dst_kind: u8, raw: f64) -> f64 { if dst_kind == KIND_BIGINT64 || dst_kind == KIND_BIGUINT64 { to_bigint_for_store(raw) } else { - jsvalue_to_f64(raw) + // Typed-array element conversion is the full observable ToNumber, + // not the primitive-only numeric unboxer. In particular an object + // source element must run its @@toPrimitive/valueOf hooks. + crate::builtins::js_number_coerce(raw) } } diff --git a/crates/perry-runtime/src/typedarray/construct.rs b/crates/perry-runtime/src/typedarray/construct.rs index cba70e3c7c..85120ebe08 100644 --- a/crates/perry-runtime/src/typedarray/construct.rs +++ b/crates/perry-runtime/src/typedarray/construct.rs @@ -307,15 +307,25 @@ pub(crate) unsafe fn typed_array_from_source_raw_values(val: f64) -> Vec { /// Coerce a snapshot of raw element values per `kind` (observable, may throw) /// and store them into a freshly allocated typed array. unsafe fn typed_array_from_snapshot(kind: u8, raw: Vec) -> *mut TypedArrayHeader { - let vals: Vec = raw - .into_iter() - .map(|v| bigint::coerce_for_kind(kind, v)) - .collect(); - let ta = typed_array_alloc(kind, vals.len() as u32); - for (i, v) in vals.iter().enumerate() { - store_at(ta, i, *v); + let scope = crate::gc::RuntimeHandleScope::new(); + let rooted = scope.root_nanbox_f64_slice(&raw); + typed_array_from_rooted_snapshot(kind, &rooted) +} + +/// Root-preserving sibling used while an iterator is still being driven: every +/// value yielded so far remains live across later `next()` calls and across +/// each observable numeric/BigInt coercion. +unsafe fn typed_array_from_rooted_snapshot( + kind: u8, + raw: &[crate::gc::RuntimeHandle<'_>], +) -> *mut TypedArrayHeader { + let scope = crate::gc::RuntimeHandleScope::new(); + let ta = scope.root_raw_mut_ptr(typed_array_alloc(kind, raw.len() as u32)); + for (i, value) in raw.iter().enumerate() { + let coerced = bigint::coerce_for_kind(kind, value.get_nanbox_f64()); + ta.with_mut_ptr::(|ta| store_at(ta, i, coerced)); } - ta + ta.across_mut::(|| ()).1 } /// `Get(obj, name)` for a plain-object or function source value. @@ -407,27 +417,21 @@ pub extern "C" fn js_typed_array_new_from_array( return typed_array_alloc(kind, 0); } rooted.set_raw_const_ptr(arr); - unsafe { - let len = (*arr).length; - // Snapshot the raw source values BEFORE any coercion. Per spec the - // source list is fully collected first and only THEN are the elements - // converted (`ToNumber`/`ToBigInt`) and stored. A converting element can - // run user code (`valueOf`/`Symbol.toPrimitive`) that mutates the source - // array — `Int32Array.from([0, { valueOf() { src.length = 0; return 100 }}, 2])` - // must still yield `[0, 100, 2]`, not lose the trailing element. Reading - // raw values first also keeps the snapshot ahead of the `typed_array_alloc` - // GC point (#871). - let raw: Vec = (0..len) - .map(|i| crate::array::js_array_get_f64(rooted.get_raw_const_ptr::(), i)) - .collect(); - let vals: Vec = raw - .into_iter() - .map(|v| bigint::coerce_for_kind(kind, v)) - .collect(); - let ta = typed_array_alloc(kind, len); - for (i, v) in vals.iter().enumerate() { - store_at(ta, i, *v); - } - ta + // InitializeTypedArrayFromArrayLike obtains and drives the source's + // iterator. Going through the real iterator protocol matters even for a + // dense Array: user code can replace Array.prototype[Symbol.iterator] or + // %ArrayIteratorPrototype%.next, and construction must observe either. + // Collect the raw values first and only then coerce them, retaining the + // mutation/snapshot rule described by `typed_array_from_snapshot`. + let source = rooted + .with_const_ptr::(|source| crate::value::js_nanbox_pointer(source as i64)); + let iter = crate::symbol::js_get_iterator(source); + let iter_rooted = scope.root_nanbox_f64(iter); + let mut raw = Vec::new(); + while let Some(value) = + crate::collection_iter::iterator_next_value(iter_rooted.get_nanbox_f64()) + { + raw.push(scope.root_nanbox_f64(value)); } + unsafe { typed_array_from_rooted_snapshot(kind, &raw) } } diff --git a/crates/perry-runtime/src/typedarray_props.rs b/crates/perry-runtime/src/typedarray_props.rs index 90630e12f6..26d3a44117 100644 --- a/crates/perry-runtime/src/typedarray_props.rs +++ b/crates/perry-runtime/src/typedarray_props.rs @@ -427,6 +427,13 @@ pub(crate) unsafe fn typed_array_define_own_property( "Cannot define property {key_name}, object is not extensible" )); } + let existing = typed_array_has_ordinary_own_prop(owner, key_name); + let current_attrs = existing + .then(|| crate::object::get_property_attrs(owner, key_name)) + .flatten() + .unwrap_or(crate::object::PropertyAttrs::new( + existing, existing, existing, + )); let has_get = descriptor_has(desc_ptr, b"get"); let has_set = descriptor_has(desc_ptr, b"set"); let has_accessor = has_get || has_set; @@ -467,9 +474,15 @@ pub(crate) unsafe fn typed_array_define_own_property( }; upsert_typed_array_own_prop(owner, key_name.to_string(), value, true); } - let writable = descriptor_bool(desc_ptr, b"writable").unwrap_or(has_accessor); - let enumerable = descriptor_bool(desc_ptr, b"enumerable").unwrap_or(false); - let configurable = descriptor_bool(desc_ptr, b"configurable").unwrap_or(false); + let writable = if has_accessor { + false + } else { + descriptor_bool(desc_ptr, b"writable").unwrap_or(current_attrs.writable()) + }; + let enumerable = + descriptor_bool(desc_ptr, b"enumerable").unwrap_or(current_attrs.enumerable()); + let configurable = + descriptor_bool(desc_ptr, b"configurable").unwrap_or(current_attrs.configurable()); crate::object::set_property_attrs( owner, key_name.to_string(), @@ -1077,6 +1090,7 @@ pub(crate) fn typed_array_own_set_descriptor( /// `Some(primitive)` when a patched method produced a non-object; `None` when /// no own patch applies (caller falls back to its default coercion). pub(crate) unsafe fn typed_array_own_to_primitive_number(owner: usize, value: f64) -> Option { + let mut non_primitive_calls = 0u8; for name in ["valueOf", "toString"] { let Some(m) = typed_array_get_property_value_by_name(owner, name) else { continue; @@ -1096,6 +1110,13 @@ pub(crate) unsafe fn typed_array_own_to_primitive_number(owner: usize, value: f6 if !is_object { return Some(r); } + non_primitive_calls += 1; + } + if non_primitive_calls >= 2 { + let msg = b"Cannot convert object to primitive value"; + let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + let err = crate::error::js_typeerror_new(s); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)); } None } diff --git a/crates/perry-runtime/src/value/dyn_index.rs b/crates/perry-runtime/src/value/dyn_index.rs index 491311bdc2..34fcb6b248 100644 --- a/crates/perry-runtime/src/value/dyn_index.rs +++ b/crates/perry-runtime/src/value/dyn_index.rs @@ -157,6 +157,16 @@ pub extern "C" fn js_dyn_index_get(value: f64, index: f64) -> f64 { if bits == TAG_UNDEFINED || bits == TAG_NULL { crate::object::has_own_helpers::throw_to_object_nullish_type_error(); } + // Property access on a Symbol primitive operates on a temporary boxed + // receiver so inherited Symbol.prototype properties/accessors participate + // in ordinary Get semantics. + if unsafe { crate::symbol::js_is_symbol(value) } != 0 { + let scope = crate::gc::RuntimeHandleScope::new(); + let symbol = scope.root_nanbox_f64(value); + let index = scope.root_nanbox_f64(index); + let boxed = crate::builtins::js_boxed_symbol_new(symbol.get_nanbox_f64()); + return js_dyn_index_get(boxed, index.get_nanbox_f64()); + } let jsval = JSValue::from_bits(bits); // #5525: a Symbol *index* (`obj[Symbol.iterator]`) must resolve through the // symbol side-table, never the integer-index / stringify paths below (which @@ -529,6 +539,16 @@ pub extern "C" fn js_dyn_index_set(obj: f64, index: f64, value: f64) -> f64 { crate::proxy::js_proxy_set(obj, index, value); return value; } + // Sloppy assignment still targets only the ephemeral ToObject wrapper, + // but it must run inherited Symbol.prototype setters before disappearing. + if unsafe { crate::symbol::js_is_symbol(obj) } != 0 { + let scope = crate::gc::RuntimeHandleScope::new(); + let symbol = scope.root_nanbox_f64(obj); + let index = scope.root_nanbox_f64(index); + let value = scope.root_nanbox_f64(value); + let boxed = crate::builtins::js_boxed_symbol_new(symbol.get_nanbox_f64()); + return js_dyn_index_set(boxed, index.get_nanbox_f64(), value.get_nanbox_f64()); + } // #5525: a Symbol *index* (`obj[sym] = v`) routes to the symbol side-table, // mirroring the get side. Codegen sends all non-string-literal unknown- // receiver writes here, so the runtime owns the symbol triage. @@ -619,6 +639,19 @@ pub extern "C" fn js_dyn_index_set(obj: f64, index: f64, value: f64) -> f64 { if raw_ptr < crate::gc::GC_HEADER_SIZE + 0x1000 { return value; } + // String-named computed writes use ordinary receiver-aware [[Set]]. This + // must precede the raw buffer/view branches: DataView and ArrayBuffer carry + // ordinary expandos in a side table, and returning from the byte-index + // branch would otherwise swallow `view[name] = value`. + let idx_top16 = index.to_bits() >> 48; + if idx_top16 == 0x7FFF || idx_top16 == 0x7FF9 { + let target = if jsval.is_pointer() { + obj + } else { + f64::from_bits(crate::value::js_nanbox_pointer(raw_ptr as i64).to_bits()) + }; + return crate::proxy::js_put_value_set(target, index, value, target, 0); + } if crate::typedarray::lookup_typed_array_kind(raw_ptr).is_some() { crate::typedarray_props::js_typed_array_index_set_dynamic( raw_ptr as *mut crate::typedarray::TypedArrayHeader, @@ -660,6 +693,30 @@ pub extern "C" fn js_dyn_index_set(obj: f64, index: f64, value: f64) -> f64 { if receiver_tag.is_some_and(|(obj_type, _)| is_registered_collection(raw_ptr, obj_type)) { return value; } + // An ordinary receiver whose explicit [[Prototype]] is a TypedArray must + // consult that integer-indexed exotic for canonical but INVALID numeric + // keys. Such a write is a successful no-op and must not create an own + // property on the receiver. Valid indices, however, continue through the + // ordinary receiver path below: they create an own property (and therefore + // reject a non-extensible receiver in strict code). + if let Some(proto_bits) = crate::object::prototype_chain::object_static_prototype(raw_ptr) { + let proto_addr = crate::value::js_nanbox_get_pointer(f64::from_bits(proto_bits)) as usize; + if proto_addr != 0 && crate::typedarray::lookup_typed_array_kind(proto_addr).is_some() { + let length = unsafe { + (*(proto_addr as *const crate::typedarray::TypedArrayHeader)).length as u32 + }; + let is_valid_index = finite_nonnegative_u32_index(index) + .is_some_and(|numeric_index| numeric_index < length); + if !is_valid_index { + let target = if jsval.is_pointer() { + obj + } else { + f64::from_bits(crate::value::js_nanbox_pointer(raw_ptr as i64).to_bits()) + }; + return crate::proxy::js_put_value_set(target, index, value, target, 0); + } + } + } // #5579 / Issue #957 (set side): a STRING index (`obj["foo"] = v`) must // route through the ordinary receiver-aware `[[Set]]`, NOT the numeric // element path below. A NaN-boxed string index otherwise reached the @@ -678,17 +735,6 @@ pub extern "C" fn js_dyn_index_set(obj: f64, index: f64, value: f64) -> f64 { // `finite_nonnegative_u32_index`, so NaN/fractional keys fall through to // the ToString write instead of aliasing element 0), so the #5544 perf // win stands. - let idx_top16 = index.to_bits() >> 48; - if idx_top16 == 0x7FFF || idx_top16 == 0x7FF9 { - // `target`/`receiver` must be a tagged value, not the raw heap address - // (`obj` arrives as a module-slot raw I64 when top16 == 0). - let target = if jsval.is_pointer() { - obj - } else { - f64::from_bits(crate::value::js_nanbox_pointer(raw_ptr as i64).to_bits()) - }; - return crate::proxy::js_put_value_set(target, index, value, target, 0); - } if let Some(idx_u32) = finite_nonnegative_u32_index(index) { if unsafe { crate::object::arguments_object_set_index( diff --git a/crates/perry-runtime/src/value/dynamic_arith.rs b/crates/perry-runtime/src/value/dynamic_arith.rs index f023a465da..6c0b1fbb21 100644 --- a/crates/perry-runtime/src/value/dynamic_arith.rs +++ b/crates/perry-runtime/src/value/dynamic_arith.rs @@ -702,6 +702,18 @@ pub unsafe extern "C" fn js_dynamic_neg(a: f64) -> f64 { -a } +/// Unary plus performs ToNumber, not the explicit `Number()` conversion: +/// after ToPrimitive an Object-wrapped BigInt must therefore throw instead of +/// being lossily converted to f64. +#[no_mangle] +pub unsafe extern "C" fn js_dynamic_pos(a: f64) -> f64 { + let numeric = to_numeric(a); + if JSValue::from_bits(numeric.to_bits()).is_bigint() { + throw_add_type_error(b"Cannot convert a BigInt value to a number"); + } + numeric +} + /// Dynamic bitwise NOT: `~BigInt` stays BigInt, otherwise use JS ToInt32. #[no_mangle] pub unsafe extern "C" fn js_dynamic_bitnot(a: f64) -> f64 { diff --git a/crates/perry-runtime/src/value/mod.rs b/crates/perry-runtime/src/value/mod.rs index 6babf4534a..6b9333de75 100644 --- a/crates/perry-runtime/src/value/mod.rs +++ b/crates/perry-runtime/src/value/mod.rs @@ -105,9 +105,9 @@ pub use nanbox::{ // ----- Dynamic arithmetic dispatch (BigInt vs float) ----- pub use dynamic_arith::{ js_dynamic_add, js_dynamic_bitand, js_dynamic_bitor, js_dynamic_bitxor, js_dynamic_div, - js_dynamic_mod, js_dynamic_mul, js_dynamic_neg, js_dynamic_pow, js_dynamic_shl, js_dynamic_shr, - js_dynamic_string_or_number_add, js_dynamic_sub, js_dynamic_ushr, js_numeric_step, - js_to_numeric, + js_dynamic_mod, js_dynamic_mul, js_dynamic_neg, js_dynamic_pos, js_dynamic_pow, js_dynamic_shl, + js_dynamic_shr, js_dynamic_string_or_number_add, js_dynamic_sub, js_dynamic_ushr, + js_numeric_step, js_to_numeric, }; // ----- Dynamic index get/set + bare-NaN check ----- diff --git a/crates/perry-runtime/src/value/to_string.rs b/crates/perry-runtime/src/value/to_string.rs index b41a9b87a4..2db7306c55 100644 --- a/crates/perry-runtime/src/value/to_string.rs +++ b/crates/perry-runtime/src/value/to_string.rs @@ -706,13 +706,30 @@ unsafe fn call_method_for_primitive( } let key = crate::string::js_string_from_bytes(method_name.as_ptr(), method_name.len() as u32); let key_handle = scope.root_string_ptr(key); - let key_ptr = key_handle.get_raw_const_ptr::(); - let has_own_method_key = crate::object::own_key_present(obj_ptr as *mut _, key_ptr); - let method = crate::object::js_object_get_field_by_name(obj_ptr, key_ptr); + // Presence is independent from the value returned by Get. In particular, + // an inherited accessor may exist yet return undefined/null; that is a + // present but non-callable method, so OrdinaryToPrimitive must continue to + // the other candidate rather than synthesizing a boxed-primitive default. + // `own_key_present(receiver)` cannot see that inherited descriptor. + let key_value = key_handle.with_const_ptr::(|key_ptr| { + f64::from_bits( + crate::value::JSValue::string_ptr(key_ptr as *mut crate::string::StringHeader).bits(), + ) + }); + let has_method_key = + crate::object::js_object_has_property(value_handle.get_nanbox_f64(), key_value).to_bits() + == crate::value::TAG_TRUE; + // `HasProperty` can run a Proxy trap and collect. Refresh the receiver and + // key from their handles before the subsequent ordinary Get. + let recv = value_handle.get_nanbox_f64(); + let obj_ptr = (recv.to_bits() & POINTER_MASK) as *const crate::object::ObjectHeader; + let method = key_handle.with_const_ptr::(|key_ptr| { + crate::object::js_object_get_field_by_name(obj_ptr, key_ptr) + }); // Must be a callable closure value (POINTER_TAG + CLOSURE_MAGIC). let method_bits = method.bits(); if (method_bits & 0xFFFF_0000_0000_0000) != POINTER_TAG { - return if has_own_method_key || (!method.is_undefined() && !method.is_null()) { + return if has_method_key || (!method.is_undefined() && !method.is_null()) { MethodOutcome::NonPrimitive } else { MethodOutcome::Absent @@ -720,7 +737,7 @@ unsafe fn call_method_for_primitive( } let method_ptr = (method_bits & POINTER_MASK) as usize; if !crate::closure::is_closure_ptr(method_ptr) { - return if has_own_method_key { + return if has_method_key { MethodOutcome::NonPrimitive } else { MethodOutcome::Absent diff --git a/crates/perry-transform/src/generator/lower.rs b/crates/perry-transform/src/generator/lower.rs index 46facb2fbb..05c341c013 100644 --- a/crates/perry-transform/src/generator/lower.rs +++ b/crates/perry-transform/src/generator/lower.rs @@ -975,7 +975,7 @@ pub fn transform_generator_function_with_extra_captures( Expr::LocalGet(return_param_id), true, )))); - if !is_async_generator && has_yielding_finally { + if has_yielding_finally { // #4438 B2-finally: route `.return(v)` into the innermost enclosing // yielding finally (record the pending return + jump in), then fall // through to the continuation loop so the finally's `yield`s suspend; diff --git a/scripts/gc_runtime_root_holders.json b/scripts/gc_runtime_root_holders.json index 274f72411f..fe829e1a29 100644 --- a/scripts/gc_runtime_root_holders.json +++ b/scripts/gc_runtime_root_holders.json @@ -780,14 +780,6 @@ "file": "crates/perry-runtime/src/object/class_registry/state.rs", "name": "FUNCTION_CLASS_IDS" }, - { - "file": "crates/perry-runtime/src/object/collection_proto_thunks.rs", - "name": "BUILTIN_MAP_SET_VALUE_BITS" - }, - { - "file": "crates/perry-runtime/src/object/collection_proto_thunks.rs", - "name": "BUILTIN_SET_ADD_VALUE_BITS" - }, { "file": "crates/perry-runtime/src/object/field_get_set/field_ops.rs", "name": "WARN_NULL_PTR_STATE" @@ -830,6 +822,14 @@ "file": "crates/perry-runtime/src/object/iterator_prototypes.rs", "name": "STRING_ITERATOR_PROTOTYPE_PTR_SLOT" }, + { + "file": "crates/perry-runtime/src/object/mod.rs", + "name": "ASYNC_FUNCTION_INTRINSIC_PROTO_PTR_SLOT" + }, + { + "file": "crates/perry-runtime/src/object/mod.rs", + "name": "ASYNC_FUNCTION_INTRINSIC_PTR_SLOT" + }, { "file": "crates/perry-runtime/src/object/mod.rs", "name": "ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR_SLOT" diff --git a/scripts/raw_handle_debt_baseline.txt b/scripts/raw_handle_debt_baseline.txt index ced7b1ba5b..209fc0eb49 100644 --- a/scripts/raw_handle_debt_baseline.txt +++ b/scripts/raw_handle_debt_baseline.txt @@ -1 +1 @@ -925 +923 diff --git a/scripts/raw_handle_debt_files.txt b/scripts/raw_handle_debt_files.txt index 40becbb8ca..91bd49155a 100644 --- a/scripts/raw_handle_debt_files.txt +++ b/scripts/raw_handle_debt_files.txt @@ -141,7 +141,7 @@ 27 crates/perry-runtime/src/thread.rs 7 crates/perry-runtime/src/timer.rs 1 crates/perry-runtime/src/typed_feedback.rs -4 crates/perry-runtime/src/typedarray/construct.rs +3 crates/perry-runtime/src/typedarray/construct.rs 2 crates/perry-runtime/src/typedarray/transform.rs 4 crates/perry-runtime/src/url/node_compat.rs 7 crates/perry-runtime/src/url/search_params.rs @@ -151,7 +151,7 @@ 10 crates/perry-runtime/src/v8.rs 6 crates/perry-runtime/src/value/dyn_index.rs 6 crates/perry-runtime/src/value/dynamic_arith.rs -3 crates/perry-runtime/src/value/to_string.rs +2 crates/perry-runtime/src/value/to_string.rs 19 crates/perry-runtime/src/wasi.rs 6 crates/perry-runtime/src/weakref.rs 20 crates/perry-runtime/src/webassembly.rs From 248df3926183fec1aa2674fff24837fc8153f533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 23 Aug 2026 20:32:04 +0200 Subject: [PATCH 10/10] chore: fmt the stack and add the two missing changelog fragments #8646 and #8650 landed without a changelog.d fragment; #8650 also lowers the raw-handle ratchet 925 -> 923, which is recorded in its fragment. --- changelog.d/8646-computed-property-reflection.md | 2 ++ changelog.d/8650-test262-builtins-misc-tail.md | 7 +++++++ crates/perry-codegen/src/expr/array_literal.rs | 6 +----- crates/perry-codegen/src/native_emit.rs | 8 +++++++- crates/perry/tests/const_array_descriptor_8583.rs | 13 +++++++++++-- 5 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 changelog.d/8646-computed-property-reflection.md create mode 100644 changelog.d/8650-test262-builtins-misc-tail.md 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/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/crates/perry-codegen/src/expr/array_literal.rs b/crates/perry-codegen/src/expr/array_literal.rs index fa49bafa53..da9616a8ec 100644 --- a/crates/perry-codegen/src/expr/array_literal.rs +++ b/crates/perry-codegen/src/expr/array_literal.rs @@ -301,11 +301,7 @@ fn const_array_descriptor_enabled() -> bool { /// 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::Number(_) | Expr::Integer(_) | Expr::Bool(_) | Expr::Null | Expr::Undefined => true, Expr::Array(elems) => elems.iter().all(is_const_materializable), _ => false, } diff --git a/crates/perry-codegen/src/native_emit.rs b/crates/perry-codegen/src/native_emit.rs index d5edc5d6db..2010ee7090 100644 --- a/crates/perry-codegen/src/native_emit.rs +++ b/crates/perry-codegen/src/native_emit.rs @@ -302,7 +302,13 @@ fn dump_dialect_failure(f: &FrozenFunction, e: anyhow::Error) -> anyhow::Error { let safe: String = f .name .chars() - .map(|c| if c.is_alphanumeric() || c == '_' { c } else { '_' }) + .map(|c| { + if c.is_alphanumeric() || c == '_' { + c + } else { + '_' + } + }) .collect(); let _ = std::fs::write(format!("{dir}/{safe}.ll"), &buf); } diff --git a/crates/perry/tests/const_array_descriptor_8583.rs b/crates/perry/tests/const_array_descriptor_8583.rs index bdcb859b29..b4ab93e504 100644 --- a/crates/perry/tests/const_array_descriptor_8583.rs +++ b/crates/perry/tests/const_array_descriptor_8583.rs @@ -37,7 +37,12 @@ fn source() -> String { if i > 0 { rows.push(','); } - rows.push_str(&format!("[{},{},{}]", i % 128, (i * 7) % 128, (i * 13) % 128)); + rows.push_str(&format!( + "[{},{},{}]", + i % 128, + (i * 7) % 128, + (i * 13) % 128 + )); } // One non-numeric row so bool/null tags are exercised in the descriptor. rows.push_str(",[true,null,false]"); @@ -164,7 +169,11 @@ fn const_array_descriptor_fires_in_ir() { .env_remove("PERRY_CONST_ARRAY_DESCRIPTOR") .output() .expect("run perry compile --no-link"); - assert!(out.status.success(), "compile failed: {}", String::from_utf8_lossy(&out.stderr)); + assert!( + out.status.success(), + "compile failed: {}", + String::from_utf8_lossy(&out.stderr) + ); let ir: String = std::fs::read_dir(&ll_dir) .unwrap()