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