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/computed_store_rooting_tests.rs b/crates/perry-codegen/src/expr/computed_store_rooting_tests.rs index 9aeadbeb13..97e220aeaf 100644 --- a/crates/perry-codegen/src/expr/computed_store_rooting_tests.rs +++ b/crates/perry-codegen/src/expr/computed_store_rooting_tests.rs @@ -796,7 +796,7 @@ fn collecting_masked_window_index_declines_the_hoisted_pointer_tier() { let collecting = masked_window_index_coercion_loop(Type::Any); assert!( - calls(&collecting, "js_number_coerce"), + calls(&collecting, "js_dynamic_pos"), "unary + over an any key must exercise the collecting coercion witness:\n{collecting}" ); assert!( @@ -819,7 +819,7 @@ fn collecting_rhs_between_masked_reads_declines_the_hoisted_pointer_tier() { let collecting = masked_window_rhs_coercion_loop(Type::Any); assert!( - calls(&collecting, "js_number_coerce"), + calls(&collecting, "js_dynamic_pos"), "the any-typed RHS must exercise the user-coercion witness:\n{collecting}" ); assert!( @@ -842,7 +842,7 @@ fn collecting_rhs_declines_the_straight_line_masked_region() { let collecting = masked_window_rhs_coercion_region(Type::Any); assert!( - calls(&collecting, "js_number_coerce"), + calls(&collecting, "js_dynamic_pos"), "the any-typed region RHS must exercise the user-coercion witness:\n{collecting}" ); assert!( diff --git a/crates/perry-codegen/src/expr/unary.rs b/crates/perry-codegen/src/expr/unary.rs index 311e3e2d7a..01f15e13eb 100644 --- a/crates/perry-codegen/src/expr/unary.rs +++ b/crates/perry-codegen/src/expr/unary.rs @@ -42,7 +42,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { if numeric { Ok(v) } else { - Ok(blk.call(DOUBLE, "js_number_coerce", &[(DOUBLE, &v)])) + Ok(blk.call(DOUBLE, "js_dynamic_pos", &[(DOUBLE, &v)])) } } UnaryOp::Not => { diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs index df1c5204d1..a89983e334 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs @@ -119,6 +119,7 @@ pub(crate) fn declare_core(module: &mut LlModule) { // ========== NaN-boxing / typeof / is_* ========== module.declare_function("js_dynamic_neg", DOUBLE, &[DOUBLE]); + module.declare_function("js_dynamic_pos", DOUBLE, &[DOUBLE]); module.declare_function("js_dynamic_string_equals", I32, &[DOUBLE, DOUBLE]); module.declare_function("js_is_nan", DOUBLE, &[DOUBLE]); module.declare_function("js_jsvalue_compare", I32, &[DOUBLE, DOUBLE]); diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs index ad24e28397..c8a1ff4a19 100644 --- a/crates/perry-codegen/src/runtime_decls/strings.rs +++ b/crates/perry-codegen/src/runtime_decls/strings.rs @@ -1303,6 +1303,7 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { module.declare_function("js_instanceof_noncallable_rhs", DOUBLE, &[]); module.declare_function("js_register_class_extends_error", VOID, &[I32]); module.declare_function("js_register_class_extends_data_view", VOID, &[I32]); + module.declare_function("js_register_class_extends_typed_array", VOID, &[I32]); module.declare_function("js_register_class_id", VOID, &[I32]); // #1021 NestJS: surface Perry class names to V8 so `metatype.name` // is non-empty. Codegen emits one call per registered class id at diff --git a/crates/perry-codegen/src/type_analysis.rs b/crates/perry-codegen/src/type_analysis.rs index c15c84113c..01d70ace64 100644 --- a/crates/perry-codegen/src/type_analysis.rs +++ b/crates/perry-codegen/src/type_analysis.rs @@ -38,8 +38,8 @@ pub(crate) use numeric::{ pub(crate) use pod::{ add_operands_have_pod_materialization_hazard, expr_may_return_boxed_value_from_raw_f64_fallback, expression_has_numeric_length, - is_fixed_width_buffer_numeric_read, is_numeric_typed_array_class, is_typed_array_expr, - numeric_proof_is_declared_only, pod_record_field_is_numeric, + is_fixed_width_buffer_numeric_read, is_numeric_typed_array_class, is_typed_array_class, + is_typed_array_expr, numeric_proof_is_declared_only, pod_record_field_is_numeric, scalar_replaced_array_element_is_raw_f64, scalar_replaced_field_is_raw_f64, scalar_replaced_field_raw_f64_store_state, }; diff --git a/crates/perry-hir/src/lower/const_fold_fn.rs b/crates/perry-hir/src/lower/const_fold_fn.rs index 39bb4f830f..7257daf37d 100644 --- a/crates/perry-hir/src/lower/const_fold_fn.rs +++ b/crates/perry-hir/src/lower/const_fold_fn.rs @@ -36,6 +36,9 @@ use super::global_eval_hoist::{ use super::lower_expr::lower_expr; use super::LoweringContext; +mod param_early_error; +use param_early_error::fn_ctor_kind_param_early_error; + /// Lower an expression that throws a `SyntaxError` when the enclosing call /// site is evaluated — a throwing IIFE in value position. Used when a folded /// `new Function(...)` / `Function(...)` body is not syntactically valid JS, @@ -311,37 +314,6 @@ pub(crate) fn try_const_fold_function_construct_kind( None => (String::new(), String::new()), }; - // CSP capability-probe handling (`PERRY_EVAL_CSP`). A trivial no-op - // `new Function("")` / `Function("")` is the canonical runtime-codegen - // feature-test (`try { new Function(""), true } catch { false }`). perry is - // ahead-of-time compiled and cannot generate code from a runtime string, so - // under CSP mode this probe must report "unavailable" — throw at - // *construction* (not when called), exactly as a CSP `unsafe-eval`-blocked - // environment does — so probing callers (e.g. zod 4's validator JIT) take - // their non-codegen interpreter fallback. Only the trivial empty-body no-op - // is refused; real literal bodies (`return 42`, the `return this` globalThis - // polyfill) still fold, preserving spec behavior by default. - // - // The probe passes AT LEAST ONE string argument (`new Function("")`). A - // ZERO-argument `new Function()` is not a codegen request at all — it - // constructs the empty function `anonymous() {}` — so it must NEVER throw, - // even under the default CSP mode (#5835 Intl-ctor test regressed here: - // `new Function()` used purely as a constructable-with-settable-prototype - // scaffold for `Reflect.construct` began throwing). Gate the refusal on - // there actually being an argument. - if !consts.is_empty() - && body_src.trim().is_empty() - && crate::eval_classifier::eval_csp_probe_unavailable() - { - return synth_throwing_iife( - ctx, - "throw new TypeError(\"Function: runtime dynamic code generation is \ - unavailable in this ahead-of-time compiled binary\");", - span, - ) - .map(Some); - } - // Assemble the exact source text the spec's CreateDynamicFunction // prescribes: newlines around the body and *before the closing paren* // so a `//` comment in the params or body can't swallow a delimiter. @@ -374,10 +346,49 @@ pub(crate) fn try_const_fold_function_construct_kind( // prologue makes duplicate or `eval`/`arguments` parameter names a // SyntaxError, and a private name (`o.#f`) outside any class body is a // SyntaxError regardless of mode (AllPrivateIdentifiersValid). - if fn_ctor_strict_param_early_error(fn_expr) || fn_body_has_stray_private_name(fn_expr) { + if fn_ctor_kind_param_early_error(fn_expr, kind) + || fn_ctor_strict_param_early_error(fn_expr) + || fn_body_has_stray_private_name(fn_expr) + { return synth_function_syntax_error(ctx, surface, span).map(Some); } + // CSP capability-probe handling (`PERRY_EVAL_CSP`). A trivial no-op + // `new Function("")` / `Function("")` is the canonical runtime-codegen + // feature-test (`try { new Function(""), true } catch { false }`). perry is + // ahead-of-time compiled and cannot generate code from a runtime string, so + // under CSP mode this probe must report "unavailable" — throw at + // *construction* (not when called), exactly as a CSP `unsafe-eval`-blocked + // environment does — so probing callers (e.g. zod 4's validator JIT) take + // their non-codegen interpreter fallback. Only the trivial empty-body no-op + // is refused; real literal bodies (`return 42`, the `return this` globalThis + // polyfill) still fold, preserving spec behavior by default. + // + // Parameter/body syntax and CreateDynamicFunction early errors take + // precedence over this Perry-specific capability signal. In particular, + // `GeneratorFunction("x = yield", "")` must throw SyntaxError rather than + // being mistaken for a valid empty-body capability probe. + // + // The probe passes AT LEAST ONE string argument (`new Function("")`). A + // ZERO-argument `new Function()` is not a codegen request at all — it + // constructs the empty function `anonymous() {}` — so it must NEVER throw, + // even under the default CSP mode (#5835 Intl-ctor test regressed here: + // `new Function()` used purely as a constructable-with-settable-prototype + // scaffold for `Reflect.construct` began throwing). Gate the refusal on + // there actually being an argument. + if !consts.is_empty() + && body_src.trim().is_empty() + && crate::eval_classifier::eval_csp_probe_unavailable() + { + return synth_throwing_iife( + ctx, + "throw new TypeError(\"Function: runtime dynamic code generation is \ + unavailable in this ahead-of-time compiled binary\");", + span, + ) + .map(Some); + } + let outer_strict = ctx.current_strict; ctx.current_strict = false; let lowered_result = lower_fn_expr(ctx, fn_expr); @@ -1352,7 +1363,7 @@ pub(crate) fn try_eval_function_call_fold( } // `var AsyncFunction = (async function(){}).constructor; AsyncFunction(...)` // — a single-assignment module var recorded as a dynamic-function ctor. - if ctx.scope_depth == 0 { + if ctx.local_decl_scope_depth(id.sym.as_str()) == Some(0) { if let Some(super::fn_ctor_env::FnCtorShape::DynCtor(kind)) = ctx.fn_ctor_env.entries.get(id.sym.as_str()).cloned() { diff --git a/crates/perry-hir/src/lower/const_fold_fn/param_early_error.rs b/crates/perry-hir/src/lower/const_fold_fn/param_early_error.rs new file mode 100644 index 0000000000..8dda185ead --- /dev/null +++ b/crates/perry-hir/src/lower/const_fold_fn/param_early_error.rs @@ -0,0 +1,116 @@ +use swc_ecma_ast as ast; + +use super::super::fn_ctor_env::DynFnCtorKind; + +/// CreateDynamicFunction's parameter early errors are kind-sensitive. Inspect +/// the parsed parameter AST so keyword-looking text in comments or string +/// literals is ignored while real `yield` / `await` syntax is rejected. +pub(super) fn fn_ctor_kind_param_early_error(fn_expr: &ast::FnExpr, kind: DynFnCtorKind) -> bool { + let forbidden = match kind { + DynFnCtorKind::Generator => &["yield"][..], + DynFnCtorKind::Async => &["await"][..], + DynFnCtorKind::AsyncGenerator => &["yield", "await"][..], + DynFnCtorKind::Plain => return false, + }; + + fn prop_name_has(name: &ast::PropName, forbidden: &[&str]) -> bool { + matches!(name, ast::PropName::Computed(c) if expr_has(&c.expr, forbidden)) + } + + fn pat_has(pat: &ast::Pat, forbidden: &[&str]) -> bool { + match pat { + ast::Pat::Ident(id) => forbidden.contains(&id.id.sym.as_ref()), + ast::Pat::Array(array) => array + .elems + .iter() + .flatten() + .any(|pat| pat_has(pat, forbidden)), + ast::Pat::Object(object) => object.props.iter().any(|prop| match prop { + ast::ObjectPatProp::KeyValue(kv) => { + prop_name_has(&kv.key, forbidden) || pat_has(&kv.value, forbidden) + } + ast::ObjectPatProp::Assign(assign) => { + forbidden.contains(&assign.key.sym.as_ref()) + || assign + .value + .as_deref() + .is_some_and(|value| expr_has(value, forbidden)) + } + ast::ObjectPatProp::Rest(rest) => pat_has(&rest.arg, forbidden), + }), + ast::Pat::Assign(assign) => { + pat_has(&assign.left, forbidden) || expr_has(&assign.right, forbidden) + } + ast::Pat::Rest(rest) => pat_has(&rest.arg, forbidden), + ast::Pat::Expr(expr) => expr_has(expr, forbidden), + ast::Pat::Invalid(_) => false, + } + } + + fn expr_has(expr: &ast::Expr, forbidden: &[&str]) -> bool { + match expr { + ast::Expr::Ident(id) => forbidden.contains(&id.sym.as_ref()), + ast::Expr::Yield(_) => forbidden.contains(&"yield"), + ast::Expr::Await(await_expr) => { + forbidden.contains(&"await") || expr_has(&await_expr.arg, forbidden) + } + ast::Expr::Paren(paren) => expr_has(&paren.expr, forbidden), + ast::Expr::Unary(unary) => expr_has(&unary.arg, forbidden), + ast::Expr::Update(update) => expr_has(&update.arg, forbidden), + ast::Expr::Bin(binary) => { + expr_has(&binary.left, forbidden) || expr_has(&binary.right, forbidden) + } + ast::Expr::Assign(assign) => expr_has(&assign.right, forbidden), + ast::Expr::Cond(cond) => { + expr_has(&cond.test, forbidden) + || expr_has(&cond.cons, forbidden) + || expr_has(&cond.alt, forbidden) + } + ast::Expr::Seq(seq) => seq.exprs.iter().any(|expr| expr_has(expr, forbidden)), + ast::Expr::Member(member) => { + expr_has(&member.obj, forbidden) + || matches!(&member.prop, ast::MemberProp::Computed(c) if expr_has(&c.expr, forbidden)) + } + ast::Expr::Call(call) => { + matches!(&call.callee, ast::Callee::Expr(expr) if expr_has(expr, forbidden)) + || call.args.iter().any(|arg| expr_has(&arg.expr, forbidden)) + } + ast::Expr::New(new_expr) => { + expr_has(&new_expr.callee, forbidden) + || new_expr + .args + .as_ref() + .is_some_and(|args| args.iter().any(|arg| expr_has(&arg.expr, forbidden))) + } + ast::Expr::Array(array) => array + .elems + .iter() + .flatten() + .any(|elem| expr_has(&elem.expr, forbidden)), + ast::Expr::Object(object) => object.props.iter().any(|prop| match prop { + ast::PropOrSpread::Spread(spread) => expr_has(&spread.expr, forbidden), + ast::PropOrSpread::Prop(prop) => match prop.as_ref() { + ast::Prop::KeyValue(kv) => { + prop_name_has(&kv.key, forbidden) || expr_has(&kv.value, forbidden) + } + ast::Prop::Assign(assign) => expr_has(&assign.value, forbidden), + ast::Prop::Getter(getter) => prop_name_has(&getter.key, forbidden), + ast::Prop::Setter(setter) => prop_name_has(&setter.key, forbidden), + ast::Prop::Method(method) => prop_name_has(&method.key, forbidden), + ast::Prop::Shorthand(id) => forbidden.contains(&id.sym.as_ref()), + }, + }), + ast::Expr::TsAs(ts) => expr_has(&ts.expr, forbidden), + ast::Expr::TsTypeAssertion(ts) => expr_has(&ts.expr, forbidden), + ast::Expr::TsConstAssertion(ts) => expr_has(&ts.expr, forbidden), + ast::Expr::TsNonNull(ts) => expr_has(&ts.expr, forbidden), + _ => false, + } + } + + fn_expr + .function + .params + .iter() + .any(|param| pat_has(¶m.pat, forbidden)) +} diff --git a/crates/perry-hir/src/lower/expr_assign.rs b/crates/perry-hir/src/lower/expr_assign.rs index a66d010223..b2c9799908 100644 --- a/crates/perry-hir/src/lower/expr_assign.rs +++ b/crates/perry-hir/src/lower/expr_assign.rs @@ -482,6 +482,12 @@ pub(crate) fn lower_ident_assignment( Ok(*value) } else { if ctx.current_strict { + if matches!(name.as_str(), "undefined" | "NaN" | "Infinity") { + return Ok(Expr::Sequence(vec![ + *value, + throw_type_error_const_assignment(&name), + ])); + } // #5989: strict-mode assignment to an existing global // builtin is a property write, not a ReferenceError. See // `strict_global_assign_existing_or_throw` for the full @@ -554,6 +560,25 @@ fn lower_assignment_target( // Check if this is a static field assignment (e.g., Counter.count = 5) if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { let obj_name = obj_ident.sym.to_string(); + // Dynamic GeneratorFunction/AsyncGeneratorFunction results + // use a compact non-closure runtime representation, but still + // inherit Function.prototype's poisoned caller/arguments + // accessors. The local's inferred brand is authoritative here. + if let ast::MemberProp::Ident(prop_ident) = &member.prop { + let is_dynamic_generator = matches!( + ctx.lookup_local_type(&obj_name), + Some(Type::Named(name)) + if matches!(name.as_str(), "GeneratorFunction" | "AsyncGeneratorFunction") + ); + if is_dynamic_generator + && matches!(prop_ident.sym.as_ref(), "caller" | "arguments") + { + return Ok(Expr::Sequence(vec![ + *value, + throw_restricted_function_property_assignment(), + ])); + } + } // `f.caller = v` / `f.arguments = v` on a declared function — // the poisoned setter-less accessor on Function.prototype // throws (strict semantics; Perry-compiled code is strict). diff --git a/crates/perry-hir/src/lower/expr_call/url_date_instance.rs b/crates/perry-hir/src/lower/expr_call/url_date_instance.rs index 840c752f9c..fd842b7a68 100644 --- a/crates/perry-hir/src/lower/expr_call/url_date_instance.rs +++ b/crates/perry-hir/src/lower/expr_call/url_date_instance.rs @@ -92,6 +92,19 @@ pub(super) fn try_url_date_weakref_instance( // `let u = new URL(...); u.toString()` (typed local) if let ast::MemberProp::Ident(method_ident) = &member.prop { let method_name = method_ident.sym.as_ref(); + // `%Date.prototype%` is an ordinary object without [[DateValue]]. + // Do not feed direct calls on it into the statically-specialized + // DateCell path; the reflective prototype thunk performs the + // required brand check and throws TypeError. + let receiver_is_date_prototype = matches!( + member.obj.as_ref(), + ast::Expr::Member(proto_member) + if matches!(proto_member.obj.as_ref(), ast::Expr::Ident(id) if id.sym.as_ref() == "Date") + && matches!(&proto_member.prop, ast::MemberProp::Ident(id) if id.sym.as_ref() == "prototype") + ); + if receiver_is_date_prototype { + return Ok(Err(args)); + } if static_receiver_class(ctx, member.obj.as_ref()) == Some("URL") { match method_name { "toString" => { @@ -288,11 +301,9 @@ pub(super) fn try_url_date_weakref_instance( let date_expr = lower_expr(ctx, &member.obj)?; return Ok(Ok(Expr::DateGetUtcMilliseconds(Box::new(date_expr)))); } - // Other getters/methods - "valueOf" => { - let date_expr = lower_expr(ctx, &member.obj)?; - return Ok(Ok(Expr::DateValueOf(Box::new(date_expr)))); - } + // Other getters/methods. `valueOf` deliberately remains a + // generic property call: an own replacement on a Date must + // take precedence over Date.prototype.valueOf. "toDateString" => { let date_expr = lower_expr(ctx, &member.obj)?; return Ok(Ok(Expr::DateToDateString(Box::new(date_expr)))); diff --git a/crates/perry-hir/src/lower/expr_member/member_tail.rs b/crates/perry-hir/src/lower/expr_member/member_tail.rs index 13d69c6ddd..74e8d1370e 100644 --- a/crates/perry-hir/src/lower/expr_member/member_tail.rs +++ b/crates/perry-hir/src/lower/expr_member/member_tail.rs @@ -27,6 +27,33 @@ pub(crate) fn lower_member_tail( _ => lower_expr(ctx, &member.obj)?, }; if let ast::MemberProp::Ident(prop_ident) = &member.prop { + // A function produced by the dynamic GeneratorFunction constructors + // inherits the caller/arguments poison accessors from + // %Function.prototype%. Its compact runtime representation is not a + // normal registered closure, so preserve the statically-known brand + // here instead of letting generic PropertyGet return undefined. + if matches!(prop_ident.sym.as_ref(), "caller" | "arguments") { + let is_dynamic_generator = match member.obj.as_ref() { + ast::Expr::Ident(obj_ident) => matches!( + ctx.lookup_local_type(obj_ident.sym.as_ref()), + Some(Type::Named(name)) + if matches!(name.as_str(), "GeneratorFunction" | "AsyncGeneratorFunction") + ), + _ => false, + }; + if is_dynamic_generator { + return Ok(Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_throw_restricted_function_property_assignment".to_string(), + param_types: vec![], + return_type: Type::Any, + }), + args: vec![], + type_args: vec![], + byte_offset: member.span.lo.0, + }); + } + } if let Some(value) = ws_ready_state_value(prop_ident.sym.as_ref()) { if is_ws_ready_state_receiver(ctx, member.obj.as_ref(), &object_expr) { return Ok(Expr::Number(value)); @@ -215,6 +242,12 @@ pub(crate) fn lower_member_tail( }, ast::MemberProp::PrivateName(_) => None, }; + // Every constructor function inherits `.constructor` from + // Function.prototype. Collapsing `Error.constructor` (and + // the equivalent read on another built-in constructor) to + // `globalThis.constructor` loses the receiver and returns + // undefined instead of the Function constructor. + let outer_is_constructor_property = outer_static_member == Some("constructor"); // #4596 follow-up: `Array.isArray` / `Array.from` / // `Array.of` read as VALUES need the reified Array // constructor receiver so they resolve to the real native @@ -377,6 +410,7 @@ pub(crate) fn lower_member_tail( let receiver_is_regexp_ctor = property == "RegExp"; let receiver_is_function_ctor = property == "Function"; if !outer_is_prototype_or_proto + && !outer_is_constructor_property && !receiver_is_namespace_value && !receiver_is_regexp_ctor && !receiver_is_function_ctor diff --git a/crates/perry-hir/src/lower/expr_new.rs b/crates/perry-hir/src/lower/expr_new.rs index 4c9835e5d6..afed3542bc 100644 --- a/crates/perry-hir/src/lower/expr_new.rs +++ b/crates/perry-hir/src/lower/expr_new.rs @@ -225,6 +225,30 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R // Try to extract class name from callee match callee_expr { ast::Expr::Ident(ident) => { + // Hidden dynamic-function constructors reached through + // `.constructor` are pre-classified by + // `fn_ctor_env`. Their call form already const-folds; construction + // must use the same kind-aware path (`new GeneratorFunction()`, + // `new AsyncFunction(...)`, and async generators) instead of the + // generic object-construction fallback. + if ctx.local_decl_scope_depth(ident.sym.as_ref()) == Some(0) { + if let Some(super::fn_ctor_env::FnCtorShape::DynCtor(kind)) = + ctx.fn_ctor_env.entries.get(ident.sym.as_str()).cloned() + { + let args = new_expr.args.as_deref().unwrap_or(&[]); + if let Some(folded) = + super::const_fold_fn::try_const_fold_function_construct_kind( + ctx, + args, + crate::eval_classifier::EvalSurface::NewFunction, + new_expr.span, + kind, + )? + { + return Ok(folded); + } + } + } // The inner name of the class currently being lowered is a lexical // binding that wins over same-named OUTER locals. A nearer method // parameter/local still shadows it: `class C { static make(C) { @@ -914,8 +938,25 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R } } } - if matches!(class_name.as_str(), "Symbol" | "BigInt" | "Math" | "JSON") - && !shadowed_by_user_binding + if matches!( + class_name.as_str(), + "Symbol" + | "BigInt" + | "Math" + | "JSON" + | "Atomics" + | "Reflect" + | "global" + | "decodeURI" + | "decodeURIComponent" + | "encodeURI" + | "encodeURIComponent" + | "eval" + | "isFinite" + | "isNaN" + | "parseFloat" + | "parseInt" + ) && !shadowed_by_user_binding { let args = new_expr .args diff --git a/crates/perry-hir/src/lower/expr_new/helpers.rs b/crates/perry-hir/src/lower/expr_new/helpers.rs index a6d160d3a6..f09e5e454d 100644 --- a/crates/perry-hir/src/lower/expr_new/helpers.rs +++ b/crates/perry-hir/src/lower/expr_new/helpers.rs @@ -124,7 +124,7 @@ pub(crate) fn nonconstructable_builtin_throw_expr(name: &str, mut args: Vec "js_throw_bigint_constructor_type_error", "Math" => "js_throw_math_constructor_type_error", "JSON" => "js_throw_json_constructor_type_error", - _ => unreachable!(), + _ => "js_throw_not_a_constructor", }; let throw_expr = Expr::Call { callee: Box::new(Expr::ExternFuncRef { diff --git a/crates/perry-hir/src/lower/lower_expr/arm_unary.rs b/crates/perry-hir/src/lower/lower_expr/arm_unary.rs index d0af98105a..848592b200 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_unary.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_unary.rs @@ -523,6 +523,31 @@ pub(crate) fn lower_unary_expr(ctx: &mut LoweringContext, unary: &ast::UnaryExpr let prop_name = prop.sym.as_ref(); let is_global = ctx.lookup_local(obj_name).is_none() && ctx.lookup_func(obj_name).is_none(); + // Global helper `.length` reads normally fold to a numeric + // constant. Under `delete`, preserve the reference so the + // configurable synthesized closure slot is actually removed. + if is_global + && prop_name == "length" + && matches!( + obj_name, + "decodeURI" + | "decodeURIComponent" + | "encodeURI" + | "encodeURIComponent" + | "eval" + | "isFinite" + | "isNaN" + | "parseFloat" + | "parseInt" + ) + { + let object = lower_expr(ctx, member.obj.as_ref())?; + return Ok(Expr::Delete(Box::new(Expr::PropertyGet { + object: Box::new(object), + property: "length".to_string(), + byte_offset: member.span.lo.0, + }))); + } if is_global && obj_name == "Number" && matches!( diff --git a/crates/perry-hir/src/lower/lower_module_fn.rs b/crates/perry-hir/src/lower/lower_module_fn.rs index 10bdb7f8eb..fd076d4760 100644 --- a/crates/perry-hir/src/lower/lower_module_fn.rs +++ b/crates/perry-hir/src/lower/lower_module_fn.rs @@ -1225,6 +1225,35 @@ pub fn lower_module_full( crate::dynamic_import::detect_top_level_await(&mut module); let is_esm_entry = !module.imports.is_empty() || !module.exports.is_empty() || module.has_top_level_await; + if ctx.is_entry_module && !is_esm_entry && module.references_global_this { + let script_var_decl_ids: HashSet<_> = ctx + .script_var_decl_names + .iter() + .filter_map(|name| ctx.lookup_local(name)) + .collect(); + // Script `var` bindings are properties of the global object. Insert + // the mirror immediately after each top-level initializer so code + // later in the same script observes the initialized value through + // `globalThis` (ES modules keep their lexical/module binding only). + let mut reflected = Vec::with_capacity(module.init.len()); + for stmt in std::mem::take(&mut module.init) { + let global_var = match &stmt { + Stmt::Let { id, name, .. } if script_var_decl_ids.contains(id) => { + Some((*id, name.clone())) + } + _ => None, + }; + reflected.push(stmt); + if let Some((id, name)) = global_var { + reflected.push(Stmt::Expr(Expr::PropertySet { + object: Box::new(Expr::GlobalThisExpr), + property: name, + value: Box::new(Expr::LocalGet(id)), + })); + } + } + module.init = reflected; + } if ctx.is_entry_module && !is_esm_entry { const RESTRICTED_GLOBAL_NAMES: [&str; 3] = ["undefined", "NaN", "Infinity"]; let restricted_scan_stmts: Vec = ast_module diff --git a/crates/perry-hir/src/lower/stmt_loops.rs b/crates/perry-hir/src/lower/stmt_loops.rs index 5e0602a4b4..34c1ec31cc 100644 --- a/crates/perry-hir/src/lower/stmt_loops.rs +++ b/crates/perry-hir/src/lower/stmt_loops.rs @@ -941,6 +941,12 @@ pub(super) fn lower_stmt_for_of_inner( method: "iterator".to_string(), args: vec![], } + } else if for_of_stmt.is_await && is_generator_call && !callee_is_async_gen { + // A sync generator used by `for await` must be adapted through + // CreateAsyncFromSyncIterator. Awaiting only its raw `next()` + // result does not await `result.value`, and therefore neither + // preserves a rejected yielded promise nor closes the generator. + Expr::GetAsyncIterator(Box::new(iter_expr)) } else { iter_expr }; @@ -1042,8 +1048,7 @@ pub(super) fn lower_stmt_for_of_inner( }); } } - // Lower user body statements. lower_stmt appends to module.init, - // so we snapshot and drain to capture the body stmts. + // Lower user body statements. Snapshot module.init and drain body stmts. // Handle both Block bodies (`for (...) { ... }`) AND single-statement // bodies (`for (...) console.log(v);`). Pre-fix the brace-less // form was silently dropped — `for (const v of gen()) doThing(v);` @@ -1061,6 +1066,7 @@ pub(super) fn lower_stmt_for_of_inner( || is_filehandle_readlines_for_await || is_fs_dir_for_await || is_readline_interface_for_await + || (for_of_stmt.is_await && is_generator_call && !callee_is_async_gen) { insert_iterator_return_before_abrupts(&mut user_body, iter_id, needs_await); } diff --git a/crates/perry-hir/src/lower_decl/body_stmt.rs b/crates/perry-hir/src/lower_decl/body_stmt.rs index d4a1169071..93ae4f1764 100644 --- a/crates/perry-hir/src/lower_decl/body_stmt.rs +++ b/crates/perry-hir/src/lower_decl/body_stmt.rs @@ -1248,6 +1248,11 @@ pub fn lower_body_stmt(ctx: &mut LoweringContext, stmt: &ast::Stmt) -> Result Result *mut ArrayHeader { @@ -52,21 +56,36 @@ fn unbox_array_ptr(value: f64) -> *mut ArrayHeader { raw as *mut ArrayHeader } -unsafe fn alloc_iterator(arr_ptr: *mut ArrayHeader, kind: i32) -> f64 { - let obj = js_object_alloc(ARRAY_ITERATOR_CLASS_ID, 3); +unsafe fn alloc_iterator_backing(backing: f64, kind: i32) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + // The iterator allocation and the lazy prototype bootstrap can both + // collect. Keep the incoming backing and the new iterator relocatable. + let backing_h = scope.root_nanbox_f64(backing); + let obj_h = scope.root_raw_mut_ptr(js_object_alloc(ARRAY_ITERATOR_CLASS_ID, 3)); // Field 0: backing array (NaN-boxed pointer so the GC scanner keeps it). - let arr_nan = js_nanbox_pointer(arr_ptr as i64); - js_object_set_field(obj, 0, JSValue::from_bits(arr_nan.to_bits())); + obj_h.with_mut_ptr(|obj| { + js_object_set_field( + obj, + 0, + JSValue::from_bits(backing_h.get_nanbox_f64().to_bits()), + ) + }); // Field 1: cursor index, starts at 0. - js_object_set_field(obj, 1, JSValue::number(0.0)); + obj_h.with_mut_ptr(|obj| js_object_set_field(obj, 1, JSValue::number(0.0))); // Field 2: iterator kind. - js_object_set_field(obj, 2, JSValue::number(kind as f64)); + obj_h.with_mut_ptr(|obj| js_object_set_field(obj, 2, JSValue::number(kind as f64))); // Link `[[Prototype]]` to the shared `%ArrayIteratorPrototype%` singleton so // `Object.getPrototypeOf(it)` and the inherited `.next` read resolve. - crate::object::attach_iterator_prototype(obj, ARRAY_ITERATOR_CLASS_ID); + obj_h + .with_mut_ptr(|obj| crate::object::attach_iterator_prototype(obj, ARRAY_ITERATOR_CLASS_ID)); + let (_, obj) = obj_h.across_mut::(|| ()); js_nanbox_pointer(obj as i64) } +unsafe fn alloc_iterator(arr_ptr: *mut ArrayHeader, kind: i32) -> f64 { + alloc_iterator_backing(js_nanbox_pointer(arr_ptr as i64), kind) +} + /// `arr.values()` iterator — yields each element value. pub fn array_values_iter(arr_f64: f64) -> f64 { let arr_ptr = unbox_array_ptr(arr_f64); @@ -124,6 +143,16 @@ pub fn array_entries_iter(arr_f64: f64) -> f64 { unsafe { alloc_iterator(arr_ptr, KIND_ENTRIES) } } +/// `arguments[Symbol.iterator]()` — a live Array-style values iterator over an +/// Arguments exotic object. Snapshotting to an Array here loses the specified +/// expansion/truncation behavior before exhaustion. +pub fn arguments_values_iter(obj: *const ObjectHeader) -> f64 { + if obj.is_null() || !crate::object::is_arguments_object(obj) { + return f64::from_bits(TAG_UNDEFINED); + } + unsafe { alloc_iterator_backing(js_nanbox_pointer(obj as i64), KIND_ARGUMENTS_VALUES) } +} + // --------------------------------------------------------------------------- // #2384: C-ABI entry points for codegen's `Expr::ArrayValues`/`ArrayKeys`/ // `ArrayEntries` fast path. These build a real `.next()`-bearing iterator @@ -607,6 +636,11 @@ pub unsafe fn dispatch_array_iterator_method( }; match method_name { "next" => { + if let Some(result) = + crate::object::call_overridden_iterator_next(iter_obj(), ARRAY_ITERATOR_CLASS_ID) + { + return result; + } if kind == KIND_VALUES_NULL_DONE { let epoch_ptr = js_nanbox_get_pointer(f64::from_bits( js_object_get_field(iter_obj(), 3).bits(), @@ -632,15 +666,17 @@ pub unsafe fn dispatch_array_iterator_method( } return make_iter_result(done_value(), true); } - let arr_ptr = js_nanbox_get_pointer(backing_f64) as *const ArrayHeader; + let backing_ptr = js_nanbox_get_pointer(backing_f64); // Field 1: current index. let idx_field = js_object_get_field(iter_obj(), 1); let idx = f64::from_bits(idx_field.bits()) as u32; - let len = if arr_ptr.is_null() { - 0u32 + let len = if kind == KIND_ARGUMENTS_VALUES { + crate::object::arguments_object_length(backing_ptr as *const ObjectHeader) + } else if backing_ptr == 0 { + 0 } else { - crate::array::js_array_length(arr_ptr) + crate::array::js_array_length(backing_ptr as *const ArrayHeader) }; if idx >= len { @@ -662,17 +698,21 @@ pub unsafe fn dispatch_array_iterator_method( // field 0, which the collector DOES rewrite, instead of reusing // the pre-store copy. `iter_obj()` re-reads the iterator's own // address from its root for the same reason. - let arr_ptr = + let backing_ptr = js_nanbox_get_pointer(f64::from_bits(js_object_get_field(iter_obj(), 0).bits())) - as *const ArrayHeader; - let elem = if arr_ptr.is_null() { + as usize; + let elem = if kind == KIND_ARGUMENTS_VALUES { + crate::object::arguments_object_index_value(backing_ptr as *const ObjectHeader, idx) + } else if backing_ptr == 0 { f64::from_bits(TAG_UNDEFINED) } else { - crate::array::js_array_get_f64(arr_ptr, idx) + crate::array::js_array_get_f64(backing_ptr as *const ArrayHeader, idx) }; let value = match kind { - KIND_VALUES | KIND_VALUES_NULL_DONE => JSValue::from_bits(elem.to_bits()), + KIND_VALUES | KIND_VALUES_NULL_DONE | KIND_ARGUMENTS_VALUES => { + JSValue::from_bits(elem.to_bits()) + } KIND_KEYS => JSValue::number(idx as f64), KIND_ENTRIES => { let pair = make_pair_array(idx, elem); diff --git a/crates/perry-runtime/src/array/iterator.rs b/crates/perry-runtime/src/array/iterator.rs index 6b4c51b099..296e9d883c 100644 --- a/crates/perry-runtime/src/array/iterator.rs +++ b/crates/perry-runtime/src/array/iterator.rs @@ -348,7 +348,16 @@ fn async_from_sync_continue(iter: f64, step_result: f64, close_on_rejection: boo if close_on_rejection { 1.0 } else { 0.0 }, ); - let value_promise = crate::promise::js_promise_resolved(value); + let value_promise = match crate::promise::js_promise_resolved_catching(value) { + Ok(promise) => promise, + Err(reason) => { + if close_on_rejection { + async_from_sync_close(iter); + } + crate::promise::js_promise_reject(outer, reason); + return boxed_promise_value(outer); + } + }; crate::promise::js_promise_then(value_promise, on_fulfilled, on_rejected); boxed_promise_value(outer) } diff --git a/crates/perry-runtime/src/array/mod.rs b/crates/perry-runtime/src/array/mod.rs index 0decd10c9a..39004ced86 100644 --- a/crates/perry-runtime/src/array/mod.rs +++ b/crates/perry-runtime/src/array/mod.rs @@ -140,9 +140,9 @@ pub use self::iter_methods::{ js_validate_array_callback, js_validate_array_map_callback, }; pub use self::iter_object::{ - array_entries_iter, array_keys_iter, array_values_iter, array_values_iter_null_done, - dispatch_array_iterator_method, js_array_entries_iter_obj, js_array_keys_iter_obj, - js_array_values_iter_obj, ARRAY_ITERATOR_CLASS_ID, + arguments_values_iter, array_entries_iter, array_keys_iter, array_values_iter, + array_values_iter_null_done, dispatch_array_iterator_method, js_array_entries_iter_obj, + js_array_keys_iter_obj, js_array_values_iter_obj, ARRAY_ITERATOR_CLASS_ID, }; pub(crate) use self::iterator::is_builtin_iterator_class_id; pub(crate) use self::iterator::iter_bt_dump; diff --git a/crates/perry-runtime/src/buffer/dataview.rs b/crates/perry-runtime/src/buffer/dataview.rs index 46241ce3d6..71727b2d24 100644 --- a/crates/perry-runtime/src/buffer/dataview.rs +++ b/crates/perry-runtime/src/buffer/dataview.rs @@ -286,7 +286,8 @@ pub fn js_data_view_set( kind: DataViewKind, little: bool, ) -> f64 { - let buf = unbox_buffer_ptr(buf_f64.to_bits()) as *mut BufferHeader; + let scope = crate::gc::RuntimeHandleScope::new(); + let buf_handle = scope.root_nanbox_f64(buf_f64); let offset = to_byte_offset(offset_value); if kind.is_bigint() { // SetViewValue for a BigInt accessor: `ToBigInt(value)` (a Number throws @@ -298,10 +299,12 @@ pub fn js_data_view_set( } else { raw.to_be_bytes() }; + let buf = unbox_buffer_ptr(buf_handle.get_nanbox_f64().to_bits()) as *mut BufferHeader; unsafe { write_bytes(buf, offset, &b) }; return f64::from_bits(crate::value::TAG_UNDEFINED); } let n = to_number(value); + let buf = unbox_buffer_ptr(buf_handle.get_nanbox_f64().to_bits()) as *mut BufferHeader; unsafe { match kind { DataViewKind::BigInt64 | DataViewKind::BigUint64 => unreachable!(), @@ -486,46 +489,15 @@ fn wrap_to_u64(n: f64, bits: u32) -> u64 { /// convertible and throw a `TypeError`; BigInt passes through, Boolean and /// String coerce. fn to_bigint_raw_or_throw(value: f64) -> u64 { - use crate::value::JSValue; - let jsval = JSValue::from_bits(value.to_bits()); - let bi: *const crate::bigint::BigIntHeader = if jsval.is_bigint() { - jsval.as_bigint_ptr() as *const crate::bigint::BigIntHeader - } else if jsval.is_bool() { - crate::bigint::js_bigint_from_i64(if jsval.as_bool() { 1 } else { 0 }) - } else if jsval.is_any_string() { - // StringToBigInt (a malformed numeric string throws SyntaxError). - crate::bigint::js_bigint_from_f64(value) - } else { - throw_bigint_conversion_type_error(value); - }; + // Reuse the typed-array lane's complete ToBigInt implementation. In + // particular, object values must run @@toPrimitive/valueOf/toString and + // propagate abrupt completion before the DataView bounds check. + let coerced = crate::typedarray::bigint::to_bigint_for_store(value); + let bi = crate::value::JSValue::from_bits(coerced.to_bits()).as_bigint_ptr() + as *const crate::bigint::BigIntHeader; let bi = crate::bigint::clean_bigint_ptr(bi); if bi.is_null() { return 0; } unsafe { (*bi).limbs[0] } } - -/// Throw `TypeError: Cannot convert to a BigInt`, matching Node's -/// `ToBigInt` rejection text for a DataView BigInt setter. -#[cold] -fn throw_bigint_conversion_type_error(value: f64) -> ! { - use crate::value::JSValue; - let jsval = JSValue::from_bits(value.to_bits()); - let label = if jsval.is_undefined() { - "undefined".to_string() - } else if jsval.is_null() { - "null".to_string() - } else if unsafe { crate::symbol::js_is_symbol(value) } != 0 { - "a Symbol value".to_string() - } else if jsval.is_int32() { - jsval.as_int32().to_string() - } else { - format!("{value}") - }; - let msg = format!("Cannot convert {label} to a BigInt"); - let err = crate::error::js_typeerror_new(crate::string::js_string_from_bytes( - msg.as_ptr(), - msg.len() as u32, - )); - crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) -} diff --git a/crates/perry-runtime/src/buffer/from.rs b/crates/perry-runtime/src/buffer/from.rs index 21fff8c32d..7ae68a0981 100644 --- a/crates/perry-runtime/src/buffer/from.rs +++ b/crates/perry-runtime/src/buffer/from.rs @@ -898,7 +898,7 @@ pub extern "C" fn js_data_view_new(value: f64, offset_value: f64, length_value: throw_dataview_buffer_not_object(); } let addr = v.as_pointer::() as usize; - if addr == 0 || !is_registered_buffer(addr) { + if addr == 0 || !is_registered_buffer(addr) || !is_any_array_buffer(addr) { throw_dataview_buffer_not_object(); } // Steps 4-6: offset = ToIndex(byteOffset) (RangeError if negative). Runs diff --git a/crates/perry-runtime/src/buffer/mod.rs b/crates/perry-runtime/src/buffer/mod.rs index 6d67e40250..4b3076ae1e 100644 --- a/crates/perry-runtime/src/buffer/mod.rs +++ b/crates/perry-runtime/src/buffer/mod.rs @@ -71,8 +71,9 @@ pub(crate) use detach::{array_buffer_transfer, detach_array_buffer}; #[cfg(test)] pub(crate) use own_props::test_buffer_own_props_owner_count; pub use own_props::{ - buffer_get_own_prop, buffer_has_own_prop, buffer_own_prop_names, buffer_own_props_possible, - buffer_set_own_prop, clear_buffer_own_props, scan_buffer_own_props_roots_mut, + buffer_define_own_data_prop, buffer_delete_own_prop, buffer_get_own_prop, buffer_has_own_prop, + buffer_own_prop_names, buffer_own_props_possible, buffer_set_own_prop, clear_buffer_own_props, + scan_buffer_own_props_roots_mut, }; // ---- Re-exports: #8149 integer-indexed-exotic discrimination ---- diff --git a/crates/perry-runtime/src/buffer/own_props.rs b/crates/perry-runtime/src/buffer/own_props.rs index 703f16208e..bf04295c8e 100644 --- a/crates/perry-runtime/src/buffer/own_props.rs +++ b/crates/perry-runtime/src/buffer/own_props.rs @@ -52,6 +52,32 @@ pub fn buffer_own_props_possible() -> bool { /// Store `buf. = value`. Only reached for a registered buffer address. pub fn buffer_set_own_prop(addr: usize, prop: &str, value: f64) { + if addr == 0 { + return; + } + if let Some(accessor) = crate::object::get_accessor_descriptor(addr, prop) { + if accessor.set != 0 { + unsafe { + crate::object::invoke_accessor_setter( + accessor.set, + crate::value::js_nanbox_pointer(addr as i64), + value, + ); + } + } + return; + } + if crate::object::get_property_attrs(addr, prop).is_some_and(|attrs| !attrs.writable()) + && buffer_get_own_prop(addr, prop).is_some() + { + return; + } + buffer_define_own_data_prop(addr, prop, value); +} + +/// Descriptor installation bypasses ordinary [[Set]] interception after it +/// has validated the redefinition and selected the new property kind. +pub fn buffer_define_own_data_prop(addr: usize, prop: &str, value: f64) { if addr == 0 { return; } @@ -104,6 +130,28 @@ pub fn buffer_own_prop_names(addr: usize) -> Vec { /// Whether the buffer carries any own dynamic prop under `prop`. pub fn buffer_has_own_prop(addr: usize, prop: &str) -> bool { buffer_get_own_prop(addr, prop).is_some() + || crate::object::get_accessor_descriptor(addr, prop).is_some() +} + +/// Delete an ordinary named own property from a registered buffer/view. +/// Returns whether the property was present. +pub fn buffer_delete_own_prop(addr: usize, prop: &str) -> bool { + if addr == 0 || !buffer_own_props_possible() { + return false; + } + let Ok(mut props) = buffer_props().lock() else { + return false; + }; + let Some(entries) = props.get_mut(&addr) else { + return false; + }; + let removed = entries.remove(prop).is_some(); + crate::object::clear_accessor_descriptor(addr, prop); + crate::object::clear_property_attrs(addr, prop); + if entries.is_empty() { + props.remove(&addr); + } + removed } /// GC: trace stored values in every phase (a stored closure is reachable ONLY diff --git a/crates/perry-runtime/src/builtins/arithmetic.rs b/crates/perry-runtime/src/builtins/arithmetic.rs index 3f11e3d02b..810ce20e5b 100644 --- a/crates/perry-runtime/src/builtins/arithmetic.rs +++ b/crates/perry-runtime/src/builtins/arithmetic.rs @@ -111,15 +111,6 @@ pub extern "C" fn js_loose_eq(a: JSValue, b: JSValue) -> JSValue { if eq_is_object(a) && eq_is_object(b) { return JSValue::bool(false); } - // Boxed primitives compare via their wrapped primitive value under - // abstract equality (`new Number(5) == 5`, and sloppy primitive accessors - // return boxed receivers). - if let Some((_, payload)) = boxed_primitive_payload(f64::from_bits(a.bits())) { - return js_loose_eq(JSValue::from_bits(payload.to_bits()), b); - } - if let Some((_, payload)) = boxed_primitive_payload(f64::from_bits(b.bits())) { - return js_loose_eq(a, JSValue::from_bits(payload.to_bits())); - } // Object == primitive → ToPrimitive(object), then retry (ES2024 §7.2.15 // steps 10-11). Object-vs-object was settled above; symbols are primitives // (`eq_is_object` excludes them) and correctly fall through to not-equal. @@ -150,8 +141,8 @@ pub extern "C" fn js_loose_eq(a: JSValue, b: JSValue) -> JSValue { ); } // BigInt abstract equality (ES2024 §7.2.15). Neither side is - // null/undefined here and boxed wrappers (incl. `Object(0n)`) have already - // been unwrapped above. + // null/undefined here and boxed wrappers have already gone through the + // observable ToPrimitive operation above. if a.is_bigint() || b.is_bigint() { // BigInt == BigInt → compare by mathematical value. if a.is_bigint() && b.is_bigint() { diff --git a/crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs b/crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs index c62f976c1c..5b760d0ea7 100644 --- a/crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs +++ b/crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs @@ -374,9 +374,16 @@ pub extern "C" fn js_boxed_bigint_new(value: f64) -> f64 { #[no_mangle] pub extern "C" fn js_boxed_symbol_new(value: f64) -> f64 { - let obj = crate::object::js_object_alloc(CLASS_ID_BOXED_SYMBOL, 0); - register_boxed_primitive_payload(obj, value); - attach_boxed_primitive_prototype(obj, CLASS_ID_BOXED_SYMBOL); + let scope = crate::gc::RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(value); + let obj = scope.root_raw_mut_ptr(crate::object::js_object_alloc(CLASS_ID_BOXED_SYMBOL, 0)); + obj.with_mut_ptr::(|obj| { + register_boxed_primitive_payload(obj, value.get_nanbox_f64()) + }); + obj.with_mut_ptr::(|obj| { + attach_boxed_primitive_prototype(obj, CLASS_ID_BOXED_SYMBOL) + }); + let (_, obj) = obj.across_mut::(|| ()); crate::value::js_nanbox_pointer(obj as i64) } diff --git a/crates/perry-runtime/src/builtins/numbers.rs b/crates/perry-runtime/src/builtins/numbers.rs index 412f4cc49d..d5678dc6f8 100644 --- a/crates/perry-runtime/src/builtins/numbers.rs +++ b/crates/perry-runtime/src/builtins/numbers.rs @@ -21,7 +21,15 @@ pub extern "C" fn js_parse_int(str_ptr: *const StringHeader, radix: f64) -> f64 let data = (str_ptr as *const u8).add(std::mem::size_of::()); let bytes = std::slice::from_raw_parts(data, len); - if let Ok(s) = std::str::from_utf8(bytes) { + // Perry stores lone UTF-16 surrogates as WTF-8. `parseInt` only + // inspects the leading digit run, so an invalid UTF-8 sequence after + // that run is a terminator, not a reason to reject the whole string. + // Decode the valid prefix in that case (`"1Z\uD800"`, radix 36, is + // still 71), matching the spec's longest-prefix rule. + let valid_len = std::str::from_utf8(bytes) + .map(|_| bytes.len()) + .unwrap_or_else(|err| err.valid_up_to()); + if let Ok(s) = std::str::from_utf8(&bytes[..valid_len]) { // StrWhiteSpace per spec (NBSP/BOM in, NEL out) — not Rust's // `trim_start`, whose White_Space set diverges from JS's. let trimmed = s.trim_start_matches(crate::string::is_js_whitespace); diff --git a/crates/perry-runtime/src/collection_iter_object.rs b/crates/perry-runtime/src/collection_iter_object.rs index a90e58489a..839513b8cc 100644 --- a/crates/perry-runtime/src/collection_iter_object.rs +++ b/crates/perry-runtime/src/collection_iter_object.rs @@ -17,6 +17,7 @@ //! next to the array iterator one; `flat_clone.rs` detects the class id so //! `[...m.entries()]` / `Array.from(s.values())` drive `.next()`. +use crate::array::ArrayHeader; use crate::map::MapHeader; use crate::object::{js_object_alloc, js_object_get_field, js_object_set_field, ObjectHeader}; use crate::set::SetHeader; @@ -58,26 +59,33 @@ fn iterator_class_id(addr: usize) -> Option { } unsafe fn alloc_iterator(class_id: u32, coll_nanboxed: f64, kind: i32) -> f64 { - let obj = js_object_alloc(class_id, 5); + let scope = crate::gc::RuntimeHandleScope::new(); + let coll_h = scope.root_nanbox_f64(coll_nanboxed); + let obj_h = scope.root_raw_mut_ptr(js_object_alloc(class_id, 5)); + let obj = || obj_h.across_mut::(|| ()).1; // Field 0: backing collection (NaN-boxed pointer so the GC scanner keeps it). - js_object_set_field(obj, 0, JSValue::from_bits(coll_nanboxed.to_bits())); + js_object_set_field( + obj(), + 0, + JSValue::from_bits(coll_h.get_nanbox_f64().to_bits()), + ); // Field 1: cursor index (index just past the last-returned entry), starts at 0. - js_object_set_field(obj, 1, JSValue::number(0.0)); + js_object_set_field(obj(), 1, JSValue::number(0.0)); // Field 2: iterator kind. - js_object_set_field(obj, 2, JSValue::number(kind as f64)); + js_object_set_field(obj(), 2, JSValue::number(kind as f64)); // Field 3: collection size observed at the last `next()`. `-1` sentinel means // "not started" (no entry returned yet). Used to detect a mid-iteration // delete (which compacts the entries array, shifting live entries below the // cursor) so the cursor can be re-derived from the last key (#6075). - js_object_set_field(obj, 3, JSValue::number(-1.0)); + js_object_set_field(obj(), 3, JSValue::number(-1.0)); // Field 4: the KEY of the last-returned entry (a Map key / Set value), used // to re-derive the cursor after a delete-shift. Undefined until started. - js_object_set_field(obj, 4, JSValue::undefined()); + js_object_set_field(obj(), 4, JSValue::undefined()); // Link `[[Prototype]]` to the shared `%MapIteratorPrototype%` / // `%SetIteratorPrototype%` singleton so `Object.getPrototypeOf(it)` and the // inherited `.next` read resolve. - crate::object::attach_iterator_prototype(obj, class_id); - js_nanbox_pointer(obj as i64) + crate::object::attach_iterator_prototype(obj(), class_id); + js_nanbox_pointer(obj() as i64) } /// Build a fresh Map iterator object for `map` (raw pointer) of the given @@ -178,11 +186,17 @@ use crate::iter_result::make_iter_result; /// `[key, value]` pair array for Map entries / Set entries (`[v, v]`). unsafe fn make_pair_array(a: f64, b: f64) -> f64 { - let pair = crate::array::js_array_alloc(2); - crate::array::store_array_slot(pair, 0, a.to_bits()); - crate::array::store_array_slot(pair, 1, b.to_bits()); - (*pair).length = 2; - crate::array::rebuild_array_layout_exact(pair); + let scope = crate::gc::RuntimeHandleScope::new(); + let a = scope.root_nanbox_f64(a); + let b = scope.root_nanbox_f64(b); + let pair = scope.root_raw_mut_ptr(crate::array::js_array_alloc(2)); + pair.with_mut_ptr::(|pair| { + crate::array::store_array_slot(pair, 0, a.get_nanbox_u64()); + crate::array::store_array_slot(pair, 1, b.get_nanbox_u64()); + (*pair).length = 2; + crate::array::rebuild_array_layout_exact(pair); + }); + let (_, pair) = pair.across_mut::(|| ()); js_nanbox_pointer(pair as i64) } @@ -219,50 +233,62 @@ fn next_read_index(cursor: u32, last_key_in_place: bool, find_last: impl FnOnce( /// Dispatch `.next()` / `[Symbol.iterator]()` on a Map iterator object. pub unsafe fn dispatch_map_iterator_method(iter_obj: *mut ObjectHeader, method_name: &str) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let iter_h = scope.root_nanbox_f64(js_nanbox_pointer(iter_obj as i64)); + let iter_obj = || js_nanbox_get_pointer(iter_h.get_nanbox_f64()) as *mut ObjectHeader; match method_name { "next" => { - let backing = f64::from_bits(js_object_get_field(iter_obj, 0).bits()); - let map = js_nanbox_get_pointer(backing) as *const MapHeader; - let kind = f64::from_bits(js_object_get_field(iter_obj, 2).bits()) as i32; - if map.is_null() { + if let Some(result) = + crate::object::call_overridden_iterator_next(iter_obj(), MAP_ITERATOR_CLASS_ID) + { + return result; + } + let backing = f64::from_bits(js_object_get_field(iter_obj(), 0).bits()); + let map_h = scope.root_nanbox_f64(backing); + let map = || js_nanbox_get_pointer(map_h.get_nanbox_f64()) as *const MapHeader; + let kind = f64::from_bits(js_object_get_field(iter_obj(), 2).bits()) as i32; + if map().is_null() { return make_iter_result(JSValue::undefined(), true); } - let cursor = f64::from_bits(js_object_get_field(iter_obj, 1).bits()) as u32; - let last_key = js_object_get_field(iter_obj, 4); - let size = crate::map::js_map_size(map); + let cursor = f64::from_bits(js_object_get_field(iter_obj(), 1).bits()) as u32; + let last_key = js_object_get_field(iter_obj(), 4); + let size = crate::map::js_map_size(map()); // Is the last-returned key still at cursor-1? (SameValueZero, so a // NaN key matches itself.) If so, no delete shifted an entry at/below // the cursor. let in_place = cursor > 0 && { - let prev = crate::map::js_map_entry_key_at(map, cursor - 1); + let prev = crate::map::js_map_entry_key_at(map(), cursor - 1); crate::value::js_jsvalue_same_value_zero(prev, f64::from_bits(last_key.bits())) != 0 }; let idx = next_read_index(cursor, in_place, || { - crate::map::find_key_index(map, f64::from_bits(last_key.bits())) + crate::map::find_key_index(map(), f64::from_bits(last_key.bits())) }); if idx >= size { - js_object_set_field(iter_obj, 1, JSValue::number(size as f64)); + js_object_set_field(iter_obj(), 1, JSValue::number(size as f64)); + // Once a collection iterator is exhausted it stays exhausted, + // even if entries are appended later. + js_object_set_field(iter_obj(), 0, JSValue::undefined()); return make_iter_result(JSValue::undefined(), true); } - let entry_key = crate::map::js_map_entry_key_at(map, idx); + let entry_key = crate::map::js_map_entry_key_at(map(), idx); // Record state for the next re-derive BEFORE any allocation below. - js_object_set_field(iter_obj, 1, JSValue::number((idx + 1) as f64)); - js_object_set_field(iter_obj, 4, JSValue::from_bits(entry_key.to_bits())); + js_object_set_field(iter_obj(), 1, JSValue::number((idx + 1) as f64)); + js_object_set_field(iter_obj(), 4, JSValue::from_bits(entry_key.to_bits())); let value = match kind { KIND_KEYS => JSValue::from_bits(entry_key.to_bits()), KIND_VALUES => { - JSValue::from_bits(crate::map::js_map_entry_value_at(map, idx).to_bits()) + JSValue::from_bits(crate::map::js_map_entry_value_at(map(), idx).to_bits()) } _ => { - let val = crate::map::js_map_entry_value_at(map, idx); + let val = crate::map::js_map_entry_value_at(map(), idx); JSValue::from_bits(make_pair_array(entry_key, val).to_bits()) } }; make_iter_result(value, false) } - "Symbol.iterator" | "@@iterator" => js_nanbox_pointer(iter_obj as i64), + "Symbol.iterator" | "@@iterator" => js_nanbox_pointer(iter_obj() as i64), "return" | "throw" => make_iter_result(JSValue::undefined(), true), _ => f64::from_bits(TAG_UNDEFINED), } @@ -270,32 +296,42 @@ pub unsafe fn dispatch_map_iterator_method(iter_obj: *mut ObjectHeader, method_n /// Dispatch `.next()` / `[Symbol.iterator]()` on a Set iterator object. pub unsafe fn dispatch_set_iterator_method(iter_obj: *mut ObjectHeader, method_name: &str) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let iter_h = scope.root_nanbox_f64(js_nanbox_pointer(iter_obj as i64)); + let iter_obj = || js_nanbox_get_pointer(iter_h.get_nanbox_f64()) as *mut ObjectHeader; match method_name { "next" => { - let backing = f64::from_bits(js_object_get_field(iter_obj, 0).bits()); - let set = js_nanbox_get_pointer(backing) as *const SetHeader; - let kind = f64::from_bits(js_object_get_field(iter_obj, 2).bits()) as i32; - if set.is_null() { + if let Some(result) = + crate::object::call_overridden_iterator_next(iter_obj(), SET_ITERATOR_CLASS_ID) + { + return result; + } + let backing = f64::from_bits(js_object_get_field(iter_obj(), 0).bits()); + let set_h = scope.root_nanbox_f64(backing); + let set = || js_nanbox_get_pointer(set_h.get_nanbox_f64()) as *const SetHeader; + let kind = f64::from_bits(js_object_get_field(iter_obj(), 2).bits()) as i32; + if set().is_null() { return make_iter_result(JSValue::undefined(), true); } - let cursor = f64::from_bits(js_object_get_field(iter_obj, 1).bits()) as u32; - let last_val = js_object_get_field(iter_obj, 4); - let size = crate::set::js_set_size(set); + let cursor = f64::from_bits(js_object_get_field(iter_obj(), 1).bits()) as u32; + let last_val = js_object_get_field(iter_obj(), 4); + let size = crate::set::js_set_size(set()); let in_place = cursor > 0 && { - let prev = crate::set::js_set_value_at(set, cursor - 1); + let prev = crate::set::js_set_value_at(set(), cursor - 1); crate::value::js_jsvalue_same_value_zero(prev, f64::from_bits(last_val.bits())) != 0 }; let idx = next_read_index(cursor, in_place, || { - crate::set::find_value_index(set, f64::from_bits(last_val.bits())) + crate::set::find_value_index(set(), f64::from_bits(last_val.bits())) }); if idx >= size { - js_object_set_field(iter_obj, 1, JSValue::number(size as f64)); + js_object_set_field(iter_obj(), 1, JSValue::number(size as f64)); + js_object_set_field(iter_obj(), 0, JSValue::undefined()); return make_iter_result(JSValue::undefined(), true); } - let elem = crate::set::js_set_value_at(set, idx); - js_object_set_field(iter_obj, 1, JSValue::number((idx + 1) as f64)); - js_object_set_field(iter_obj, 4, JSValue::from_bits(elem.to_bits())); + let elem = crate::set::js_set_value_at(set(), idx); + js_object_set_field(iter_obj(), 1, JSValue::number((idx + 1) as f64)); + js_object_set_field(iter_obj(), 4, JSValue::from_bits(elem.to_bits())); let value = match kind { // For Sets, keys === values; entries yields [v, v] pairs. @@ -304,7 +340,7 @@ pub unsafe fn dispatch_set_iterator_method(iter_obj: *mut ObjectHeader, method_n }; make_iter_result(value, false) } - "Symbol.iterator" | "@@iterator" => js_nanbox_pointer(iter_obj as i64), + "Symbol.iterator" | "@@iterator" => js_nanbox_pointer(iter_obj() as i64), "return" | "throw" => make_iter_result(JSValue::undefined(), true), _ => f64::from_bits(TAG_UNDEFINED), } diff --git a/crates/perry-runtime/src/error.rs b/crates/perry-runtime/src/error.rs index 516e691a8d..4550d0cc3c 100644 --- a/crates/perry-runtime/src/error.rs +++ b/crates/perry-runtime/src/error.rs @@ -822,6 +822,20 @@ pub(crate) unsafe fn js_error_has_own_property(error: *mut ErrorHeader, key: &st } } +/// Clear an Error instance's spec-visible own-slot presence after a +/// successful configurable-property deletion. +pub(crate) unsafe fn js_error_delete_builtin_own_property(error: *mut ErrorHeader, key: &str) { + if error.is_null() { + return; + } + match key { + "message" => (*error).flags &= !ERROR_FLAG_HAS_MESSAGE, + "cause" => (*error).flags &= !ERROR_FLAG_HAS_CAUSE, + "errors" => (*error).flags &= !ERROR_FLAG_HAS_ERRORS, + _ => {} + } +} + pub(crate) unsafe fn js_error_builtin_own_property_is_enumerable( error: *mut ErrorHeader, key: &str, diff --git a/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs b/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs index 258250bfa2..a2e82441d8 100644 --- a/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs +++ b/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs @@ -253,7 +253,7 @@ fn realm_owned_intrinsic_module_and_storage_roots_are_distinct() { let a = a.join().expect("agent A panicked"); let b = b.join().expect("agent B panicked"); - assert_eq!(a.len(), 23, "the gate must cover every #8002/#8003 root"); + assert_eq!(a.len(), 25, "the gate must cover every #8002/#8003 root"); assert_eq!(a.len(), b.len()); for ((a_name, a_slot, a_root), (b_name, b_slot, b_root)) in a.iter().zip(&b) { assert_eq!(a_name, b_name, "snapshot wiring diverged between agents"); diff --git a/crates/perry-runtime/src/map.rs b/crates/perry-runtime/src/map.rs index 856915bf0a..d49184a77f 100644 --- a/crates/perry-runtime/src/map.rs +++ b/crates/perry-runtime/src/map.rs @@ -2278,11 +2278,25 @@ pub extern "C" fn js_map_from_array(arr: *const crate::array::ArrayHeader) -> *m pub extern "C" fn js_map_from_iterable(value: f64) -> *mut MapHeader { use crate::collection_iter::{constructor_iter, ConstructorIter}; + // The constructor returns before Get(map, "set") for a nullish iterable. + // In particular, a throwing accessor installed on Map.prototype.set must + // not affect `new Map()` / `new Map(null)`. + if crate::collection_iter::is_null_or_undefined(value) { + return js_map_alloc(4); + } + let scope = crate::gc::RuntimeHandleScope::new(); let value_handle = scope.root_nanbox_f64(value); + let map_handle = scope.root_raw_mut_ptr(js_map_alloc(4)); let adder = crate::collection_iter::require_callable( - crate::collection_iter::builtin_prototype_method("Map", "set"), + map_handle.with_mut_ptr::(|map| { + crate::collection_iter::builtin_prototype_adder( + "Map", + "set", + crate::value::js_nanbox_pointer(map as i64), + ) + }), "Map.prototype.set", ); let adder = crate::collection_iter::normalize_callable_value(adder); @@ -2325,11 +2339,9 @@ pub extern "C" fn js_map_from_iterable(value: f64) -> *mut MapHeader { } match constructor_iter(value_handle.get_nanbox_f64()) { - ConstructorIter::Empty => js_map_alloc(4), + ConstructorIter::Empty => map_handle.across_mut::(|| ()).1, ConstructorIter::Array(arr_value) => { let arr_handle = scope.root_nanbox_f64(arr_value); - let map = js_map_alloc(4); - let map_handle = scope.root_raw_mut_ptr(map); let arr_ptr = crate::value::js_nanbox_get_pointer(arr_handle.get_nanbox_f64()) as *mut crate::array::ArrayHeader; if !arr_ptr.is_null() { @@ -2351,8 +2363,6 @@ pub extern "C" fn js_map_from_iterable(value: f64) -> *mut MapHeader { } ConstructorIter::Iterator(iter) => { let iter_handle = scope.root_nanbox_f64(iter); - let map = js_map_alloc(4); - let map_handle = scope.root_raw_mut_ptr(map); loop { let iter = iter_handle.get_nanbox_f64(); let next = crate::collection_iter::iterator_next_value(iter); diff --git a/crates/perry-runtime/src/object/arguments.rs b/crates/perry-runtime/src/object/arguments.rs index d5b90ce334..58f716b8e6 100644 --- a/crates/perry-runtime/src/object/arguments.rs +++ b/crates/perry-runtime/src/object/arguments.rs @@ -138,6 +138,23 @@ extern "C" fn arguments_throw_type_error(_closure: *const crate::closure::Closur fn thrower_closure_value() -> f64 { let closure = crate::closure::js_closure_alloc_singleton(arguments_throw_type_error as *const u8); + crate::closure::js_register_closure_arity(arguments_throw_type_error as *const u8, 0); + super::native_module::set_bound_native_closure_name(closure, ""); + super::native_module::set_builtin_closure_length(closure as usize, 0); + super::native_module::set_builtin_closure_non_constructable(closure as usize); + let frozen = PropertyAttrs::new(false, false, false); + set_property_attrs(closure as usize, "name".to_string(), frozen); + set_property_attrs(closure as usize, "length".to_string(), frozen); + if !closure.is_null() { + unsafe { + if let Some(gc) = crate::value::addr_class::try_read_tracked_gc_header(closure as usize) + { + (*gc.as_ptr())._reserved |= crate::gc::OBJ_FLAG_FROZEN + | crate::gc::OBJ_FLAG_SEALED + | crate::gc::OBJ_FLAG_NO_EXTEND; + } + } + } crate::value::js_nanbox_pointer(closure as i64) } @@ -559,6 +576,39 @@ pub(crate) unsafe fn arguments_object_to_vec(obj: *const ObjectHeader) -> Option Some(out) } +/// Live `length` read used by the Arguments Array iterator. +pub(crate) unsafe fn arguments_object_length(obj: *const ObjectHeader) -> u32 { + if !is_arguments_object(obj) { + return 0; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_h = scope.root_raw_const_ptr(obj); + let key = intern_key("length"); + let value = obj_h + .with_const_ptr(|obj| arguments_object_get_field(obj, key)) + .map(|v| f64::from_bits(v.bits())) + .unwrap_or(0.0); + if value.is_finite() && value > 0.0 { + value.floor().min(u32::MAX as f64) as u32 + } else { + 0 + } +} + +/// Live indexed read used by the Arguments Array iterator. +pub(crate) unsafe fn arguments_object_index_value(obj: *const ObjectHeader, index: u32) -> f64 { + if !is_arguments_object(obj) { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_h = scope.root_raw_const_ptr(obj); + let key = intern_key(&index.to_string()); + obj_h + .with_const_ptr(|obj| arguments_object_get_field(obj, key)) + .map(|v| f64::from_bits(v.bits())) + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)) +} + pub(crate) unsafe fn arguments_object_to_array( obj: *const ObjectHeader, ) -> Option<*mut ArrayHeader> { diff --git a/crates/perry-runtime/src/object/async_generator_queue.rs b/crates/perry-runtime/src/object/async_generator_queue.rs index dcccaba260..16c8b30a67 100644 --- a/crates/perry-runtime/src/object/async_generator_queue.rs +++ b/crates/perry-runtime/src/object/async_generator_queue.rs @@ -41,6 +41,9 @@ struct AsyncGeneratorRequest { struct AsyncGeneratorQueueState { active: bool, drain_scheduled: bool, + started: bool, + completed: bool, + original_throw: *const ClosureHeader, queue: VecDeque, } @@ -91,22 +94,27 @@ pub(crate) fn wrap_async_generator_instance(obj: *mut ObjectHeader) { let next_h = scope.root_nanbox_f64(js_nanbox_pointer(next as i64)); let ret_h = scope.root_nanbox_f64(js_nanbox_pointer(ret as i64)); let throw_h = scope.root_nanbox_f64(js_nanbox_pointer(throw as i64)); + let closure_now = |h: &crate::gc::RuntimeHandle<'_>| { + js_nanbox_get_pointer(h.get_nanbox_f64()) as *const ClosureHeader + }; let state_id = STATES.with(|states| { let mut states = states.borrow_mut(); let id = states.len() + 1; + let original_throw = closure_now(&throw_h); + crate::gc::runtime_write_barrier_root_raw_ptr(original_throw); states.push(AsyncGeneratorQueueState { active: false, drain_scheduled: false, + started: false, + completed: false, + original_throw, queue: VecDeque::new(), }); id }); let obj_now = || js_nanbox_get_pointer(obj_h.get_nanbox_f64()) as *mut ObjectHeader; - let closure_now = |h: &crate::gc::RuntimeHandle<'_>| { - js_nanbox_get_pointer(h.get_nanbox_f64()) as *const ClosureHeader - }; for (name, original_h, func) in [ ( @@ -136,6 +144,7 @@ pub(crate) fn scan_async_generator_queue_roots_mut( ) { STATES.with(|states| { for state in states.borrow_mut().iter_mut() { + visitor.visit_raw_const_ptr_slot(&mut state.original_throw); for request in state.queue.iter_mut() { visitor.visit_raw_const_ptr_slot(&mut request.original); visitor.visit_nanbox_f64_slot(&mut request.arg); @@ -362,16 +371,46 @@ fn dispatch_return_with_await( arg: f64, out: *mut Promise, ) { - let arg_promise = crate::promise::js_promise_resolved(arg); - let fulfill = make_return_step_wrapper(state_id, original, out, true); - let reject = make_return_step_wrapper(state_id, original, out, false); - js_promise_attach_settle_listener(arg_promise, fulfill, reject); + let scope = crate::gc::RuntimeHandleScope::new(); + let original_h = scope.root_raw_const_ptr(original); + let out_h = scope.root_raw_mut_ptr(out); + let arg_h = scope.root_nanbox_f64(arg); + let arg_promise = match crate::promise::js_promise_resolved_catching(arg_h.get_nanbox_f64()) { + Ok(promise) => promise, + Err(reason) => { + let suspended_throw = STATES.with(|states| { + states + .borrow() + .get(state_id - 1) + .filter(|state| state.started && !state.completed) + .map(|state| state.original_throw) + }); + if let Some(throw_original) = suspended_throw { + let throw_h = scope.root_raw_const_ptr(throw_original); + let result = throw_h.with_const_ptr(|throw| call_original(throw, reason)); + out_h.with_mut_ptr(|out| after_queued_result(state_id, out, result)); + } else { + out_h.with_mut_ptr(|out| { + finish_after_immediate_queued_result(state_id, out, false, reason) + }); + } + return; + } + }; + let arg_promise_h = scope.root_raw_mut_ptr(arg_promise); + let fulfill = make_return_step_wrapper(state_id, &original_h, &out_h, true); + let fulfill_h = scope.root_raw_mut_ptr(fulfill); + let reject = make_return_step_wrapper(state_id, &original_h, &out_h, false); + arg_promise_h.with_mut_ptr(|arg_promise| { + fulfill_h + .with_mut_ptr(|fulfill| js_promise_attach_settle_listener(arg_promise, fulfill, reject)) + }); } fn make_return_step_wrapper( state_id: usize, - original: *const ClosureHeader, - out: *mut Promise, + original: &crate::gc::RuntimeHandle<'_>, + out: &crate::gc::RuntimeHandle<'_>, is_fulfilled: bool, ) -> *mut ClosureHeader { let func = if is_fulfilled { @@ -381,8 +420,10 @@ fn make_return_step_wrapper( }; let wrapper = js_closure_alloc(func, 3); js_closure_set_capture_f64(wrapper, 0, state_id as f64); - js_closure_set_capture_ptr(wrapper, 1, original as i64); - js_closure_set_capture_ptr(wrapper, 2, out as i64); + original.with_const_ptr::(|original| { + js_closure_set_capture_ptr(wrapper, 1, original as i64) + }); + out.with_mut_ptr::(|out| js_closure_set_capture_ptr(wrapper, 2, out as i64)); wrapper } @@ -459,6 +500,11 @@ fn after_initial_result(state_id: usize, result: f64) { attach_pending_settle(state_id, promise, std::ptr::null_mut()); return; } + if state == PromiseState::Fulfilled { + note_step_settlement(state_id, true, unsafe { (*promise).value }); + } else if state == PromiseState::Rejected { + note_step_settlement(state_id, false, unsafe { (*promise).reason }); + } } schedule_drain(state_id); } @@ -532,6 +578,7 @@ fn finish_after_pending_result(state_id: usize, out: *mut Promise, fulfilled: bo // pending path is now always taken; before #6709 `.next()` resolved // synchronously (busy-wait) and hit the immediate path, which already // deferred the drain via `schedule_drain`. + note_step_settlement(state_id, fulfilled, value); settle_out(out, fulfilled, value); let has_queue = STATES.with(|states| { states @@ -552,6 +599,7 @@ fn finish_after_immediate_queued_result( fulfilled: bool, value: f64, ) { + note_step_settlement(state_id, fulfilled, value); let has_queue = STATES.with(|states| { states .borrow() @@ -566,6 +614,29 @@ fn finish_after_immediate_queued_result( settle_out(out, fulfilled, value); } +fn note_step_settlement(state_id: usize, fulfilled: bool, value: f64) { + let done = if fulfilled { + let field = js_object_get_own_field_or_undef(value, b"done".as_ptr(), 4); + let jv = JSValue::from_bits(field.to_bits()); + (!jv.is_undefined()).then(|| crate::value::js_is_truthy(field) != 0) + } else { + None + }; + STATES.with(|states| { + let mut states = states.borrow_mut(); + let Some(state) = states.get_mut(state_id - 1) else { + return; + }; + if !fulfilled { + state.completed = true; + } else if let Some(true) = done { + state.completed = true; + } else if let Some(false) = done { + state.started = true; + } + }); +} + fn mark_inactive(state_id: usize) { STATES.with(|states| { if let Some(state) = states.borrow_mut().get_mut(state_id - 1) { diff --git a/crates/perry-runtime/src/object/buffer_dispatch.rs b/crates/perry-runtime/src/object/buffer_dispatch.rs index 4d569198e2..0383349399 100644 --- a/crates/perry-runtime/src/object/buffer_dispatch.rs +++ b/crates/perry-runtime/src/object/buffer_dispatch.rs @@ -516,15 +516,24 @@ pub unsafe fn dispatch_buffer_method( crate::buffer::array_buffer_transfer(addr, args) } "slice" | "subarray" => { - let len = (*buf_ptr).length as i32; - let (start, end) = if crate::buffer::is_array_buffer(addr) - || crate::buffer::is_shared_array_buffer(addr) - { + let source_is_array_buffer = crate::buffer::is_array_buffer(addr); + let source_is_shared_array_buffer = crate::buffer::is_shared_array_buffer(addr); + let source_is_any_array_buffer = + source_is_array_buffer || source_is_shared_array_buffer; + let scope = crate::gc::RuntimeHandleScope::new(); + let buffer = scope.root_raw_mut_ptr(buf_ptr); + let len = + buffer.with_mut_ptr::(|buf| (*buf).length as i32); + let (start, end) = if source_is_any_array_buffer { + // Instance-call lowering reaches this fused dispatch directly, + // bypassing the prototype thunk. The shared path therefore owns + // both index coercion and ArrayBufferSpeciesCreate ordering. // A detached ArrayBuffer refuses slice with a TypeError // (ES2024 DetachArrayBuffer; `transfer` is the only detach // source in Perry). - if crate::buffer::is_detached_buffer(addr) - && !crate::buffer::is_shared_array_buffer(addr) + if buffer.with_mut_ptr::(|buf| { + crate::buffer::is_detached_buffer(buf as usize) + }) && !source_is_shared_array_buffer { crate::collection_iter::throw_type_error( "Cannot perform ArrayBuffer.prototype.slice on a detached ArrayBuffer", @@ -542,19 +551,40 @@ pub unsafe fn dispatch_buffer_method( } else { ab_slice_index(args[1]) }; + // SpeciesConstructor is observed only after BOTH index + // coercions. It may run getters and collect, so the buffer is + // re-read from its handle again by the copy below. + if method_name == "slice" { + buffer.with_mut_ptr::(|buf| { + super::global_this::validate_array_buffer_species_constructor(buf as usize) + }); + // ArrayBuffer.prototype.slice checks detachment again + // after the observable species lookup/creation sequence. + if !source_is_shared_array_buffer + && buffer.with_mut_ptr::(|buf| { + crate::buffer::is_detached_buffer(buf as usize) + }) + { + crate::collection_iter::throw_type_error( + "Cannot perform ArrayBuffer.prototype.slice on a detached ArrayBuffer", + ); + } + } (s, e) } else { let s = arg_i32(0); let e = if args.len() >= 2 { arg_i32(1) } else { len }; (s, e) }; - let result = crate::buffer::js_buffer_slice(buf_ptr, start, end); + let result = buffer.with_mut_ptr::(|buf| { + crate::buffer::js_buffer_slice(buf, start, end) + }); // #2877: `ArrayBuffer.prototype.slice` returns a NEW ArrayBuffer // (a copy), so mark the result so `ArrayBuffer.isView(slice)` is // false and a subsequent `new Uint8Array(slice)` aliases it. - if crate::buffer::is_array_buffer(addr) { + if source_is_array_buffer { crate::buffer::mark_as_array_buffer(result as usize); - } else if crate::buffer::is_shared_array_buffer(addr) { + } else if source_is_shared_array_buffer { crate::buffer::mark_as_shared_array_buffer(result as usize); } f64::from_bits(JSValue::pointer(result as *mut u8).bits()) diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 64dd9aefa5..c0387c3bbe 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -43,6 +43,7 @@ mod class_meta; mod construct; pub(crate) use construct::scan_current_new_target_root_mut; mod dispatch; +mod function_prototype; mod gc_roots; pub(crate) mod parent_static; mod prototype_methods; @@ -117,15 +118,18 @@ pub use prototype_methods::{ // ── construct.rs / vm_brand.rs ────────────────────────────────────────────── pub(crate) use construct::{ - extends_target_must_throw, function_would_have_own_prototype, is_callable_function_value, - js_value_is_constructor, lookup_prototype_method, nm_ctor_child_process, nm_ctor_cluster, - nm_ctor_fs, nm_ctor_readline, nm_ctor_repl, nm_ctor_stream, nm_ctor_tls, nm_ctor_tty, - nm_ctor_vm, nm_ctor_wasi, ordinary_function_prototype_value_for_read, promise_parent_in_chain, + extends_target_must_throw, is_callable_function_value, js_value_is_constructor, + lookup_prototype_method, nm_ctor_child_process, nm_ctor_cluster, nm_ctor_fs, nm_ctor_readline, + nm_ctor_repl, nm_ctor_stream, nm_ctor_tls, nm_ctor_tty, nm_ctor_vm, nm_ctor_wasi, + promise_parent_in_chain, }; pub use construct::{ - js_ctor_return_override, js_function_prototype_value_for_read, js_new_function_construct, - js_new_function_construct_apply, js_new_function_construct_with_new_target, - js_new_target_value, + js_ctor_return_override, js_new_function_construct, js_new_function_construct_apply, + js_new_function_construct_with_new_target, js_new_target_value, +}; +pub use function_prototype::js_function_prototype_value_for_read; +pub(crate) use function_prototype::{ + function_would_have_own_prototype, ordinary_function_prototype_value_for_read, }; pub(crate) use vm_brand::brand_vm_script_instance; diff --git a/crates/perry-runtime/src/object/class_registry/construct.rs b/crates/perry-runtime/src/object/class_registry/construct.rs index e50e4f94f0..133aa98e7c 100644 --- a/crates/perry-runtime/src/object/class_registry/construct.rs +++ b/crates/perry-runtime/src/object/class_registry/construct.rs @@ -988,6 +988,9 @@ pub unsafe extern "C" fn js_new_function_construct( class_cid, class_cid, func_value, args_ptr, args_len, ); } + if extends_target_must_throw(func_value) { + super::super::object_ops::throw_object_type_error(b"is not a constructor"); + } if is_arrow_function_value(func_value) { crate::fs::validate::throw_type_error_with_code( "Arrow function is not a constructor", @@ -1130,7 +1133,10 @@ pub unsafe extern "C" fn js_new_function_construct( } return inst_handle.get_nanbox_f64(); } - nan_boxed + // Ordinary objects, symbols and every other non-callable heap value do not + // have [[Construct]]. The historical placeholder return made `new + // Reflect()`, `new Error.prototype()` and `new sym()` silently succeed. + super::super::object_ops::throw_object_type_error(b"is not a constructor") } /// `new (...spread)` — spread-bearing construction. Codegen builds a @@ -1666,13 +1672,16 @@ pub unsafe extern "C" fn js_new_function_construct_with_new_target( | "BigInt64Array" | "BigUint64Array" ) { - // Read `newTarget.prototype` (GetPrototypeFromConstructor) BEFORE - // building the view: Node evaluates the proto access as part of - // AllocateTypedArray, so a throwing `prototype` getter must surface - // here even when later steps would also throw (test262 - // `throw-type-error-before-custom-proto-access` agreement). - let proto_bits = new_target_custom_object_prototype(nt); + // Validate and initialize the typed-array contents before reading + // a custom newTarget prototype. In particular, a Number/BigInt + // element-type mismatch must throw TypeError without observing a + // poisoned `newTarget.prototype` getter. + let scope = crate::gc::RuntimeHandleScope::new(); + let nt_h = scope.root_nanbox_f64(nt); let result = js_new_function_construct(func_value, args_ptr, args_len); + let result_h = scope.root_heap_word_u64(result.to_bits()); + let proto_bits = new_target_custom_object_prototype(nt_h.get_nanbox_f64()); + let result = f64::from_bits(result_h.get_heap_word_u64()); if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(result) { if let Some(proto_bits) = proto_bits { super::super::prototype_chain::object_set_static_prototype(addr, proto_bits); @@ -1697,7 +1706,7 @@ pub unsafe extern "C" fn js_new_function_construct_with_new_target( let bits = result.to_bits(); let addr = if (bits >> 48) == 0x7FFD { (bits & crate::value::POINTER_MASK) as usize - } else if (bits >> 48) == 0 && bits >= 0x1000 { + } else if (bits >> 48) == 0 && crate::buffer::is_registered_buffer(bits as usize) { // ArrayBuffer and SharedArrayBuffer are represented by a // raw BufferHeader pointer rather than a NaN-boxed object. bits as usize @@ -1820,7 +1829,7 @@ pub(crate) fn is_callable_function_value(value: f64) -> bool { unsafe { (*ptr).type_tag == crate::closure::CLOSURE_MAGIC } } -fn is_arrow_function_value(value: f64) -> bool { +pub(super) fn is_arrow_function_value(value: f64) -> bool { use crate::value::JSValue; let jv = JSValue::from_bits(value.to_bits()); if !jv.is_pointer() { @@ -1841,133 +1850,6 @@ fn is_arrow_function_value(value: f64) -> bool { crate::closure::closure_is_arrow(ptr) } -/// Predicate-only sibling of `ordinary_function_prototype_value_for_read`: -/// would this function have an own `.prototype` slot? Crucially does NOT -/// materialize the prototype object — `fn.hasOwnProperty('prototype')` must -/// not lock the slot's attributes before a later -/// `Object.defineProperty(fn, "prototype", …)` (TypedArrayConstructors -/// custom-proto tests). -pub(crate) fn function_would_have_own_prototype(func_value: f64) -> bool { - if !is_callable_function_value(func_value) || is_arrow_function_value(func_value) { - return false; - } - if super::super::native_module::builtin_closure_is_non_constructable_value(func_value) { - return false; - } - synthetic_class_id_for_function(func_value) != 0 -} - -pub(crate) fn ordinary_function_prototype_value_for_read(func_value: f64) -> Option { - if !is_callable_function_value(func_value) || is_arrow_function_value(func_value) { - return None; - } - // Bound-method / bound-function values (class method/getter/setter reads via - // `C.prototype.m`, instance method reads, `fn.bind(...)`) are non-constructors - // and have NO `prototype` own property (`C.prototype.m.prototype === undefined`, - // `'prototype' in C.prototype.m === false`). (Test262 definition method/accessor - // prop-desc.) - // - // #4973 / #3527 / #5268 exception: bound NATIVE-MODULE *class* exports - // (`http.Server`, `fs.ReadStream`, `events.EventEmitter`, …) are - // constructors in Node, and the util.inherits / `Object.create(Ctor. - // prototype)` / `Object.setPrototypeOf(x, Ctor.prototype)` subclass - // pattern reads their `.prototype` as a setPrototypeOf / Object.create - // operand. Returning None here made that read `undefined`, and - // `Object.create(undefined)` / `Object.setPrototypeOf(x, undefined)` then - // threw "Object prototype may only be an Object or null" — the blocker hit - // at Express init (`express/lib/request.js`: - // `Object.create(http.IncomingMessage.prototype)`), graceful-fs's - // `ReadStream.prototype = Object.create(fs$ReadStream.prototype)`, and - // pino's `Object.setPrototypeOf(prototype, EventEmitter.prototype)`. - // - // A bound-native export is a constructor class when its method name uses - // Node's constructor-cased convention (a leading uppercase ASCII letter, - // e.g. `ReadStream`/`EventEmitter`/`Server`) AND it isn't explicitly - // marked non-constructable (built-in prototype methods like - // `String.prototype.charAt` carry that flag). Such exports are cached - // singleton closures (NATIVE_CALLABLE_EXPORTS), so the synthetic-class - // path below gives them a stable `.prototype` object. Non-constructor - // bound methods (`fs.readFile`, `path.join`, …) keep `prototype === - // undefined`, matching Node's built-in non-constructor functions. - { - let jv = crate::value::JSValue::from_bits(func_value.to_bits()); - if jv.is_pointer() { - let cptr = jv.as_pointer::(); - if !cptr.is_null() - && is_valid_obj_ptr(cptr as *const u8) - && crate::closure::closure_is_bound_method(cptr) - { - if super::super::native_module::builtin_closure_is_non_constructable_value( - func_value, - ) { - return None; - } - let is_native_class_export = unsafe { - super::super::native_module::bound_native_callable_module_and_method(func_value) - } - .map(|(_module, method)| { - method - .as_bytes() - .first() - .is_some_and(|b| b.is_ascii_uppercase()) - }) - .unwrap_or(false); - if !is_native_class_export { - return None; - } - } - } - } - // Built-in methods (`String.prototype.charAt`, `Array.prototype.map`, …) are - // not constructors and have NO `prototype` own property — `String.prototype. - // charAt.prototype === undefined` (ECMA-262: built-in non-constructor - // functions don't get the auto-created `.prototype`). Don't lazily synthesize - // one for them. - if super::super::native_module::builtin_closure_is_non_constructable_value(func_value) { - return None; - } - let cid = synthetic_class_id_for_function(func_value); - if cid == 0 { - return None; - } - let proto = ensure_function_prototype_object(func_value, cid); - if proto.is_null() { - return None; - } - Some(crate::value::js_nanbox_pointer(proto as i64)) -} - -#[no_mangle] -pub extern "C" fn js_function_prototype_value_for_read(func_value: f64) -> f64 { - let undef = f64::from_bits(crate::value::TAG_UNDEFINED); - let jv = crate::value::JSValue::from_bits(func_value.to_bits()); - if !jv.is_pointer() { - return undef; - } - let ptr = jv.as_pointer() as *const crate::closure::ClosureHeader; - if ptr.is_null() || !is_valid_obj_ptr(ptr as *const u8) { - return undef; - } - unsafe { - if (*ptr).type_tag != crate::closure::CLOSURE_MAGIC { - return undef; - } - } - - let closure_addr = ptr as usize; - if crate::closure::closure_is_key_deleted(closure_addr, "prototype") { - return undef; - } - let dynamic = crate::closure::closure_get_dynamic_prop(closure_addr, "prototype"); - if dynamic.to_bits() != crate::value::TAG_UNDEFINED { - return dynamic; - } - if let Some(proto) = generator_function_prototype_of(closure_addr) { - return proto; - } - ordinary_function_prototype_value_for_read(func_value).unwrap_or(undef) -} - /// Lookup helper: returns the registered prototype-method value for /// `(class_id, name)`, or None if no assignment matched. Walks the /// parent-class chain so methods registered on a base class are found diff --git a/crates/perry-runtime/src/object/class_registry/construct/class_return.rs b/crates/perry-runtime/src/object/class_registry/construct/class_return.rs index be5091de83..6f3077ae2b 100644 --- a/crates/perry-runtime/src/object/class_registry/construct/class_return.rs +++ b/crates/perry-runtime/src/object/class_registry/construct/class_return.rs @@ -39,7 +39,7 @@ fn constructor_return_overrides_this(value: f64) -> bool { let bits = value.to_bits(); let raw_addr = if jv.is_pointer() { (bits & crate::value::POINTER_MASK) as usize - } else if (bits >> 48) == 0 && bits >= 0x1000 { + } else if (bits >> 48) == 0 { bits as usize } else { 0 @@ -72,13 +72,12 @@ fn constructor_return_overrides_this(value: f64) -> bool { if !arr.is_null() { return true; } - if !is_valid_obj_ptr(raw as *const u8) { + let Some(gc_header) = crate::value::addr_class::try_read_tracked_gc_header(raw as usize) + else { return false; - } - let gc_header = - (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + }; matches!( - (*gc_header).obj_type, + (*gc_header.as_ptr()).obj_type, // Per spec, a constructor returning ANY Object overrides the // implicit `this`. Promises are objects — a user constructor like // `function P(exec){ return new Promise(...) }` (the diff --git a/crates/perry-runtime/src/object/class_registry/function_prototype.rs b/crates/perry-runtime/src/object/class_registry/function_prototype.rs new file mode 100644 index 0000000000..da90ae67fd --- /dev/null +++ b/crates/perry-runtime/src/object/class_registry/function_prototype.rs @@ -0,0 +1,104 @@ +use super::construct::{is_arrow_function_value, is_callable_function_value}; +use super::*; + +/// Does this function have an own `.prototype` slot? This intentionally does +/// not materialize the prototype, so `hasOwnProperty` cannot freeze its attrs. +pub(crate) fn function_would_have_own_prototype(func_value: f64) -> bool { + if !is_callable_function_value(func_value) + || is_arrow_function_value(func_value) + || is_plain_async_function_value(func_value) + || super::super::native_module::builtin_closure_is_non_constructable_value(func_value) + { + return false; + } + synthetic_class_id_for_function(func_value) != 0 +} + +pub(crate) fn ordinary_function_prototype_value_for_read(func_value: f64) -> Option { + if !is_callable_function_value(func_value) + || is_arrow_function_value(func_value) + || is_plain_async_function_value(func_value) + { + return None; + } + // Bound native class exports are constructors; ordinary bound methods are + // not. A stable synthetic class gives constructor exports a prototype. + let jv = crate::value::JSValue::from_bits(func_value.to_bits()); + if jv.is_pointer() { + let cptr = jv.as_pointer::(); + if !cptr.is_null() + && crate::value::addr_class::is_plausible_heap_addr(cptr as usize) + && crate::closure::closure_is_bound_method(cptr) + { + if super::super::native_module::builtin_closure_is_non_constructable_value(func_value) { + return None; + } + let is_native_class_export = unsafe { + super::super::native_module::bound_native_callable_module_and_method(func_value) + } + .is_some_and(|(_, method)| { + method + .as_bytes() + .first() + .is_some_and(|b| b.is_ascii_uppercase()) + }); + if !is_native_class_export { + return None; + } + } + } + if super::super::native_module::builtin_closure_is_non_constructable_value(func_value) { + return None; + } + let cid = synthetic_class_id_for_function(func_value); + if cid == 0 { + return None; + } + let proto = ensure_function_prototype_object(func_value, cid); + (!proto.is_null()).then(|| crate::value::js_nanbox_pointer(proto as i64)) +} + +fn is_plain_async_function_value(func_value: f64) -> bool { + let jv = crate::value::JSValue::from_bits(func_value.to_bits()); + if !jv.is_pointer() { + return false; + } + let ptr = jv.as_pointer::(); + if ptr.is_null() || !crate::value::addr_class::is_plausible_heap_addr(ptr as usize) { + return false; + } + let fp = crate::closure::get_valid_func_ptr(ptr); + !fp.is_null() + && crate::closure::is_registered_async_function(fp) + && !crate::closure::is_registered_generator_function(fp) +} + +#[no_mangle] +pub extern "C" fn js_function_prototype_value_for_read(func_value: f64) -> f64 { + let undef = f64::from_bits(crate::value::TAG_UNDEFINED); + let jv = crate::value::JSValue::from_bits(func_value.to_bits()); + if !jv.is_pointer() { + return undef; + } + let ptr = jv.as_pointer() as *const crate::closure::ClosureHeader; + if ptr.is_null() || !crate::value::addr_class::is_plausible_heap_addr(ptr as usize) { + return undef; + } + unsafe { + if (*ptr).type_tag != crate::closure::CLOSURE_MAGIC { + return undef; + } + } + let closure_addr = ptr as usize; + if crate::closure::closure_is_key_deleted(closure_addr, "prototype") { + return undef; + } + let dynamic = crate::closure::closure_get_dynamic_prop(closure_addr, "prototype"); + if dynamic.to_bits() != crate::value::TAG_UNDEFINED { + return dynamic; + } + if let Some(proto) = generator_function_prototype_of(closure_addr) { + return proto; + } + ordinary_function_prototype_value_for_read(func_value).unwrap_or(undef) +} diff --git a/crates/perry-runtime/src/object/class_registry/parent_static.rs b/crates/perry-runtime/src/object/class_registry/parent_static.rs index 390de575ed..01c05a9a3c 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static.rs @@ -109,7 +109,7 @@ pub extern "C" fn js_register_class_parent_dynamic(class_id: u32, mut parent_val match name { "Request" => super::super::register_fetch_parent_kind(class_id, 1), "Response" => super::super::register_fetch_parent_kind(class_id, 2), - _ => {} + _ => super::super::data_view_registry::register_builtin_view_parent(class_id, name), } return; } diff --git a/crates/perry-runtime/src/object/collection_proto_thunks.rs b/crates/perry-runtime/src/object/collection_proto_thunks.rs index 23f68c6720..f2fe0399d6 100644 --- a/crates/perry-runtime/src/object/collection_proto_thunks.rs +++ b/crates/perry-runtime/src/object/collection_proto_thunks.rs @@ -21,6 +21,19 @@ crate::perry_thread_local! { static BUILTIN_SET_ADD_VALUE_BITS: std::cell::Cell = const { std::cell::Cell::new(0) }; } +pub(crate) fn scan_builtin_collection_method_roots_mut( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, +) { + for slot in [&BUILTIN_MAP_SET_VALUE_BITS, &BUILTIN_SET_ADD_VALUE_BITS] { + slot.with(|slot| { + let mut bits = slot.get(); + if visitor.visit_heap_word_u64_slot(&mut bits) { + slot.set(bits); + } + }); + } +} + pub(crate) fn is_builtin_map_set_value(value: f64) -> bool { is_remembered_builtin_collection_method(value, &BUILTIN_MAP_SET_VALUE_BITS) } @@ -164,6 +177,10 @@ pub(super) fn install_collection_proto_methods( ipm(proto_obj, "has", set_proto_has_thunk as *const u8, 1); ipm(proto_obj, "keys", set_proto_keys_thunk as *const u8, 0); let values_value = ipm(proto_obj, "values", set_proto_values_thunk as *const u8, 0); + // ECMA-262 specifies these as the same function object, not merely + // two functions with equivalent behavior. + let keys_key = crate::string::js_string_from_bytes(b"keys".as_ptr(), 4); + super::js_object_set_field_by_name(proto_obj, keys_key, values_value); install_collection_iterator_symbol(proto_obj, values_value); remember_builtin_collection_method( proto_obj, diff --git a/crates/perry-runtime/src/object/data_view_registry.rs b/crates/perry-runtime/src/object/data_view_registry.rs index 3eb76f6a04..d7f5bf433d 100644 --- a/crates/perry-runtime/src/object/data_view_registry.rs +++ b/crates/perry-runtime/src/object/data_view_registry.rs @@ -3,6 +3,8 @@ use super::*; /// Global registry of class IDs that extend the built-in DataView class. static EXTENDS_DATA_VIEW_REGISTRY: RwLock>> = RwLock::new(None); +static EXTENDS_TYPED_ARRAY_REGISTRY: RwLock>> = + RwLock::new(None); /// Mark a user-defined class as extending the built-in DataView class. #[no_mangle] @@ -39,3 +41,48 @@ pub(crate) fn extends_builtin_data_view(class_id: u32) -> bool { } false } + +#[no_mangle] +pub extern "C" fn js_register_class_extends_typed_array(class_id: u32) { + let mut registry = EXTENDS_TYPED_ARRAY_REGISTRY.write().unwrap(); + registry + .get_or_insert_with(std::collections::HashSet::new) + .insert(class_id); +} + +pub(crate) fn register_builtin_view_parent(class_id: u32, parent_name: &str) { + if parent_name == "DataView" { + js_register_class_extends_data_view(class_id); + } else if crate::typedarray::kind_for_name(parent_name).is_some() { + js_register_class_extends_typed_array(class_id); + } +} + +pub(crate) fn extends_builtin_typed_array(class_id: u32) -> bool { + let registry = EXTENDS_TYPED_ARRAY_REGISTRY.read().unwrap(); + let Some(registered) = registry.as_ref() else { + return false; + }; + if registered.contains(&class_id) { + return true; + } + let mut current = class_id; + let parent_reg = super::CLASS_REGISTRY.read().unwrap(); + if let Some(parents) = parent_reg.as_ref() { + // A valid parent chain cannot visit more entries than the registry + // contains. This follows arbitrarily deep user hierarchies while still + // terminating if malformed registry data contains a cycle. + for _ in 0..=parents.len() { + match parents.get(¤t).copied() { + Some(parent) if parent != 0 => { + if registered.contains(&parent) { + return true; + } + current = parent; + } + _ => break, + } + } + } + false +} diff --git a/crates/perry-runtime/src/object/date_proto_thunks.rs b/crates/perry-runtime/src/object/date_proto_thunks.rs index c58a56c389..9c45dd9b23 100644 --- a/crates/perry-runtime/src/object/date_proto_thunks.rs +++ b/crates/perry-runtime/src/object/date_proto_thunks.rs @@ -96,6 +96,17 @@ extern "C" fn date_to_utc_string(_closure: *const crate::closure::ClosureHeader) crate::value::js_nanbox_string(s as i64) } +#[cfg(feature = "temporal")] +extern "C" fn date_to_temporal_instant(_closure: *const crate::closure::ClosureHeader) -> f64 { + let timestamp = require_date_timestamp(); + crate::temporal::instant::from_epoch_milliseconds_static(&[timestamp]) +} + +#[cfg(not(feature = "temporal"))] +extern "C" fn date_to_temporal_instant(_closure: *const crate::closure::ClosureHeader) -> f64 { + f64::from_bits(crate::value::TAG_UNDEFINED) +} + /// `Date.prototype.toJSON` reflective thunk. Unlike the getters this is a /// *generic* method — it is not brand-checked to Date. Per ECMA-262 /// (`thisTimeValue` is NOT used): `ToObject(this)`, then `ToPrimitive(this, @@ -406,6 +417,12 @@ pub(crate) fn install_date_proto_getters(proto_obj: *mut ObjectHeader) { 0, ); super::global_this::install_proto_method(proto_obj, "toJSON", date_to_json as *const u8, 1); + super::global_this::install_proto_method( + proto_obj, + "toTemporalInstant", + date_to_temporal_instant as *const u8, + 0, + ); let utc = super::global_this::install_proto_method( proto_obj, "toUTCString", diff --git a/crates/perry-runtime/src/object/delete_rest.rs b/crates/perry-runtime/src/object/delete_rest.rs index a44acfd53b..4b7015e949 100644 --- a/crates/perry-runtime/src/object/delete_rest.rs +++ b/crates/perry-runtime/src/object/delete_rest.rs @@ -97,6 +97,22 @@ pub extern "C" fn js_object_delete_field( key, ); } + // ArrayBuffer / SharedArrayBuffer / DataView are registered + // BufferHeaders with ordinary named expandos. They must not fall + // through to the ObjectHeader keys-array walk. + if crate::buffer::is_registered_buffer(obj as usize) { + if let Some(name) = super::has_own_helpers::str_from_string_header(key) { + if let Some(attrs) = get_property_attrs(obj as usize, name) { + if !attrs.configurable() { + return 0; + } + } + crate::buffer::buffer_delete_own_prop(obj as usize, name); + super::clear_accessor_descriptor(obj as usize, name); + super::clear_property_attrs(obj as usize, name); + } + return 1; + } if let Some(result) = super::arguments_object_before_delete(obj, key) { return result; } @@ -115,6 +131,12 @@ pub extern "C" fn js_object_delete_field( return 0; } super::exotic_expando::value_remove(kind, obj as usize, name); + if kind == ExoticKind::Error { + crate::error::js_error_delete_builtin_own_property( + obj as *mut crate::error::ErrorHeader, + name, + ); + } super::clear_accessor_descriptor(obj as usize, name); super::clear_property_attrs(obj as usize, name); } diff --git a/crates/perry-runtime/src/object/descriptors.rs b/crates/perry-runtime/src/object/descriptors.rs index b75fbe0173..79d16cd3f1 100644 --- a/crates/perry-runtime/src/object/descriptors.rs +++ b/crates/perry-runtime/src/object/descriptors.rs @@ -275,6 +275,47 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu ); } + if obj_jv.is_pointer() { + let addr = crate::value::js_nanbox_get_pointer(obj_value) as usize; + if crate::buffer::is_registered_buffer(addr) { + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(obj_value); + let Some(name) = metadata_key_to_string(key_value) else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + let addr = crate::value::js_nanbox_get_pointer(receiver.get_nanbox_f64()) as usize; + if let Some(accessor) = get_accessor_descriptor(addr, &name) { + let attrs = get_property_attrs(addr, &name) + .unwrap_or_else(|| PropertyAttrs::new(false, false, false)); + return build_accessor_descriptor( + f64::from_bits(if accessor.get == 0 { + crate::value::TAG_UNDEFINED + } else { + accessor.get + }), + f64::from_bits(if accessor.set == 0 { + crate::value::TAG_UNDEFINED + } else { + accessor.set + }), + attrs.enumerable(), + attrs.configurable(), + ); + } + let Some(value) = crate::buffer::buffer_get_own_prop(addr, &name) else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + let attrs = get_property_attrs(addr, &name) + .unwrap_or_else(|| PropertyAttrs::new(true, true, true)); + return build_data_descriptor( + value, + attrs.writable(), + attrs.enumerable(), + attrs.configurable(), + ); + } + } + // Date / RegExp / Error exotic instances: own properties live in the // expando side tables (plus a few builtin own slots), never in an // `ObjectHeader` — the ordinary path below would bit-cast the cell. @@ -1014,12 +1055,14 @@ pub(crate) unsafe fn build_data_descriptor( const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; let bf = |b: bool| f64::from_bits(if b { TAG_TRUE } else { TAG_FALSE }); + let scope = crate::gc::RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(value); let packed = b"value\0writable\0enumerable\0configurable"; let desc = js_object_alloc_with_shape(0x0D_E5_C0, 4, packed.as_ptr(), packed.len() as u32); let header_size = std::mem::size_of::(); let fields = (desc as *mut u8).add(header_size) as *mut f64; // GC_STORE_AUDIT(INIT): descriptor object is freshly allocated; layout is rebuilt before publication. - *fields = value; + *fields = value.get_nanbox_f64(); *fields.add(1) = bf(writable); *fields.add(2) = bf(enumerable); *fields.add(3) = bf(configurable); @@ -1036,13 +1079,16 @@ pub(crate) unsafe fn build_accessor_descriptor( const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; let bf = |b: bool| f64::from_bits(if b { TAG_TRUE } else { TAG_FALSE }); + let scope = crate::gc::RuntimeHandleScope::new(); + let get = scope.root_nanbox_f64(get); + let set = scope.root_nanbox_f64(set); let packed = b"get\0set\0enumerable\0configurable"; let desc = js_object_alloc_with_shape(0x0D_E5_C1, 4, packed.as_ptr(), packed.len() as u32); let header_size = std::mem::size_of::(); let fields = (desc as *mut u8).add(header_size) as *mut f64; // GC_STORE_AUDIT(INIT): descriptor object is freshly allocated; layout is rebuilt before publication. - *fields = get; - *fields.add(1) = set; + *fields = get.get_nanbox_f64(); + *fields.add(1) = set.get_nanbox_f64(); *fields.add(2) = bf(enumerable); *fields.add(3) = bf(configurable); super::rebuild_object_field_layout(desc, 4); diff --git a/crates/perry-runtime/src/object/field_get_set/buffer_own_prop.rs b/crates/perry-runtime/src/object/field_get_set/buffer_own_prop.rs index b5fb088114..934d1ab998 100644 --- a/crates/perry-runtime/src/object/field_get_set/buffer_own_prop.rs +++ b/crates/perry-runtime/src/object/field_get_set/buffer_own_prop.rs @@ -25,6 +25,13 @@ pub(super) fn buffer_own_prop_or_method( key_bytes: &[u8], ) -> Option { let name = std::str::from_utf8(key_bytes).ok()?; + if let Some(accessor) = super::get_accessor_descriptor(obj as usize, name) { + if accessor.get == 0 { + return Some(JSValue::undefined()); + } + let receiver = crate::value::js_nanbox_pointer(obj as i64); + return Some(unsafe { super::invoke_accessor_getter(accessor.get, receiver) }); + } if let Some(v) = crate::buffer::buffer_get_own_prop(obj as usize, name) { return Some(JSValue::from_bits(v.to_bits())); } diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs index 9ab7ca06ce..da053cb3aa 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs @@ -593,6 +593,12 @@ pub(crate) fn get_field_by_name_object_tail( if val.to_bits() != crate::value::TAG_UNDEFINED { return JSValue::from_bits(val.to_bits()); } + if matches!(name_str, "caller" | "arguments") { + crate::fs::validate::throw_type_error_with_code( + "Restricted function property access", + "ERR_INVALID_ARG_TYPE", + ); + } // #3664: `g.constructor` for a generator/async-generator // function resolves through its [[Prototype]] (`%Generator%`) // to `%GeneratorFunction%` / `%AsyncGeneratorFunction%`. @@ -604,6 +610,11 @@ pub(crate) fn get_field_by_name_object_tail( { return JSValue::from_bits(ctor.to_bits()); } + let ctor = + super::super::js_get_global_this_builtin_value(b"Function".as_ptr(), 8); + if !JSValue::from_bits(ctor.to_bits()).is_undefined() { + return JSValue::from_bits(ctor.to_bits()); + } } // #3664: `g.prototype` for a generator/async-generator // function is a lazily-created object whose [[Prototype]] is @@ -704,12 +715,6 @@ pub(crate) fn get_field_by_name_object_tail( let v = crate::error::js_error_get_cause(err_ptr); return JSValue::from_bits(v.to_bits()); } - b"toString" => { - let this_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - let result = js_class_method_bind(this_f64, b"toString".as_ptr(), 8); - return JSValue::from_bits(result.to_bits()); - } b"constructor" => { let name = crate::error::error_kind_constructor_name((*err_ptr).error_kind); let name = name.as_bytes(); @@ -1209,8 +1214,11 @@ pub(crate) fn get_field_by_name_object_tail( let key_ptr = (key as *const u8).add(std::mem::size_of::()); let key_len = (*key).byte_len as usize; let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + // `next` is an ordinary prototype method. Do not bind it to the + // iterator at property-read time: `iterator.next.call(other)` must + // receive `other` and perform the spec brand check. The remaining + // legacy synthetic methods still use the bound-method path. let bind_name: Option<&'static [u8]> = match key_bytes { - b"next" => Some(b"next"), b"return" => Some(b"return"), b"throw" => Some(b"throw"), b"@@iterator" => Some(b"@@iterator"), @@ -1222,7 +1230,9 @@ pub(crate) fn get_field_by_name_object_tail( let result = js_class_method_bind(this_f64, name.as_ptr(), name.len()); return JSValue::from_bits(result.to_bits()); } - return JSValue::undefined(); + if key_bytes != b"next" { + return JSValue::undefined(); + } } // Issue #649: native-module sub-namespace property access. diff --git a/crates/perry-runtime/src/object/global_this.rs b/crates/perry-runtime/src/object/global_this.rs index 5228586c4c..8dd1613f0a 100644 --- a/crates/perry-runtime/src/object/global_this.rs +++ b/crates/perry-runtime/src/object/global_this.rs @@ -93,19 +93,20 @@ pub(crate) use builtin_thunks::{ }; pub use ctor_thunks::js_webcrypto_illegal_constructor; pub(crate) use ctor_thunks::{ - builtin_prototype_value, cryptokey_algorithm_getter_thunk, cryptokey_extractable_getter_thunk, - cryptokey_type_getter_thunk, cryptokey_usages_getter_thunk, error_constructor_call_thunk, - eval_error_constructor_call_thunk, global_this_crypto_getter_thunk, - global_this_url_pattern_call_thunk, is_array_prototype_method_value, - is_function_prototype_object_value, map_constructor_call_thunk, normalize_eval_this_body, - promise_constructor_call_thunk, range_error_constructor_call_thunk, - reference_error_constructor_call_thunk, regexp_constructor_call_thunk, - set_constructor_call_thunk, subtle_crypto_method_value, syntax_error_constructor_call_thunk, - type_error_constructor_call_thunk, typed_array_constructor_call_thunk, - uri_error_constructor_call_thunk, weak_map_constructor_call_thunk, - weak_ref_constructor_call_thunk, weak_set_constructor_call_thunk, - webcrypto_get_random_values_thunk, webcrypto_illegal_constructor_thunk, webcrypto_method_value, - webcrypto_random_uuid_thunk, webcrypto_subtle_getter_thunk, + builtin_prototype_value, construct_only_builtin_call_thunk, cryptokey_algorithm_getter_thunk, + cryptokey_extractable_getter_thunk, cryptokey_type_getter_thunk, cryptokey_usages_getter_thunk, + error_constructor_call_thunk, eval_error_constructor_call_thunk, + global_this_crypto_getter_thunk, global_this_url_pattern_call_thunk, + is_array_prototype_method_value, is_function_prototype_object_value, + map_constructor_call_thunk, normalize_eval_this_body, promise_constructor_call_thunk, + range_error_constructor_call_thunk, reference_error_constructor_call_thunk, + regexp_constructor_call_thunk, set_constructor_call_thunk, subtle_crypto_method_value, + syntax_error_constructor_call_thunk, type_error_constructor_call_thunk, + typed_array_constructor_call_thunk, uri_error_constructor_call_thunk, + weak_map_constructor_call_thunk, weak_ref_constructor_call_thunk, + weak_set_constructor_call_thunk, webcrypto_get_random_values_thunk, + webcrypto_illegal_constructor_thunk, webcrypto_method_value, webcrypto_random_uuid_thunk, + webcrypto_subtle_getter_thunk, }; #[cfg(feature = "temporal")] pub(crate) use fetch_globals::temporal_subclass_super; @@ -120,9 +121,12 @@ pub use fetch_globals::{ js_fetch_or_value_super, js_get_global_this, js_global_or_console_property_by_name, js_module_top_this, js_request_subclass_init, js_response_subclass_init, }; +#[cfg(test)] +pub(crate) use generator::append_async_function_root_snapshot; pub(crate) use generator::{ ensure_generator_intrinsics, generator_function_constructor_of, generator_function_proto_of, generator_function_prototype_of, set_intrinsic_data_prop, set_intrinsic_to_string_tag, + wire_async_function_intrinsic_parents, }; pub use generator::{js_generator_attach_closure_prototype, js_generator_attach_prototype}; pub use install_static::js_promise_static_function_value; @@ -149,8 +153,9 @@ pub(crate) use proto_methods::{ install_error_prototype_data_properties, populate_builtin_prototype_methods, }; pub(crate) use typed_array::{ - array_buffer_byte_length_getter_thunk, array_buffer_is_view_thunk, + array_buffer_byte_length_getter_thunk, array_buffer_is_view_thunk, array_buffer_slice_thunk, ensure_typed_array_intrinsic, install_function_has_instance_symbol, shared_array_buffer_byte_length_getter_thunk, shared_array_buffer_slice_thunk, typed_array_constructor_this_kind, typed_array_intrinsic_proto_ptr, + validate_array_buffer_species_constructor, }; diff --git a/crates/perry-runtime/src/object/global_this/ctor_thunks.rs b/crates/perry-runtime/src/object/global_this/ctor_thunks.rs index 60430e6619..0cc46e03b5 100644 --- a/crates/perry-runtime/src/object/global_this/ctor_thunks.rs +++ b/crates/perry-runtime/src/object/global_this/ctor_thunks.rs @@ -24,6 +24,12 @@ pub(crate) extern "C" fn typed_array_constructor_call_thunk( super::super::object_ops::throw_object_type_error(b"Constructor %TypedArray% requires 'new'") } +pub(crate) extern "C" fn construct_only_builtin_call_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + super::super::object_ops::throw_object_type_error(b"Constructor requires 'new'") +} + /// `RegExp(pattern, flags)` called WITHOUT `new` — unlike Map/Set below, /// RegExp IS callable: ECMA-262 22.2.4 makes the call form construct exactly /// like `new RegExp(pattern, flags)`, with one identity shortcut — `RegExp(re)` diff --git a/crates/perry-runtime/src/object/global_this/generator.rs b/crates/perry-runtime/src/object/global_this/generator.rs index 28026aff9a..4ca1337ff4 100644 --- a/crates/perry-runtime/src/object/global_this/generator.rs +++ b/crates/perry-runtime/src/object/global_this/generator.rs @@ -1,5 +1,25 @@ use super::*; +#[cfg(test)] +pub(crate) fn append_async_function_root_snapshot(roots: &mut Vec<(&'static str, usize, u64)>) { + for (name, slot) in [ + ( + "ASYNC_FUNCTION_INTRINSIC_PTR", + &crate::object::ASYNC_FUNCTION_INTRINSIC_PTR, + ), + ( + "ASYNC_FUNCTION_INTRINSIC_PROTO_PTR", + &crate::object::ASYNC_FUNCTION_INTRINSIC_PROTO_PTR, + ), + ] { + roots.push(( + name, + slot.test_slot_addr(), + slot.load(Ordering::Acquire) as u64, + )); + } +} + /// Distinguishes plain vs async generator closures for the intrinsic-tower /// lookups. #[derive(Clone, Copy, PartialEq, Eq)] @@ -30,6 +50,18 @@ fn closure_generator_kind(closure_ptr: usize) -> Option { } } +/// Plain async closures have their own hidden `%AsyncFunction%` intrinsic +/// tower. Async generators are intentionally excluded: they use the separate +/// async-generator tower classified above. +fn is_plain_async_function(closure_ptr: usize) -> bool { + let closure = closure_ptr as *const crate::closure::ClosureHeader; + let func_ptr = crate::closure::get_valid_func_ptr(closure); + !func_ptr.is_null() + && crate::closure::is_registered_async_function(func_ptr) + && !crate::closure::is_registered_generator_function(func_ptr) + && !crate::closure::is_registered_async_generator_function(func_ptr) +} + fn intrinsic_pointer_value(slot: i64) -> Option { if slot != 0 { Some(crate::value::js_nanbox_pointer(slot)) @@ -43,6 +75,12 @@ fn intrinsic_pointer_value(slot: i64) -> Option { /// `None` for non-generator closures so the caller keeps its existing /// `closure_static_prototype` / null resolution. (#3664) pub(crate) fn generator_function_proto_of(closure_ptr: usize) -> Option { + if is_plain_async_function(closure_ptr) { + ensure_generator_intrinsics(); + return intrinsic_pointer_value( + crate::object::ASYNC_FUNCTION_INTRINSIC_PROTO_PTR.load(Ordering::Acquire), + ); + } let kind = closure_generator_kind(closure_ptr)?; // The towers are normally built in `populate_global_this_builtins`, but a // program that reflects on a generator without ever touching `globalThis` @@ -57,6 +95,65 @@ pub(crate) fn generator_function_proto_of(closure_ptr: usize) -> Option { intrinsic_pointer_value(slot) } +/// Build the hidden `%AsyncFunction%` constructor and its ordinary, +/// non-callable `.prototype` object. The parent links are attached separately +/// once the global `Function` constructor has been populated. +fn build_async_function_tower() { + let _no_move = crate::gc::GcSuppressScope::new(); + let noop = global_this_builtin_noop_thunk as *const u8; + let ctor = crate::closure::js_closure_alloc(noop, 0); + let proto = js_object_alloc(0, 0); + if ctor.is_null() || proto.is_null() { + return; + } + let configurable = super::super::PropertyAttrs::new(false, false, true); + let fixed = super::super::PropertyAttrs::new(false, false, false); + + crate::closure::js_register_closure_arity(noop, 1); + super::super::native_module::set_bound_native_closure_name(ctor, "AsyncFunction"); + super::super::native_module::set_builtin_closure_length(ctor as usize, 1); + super::super::set_builtin_property_attrs(ctor as usize, "name".to_string(), configurable); + super::super::set_builtin_property_attrs(ctor as usize, "length".to_string(), configurable); + set_intrinsic_data_prop( + ctor as *mut ObjectHeader, + "prototype", + crate::value::js_nanbox_pointer(proto as i64), + fixed, + ); + set_intrinsic_data_prop( + proto, + "constructor", + crate::value::js_nanbox_pointer(ctor as i64), + configurable, + ); + set_intrinsic_to_string_tag(proto, "AsyncFunction"); + + crate::object::ASYNC_FUNCTION_INTRINSIC_PTR.store(ctor as i64, Ordering::Release); + crate::object::ASYNC_FUNCTION_INTRINSIC_PROTO_PTR.store(proto as i64, Ordering::Release); +} + +/// Complete `%AsyncFunction%.__proto__ = Function` and +/// `%AsyncFunction.prototype%.__proto__ = Function.prototype` after the +/// global constructor table exists. +pub(crate) fn wire_async_function_intrinsic_parents() { + let ctor = crate::object::ASYNC_FUNCTION_INTRINSIC_PTR.load(Ordering::Acquire); + let proto = crate::object::ASYNC_FUNCTION_INTRINSIC_PROTO_PTR.load(Ordering::Acquire); + if ctor == 0 || proto == 0 { + return; + } + let function_ctor = js_get_global_this_builtin_value(b"Function".as_ptr(), 8); + if crate::value::JSValue::from_bits(function_ctor.to_bits()).is_pointer() { + crate::closure::closure_set_static_prototype(ctor as usize, function_ctor.to_bits()); + } + let function_proto = builtin_prototype_value("Function"); + if crate::value::JSValue::from_bits(function_proto.to_bits()).is_pointer() { + super::super::prototype_chain::object_set_static_prototype( + proto as usize, + function_proto.to_bits(), + ); + } +} + /// `g.constructor` for a generator-function closure `g` → `%GeneratorFunction%` /// / `%AsyncGeneratorFunction%`. `None` for non-generator closures. (#3664) pub(crate) fn generator_function_constructor_of(closure_ptr: usize) -> Option { @@ -79,10 +176,16 @@ pub(crate) fn generator_function_constructor_of(closure_ptr: usize) -> Option Option { - let kind = closure_generator_kind(closure_ptr)?; + let scope = crate::gc::RuntimeHandleScope::new(); + let closure_h = scope.root_raw_const_ptr(closure_ptr as *const crate::closure::ClosureHeader); + let kind = closure_h.with_const_ptr::(|closure| { + closure_generator_kind(closure as usize) + })?; // A previously-created (or user-assigned) `prototype` wins — preserves // identity and lets `g.prototype = X` overrides stick. - let existing = crate::closure::closure_get_dynamic_prop(closure_ptr, "prototype"); + let existing = closure_h.with_const_ptr::(|closure| { + crate::closure::closure_get_dynamic_prop(closure as usize, "prototype") + }); if existing.to_bits() != crate::value::TAG_UNDEFINED { return Some(f64::from_bits(existing.to_bits())); } @@ -99,7 +202,6 @@ pub(crate) fn generator_function_prototype_of(closure_ptr: usize) -> Option if obj.is_null() { return None; } - let scope = crate::gc::RuntimeHandleScope::new(); let obj_h = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(obj as i64)); let gen_proto = generator_prototype_ptr(matches!(kind, GeneratorKind::Async)); @@ -111,12 +213,15 @@ pub(crate) fn generator_function_prototype_of(closure_ptr: usize) -> Option ); } let obj_value = obj_h.get_nanbox_f64(); - crate::closure::closure_set_dynamic_prop(closure_ptr, "prototype", obj_value); - super::super::set_builtin_property_attrs( - closure_ptr, - "prototype".to_string(), - super::super::PropertyAttrs::new(true, false, false), - ); + closure_h.with_const_ptr::(|closure| { + let closure = closure as usize; + crate::closure::closure_set_dynamic_prop(closure, "prototype", obj_value); + super::super::set_builtin_property_attrs( + closure, + "prototype".to_string(), + super::super::PropertyAttrs::new(true, false, false), + ); + }); Some(obj_h.get_nanbox_f64()) } @@ -340,6 +445,11 @@ fn install_proto_symbol_self_method( crate::value::js_nanbox_pointer(closure as i64), ); } + crate::symbol::set_symbol_property_attrs( + proto as usize, + sym as usize, + super::super::PropertyAttrs::new(true, false, true), + ); } /// Stamp `NO_THIS_REBIND_FLAG` onto the `next`/`return`/`throw` step-closure @@ -707,7 +817,18 @@ fn build_generator_tower( generator_proto_iterator_thunk as *const u8, ) }; - install_proto_symbol_self_method(gen_proto, symbol_name, display_name, thunk); + if is_async { + // `%AsyncGenerator.prototype%` inherits from a distinct + // `%AsyncIteratorPrototype%`; reflection reaches that parent with two + // `Object.getPrototypeOf` calls from an async generator function's + // `.prototype`. + let async_iterator_proto = js_object_alloc(0, 0); + install_proto_symbol_self_method(async_iterator_proto, symbol_name, display_name, thunk); + let parent_bits = crate::value::js_nanbox_pointer(async_iterator_proto as i64).to_bits(); + super::super::prototype_chain::object_set_static_prototype(gen_proto as usize, parent_bits); + } else { + install_proto_symbol_self_method(gen_proto, symbol_name, display_name, thunk); + } set_intrinsic_to_string_tag(gen_proto, inst_tag); ctor_slot.store(ctor as i64, Ordering::Release); @@ -718,6 +839,9 @@ fn build_generator_tower( /// Build both generator intrinsic towers. Idempotent within the current /// agent; called during its global bootstrap or by the lazy accessors. (#3664) pub(crate) fn ensure_generator_intrinsics() { + if crate::object::ASYNC_FUNCTION_INTRINSIC_PTR.load(Ordering::Acquire) == 0 { + build_async_function_tower(); + } if crate::object::GENERATOR_FUNCTION_INTRINSIC_PTR.load(Ordering::Acquire) == 0 { build_generator_tower( false, diff --git a/crates/perry-runtime/src/object/global_this/install_static.rs b/crates/perry-runtime/src/object/global_this/install_static.rs index 2c2cf65811..0e8479b50c 100644 --- a/crates/perry-runtime/src/object/global_this/install_static.rs +++ b/crates/perry-runtime/src/object/global_this/install_static.rs @@ -570,6 +570,19 @@ pub(crate) fn install_builtin_constructor_statics( "Symbol" => { install_constructor_static(ctor, "for", symbol_for_thunk as *const u8, 1, false); install_constructor_static(ctor, "keyFor", symbol_key_for_thunk as *const u8, 1, false); + for name in ["iterator", "asyncIterator"] { + let symbol = crate::symbol::well_known_symbol(name); + crate::closure::closure_set_dynamic_prop( + ctor as usize, + name, + crate::value::js_nanbox_pointer(symbol as i64), + ); + super::super::set_builtin_property_attrs( + ctor as usize, + name.to_string(), + super::super::PropertyAttrs::new(false, false, false), + ); + } } "String" => { // #4627: reify the variadic `String.fromCharCode` / `fromCodePoint` diff --git a/crates/perry-runtime/src/object/global_this/populate.rs b/crates/perry-runtime/src/object/global_this/populate.rs index 37fdfb2b48..919bae51c5 100644 --- a/crates/perry-runtime/src/object/global_this/populate.rs +++ b/crates/perry-runtime/src/object/global_this/populate.rs @@ -106,6 +106,11 @@ pub(crate) fn populate_global_this_builtins(singleton_at_entry: *mut ObjectHeade let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); let value = crate::value::js_nanbox_pointer(singleton() as i64); js_object_set_field_by_name(singleton(), key, value); + super::super::set_builtin_property_attrs( + singleton() as usize, + "globalThis".to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); } { // #4511: Node exposes the global object as `global` too @@ -195,6 +200,9 @@ pub(crate) fn populate_global_this_builtins(singleton_at_entry: *mut ObjectHeade "WeakSet" => weak_set_constructor_call_thunk as *const u8, "WeakRef" => weak_ref_constructor_call_thunk as *const u8, "Promise" => promise_constructor_call_thunk as *const u8, + "ArrayBuffer" | "SharedArrayBuffer" | "DataView" => { + construct_only_builtin_call_thunk as *const u8 + } _ => global_this_builtin_noop_thunk as *const u8, }; let closure_ptr = crate::closure::js_closure_alloc(func_ptr, 0); @@ -486,6 +494,10 @@ pub(crate) fn populate_global_this_builtins(singleton_at_entry: *mut ObjectHeade super::super::PropertyAttrs::new(true, false, true), ); } + // The hidden `%AsyncFunction%` tower is allocated before the constructor + // loop, but its two parents are the `Function` values installed by that + // loop. Complete those links now that both are available. + wire_async_function_intrinsic_parents(); // Callable global functions: ClosureHeader-backed values with real // dispatch so direct property reads and rebound calls match bare calls. for name in GLOBAL_THIS_BUILTIN_FUNCTIONS.iter().copied() { diff --git a/crates/perry-runtime/src/object/global_this/proto_methods.rs b/crates/perry-runtime/src/object/global_this/proto_methods.rs index 4e09f6adb6..d63cf18478 100644 --- a/crates/perry-runtime/src/object/global_this/proto_methods.rs +++ b/crates/perry-runtime/src/object/global_this/proto_methods.rs @@ -246,7 +246,7 @@ pub(crate) fn populate_builtin_prototype_methods(builtin_name: &str, proto_obj: install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); } "ArrayBuffer" => { - install_noop_proto_methods(proto_obj, &[("slice", 2)]); + install_proto_method(proto_obj, "slice", array_buffer_slice_thunk as *const u8, 2); unsafe { crate::closure::js_register_closure_arity( array_buffer_byte_length_getter_thunk as *const u8, @@ -509,12 +509,16 @@ pub(crate) fn populate_builtin_prototype_methods(builtin_name: &str, proto_obj: // #4100: mirror `Symbol` — brand-checking `toString`(radix)/`valueOf` // re-dispatched to the canonical BigInt logic (`(5n).toString(2)` // → `"101"`). After OBJECT_PROTO_METHODS so the brand `valueOf` wins. - install_proto_method( + let to_string = install_proto_method( proto_obj, "toString", primitive_proto_thunks::bigint_proto_to_string_thunk as *const u8, 1, ); + super::super::native_module::set_builtin_closure_length( + crate::value::js_nanbox_get_pointer(to_string) as usize, + 0, + ); install_proto_method( proto_obj, "valueOf", diff --git a/crates/perry-runtime/src/object/global_this/typed_array.rs b/crates/perry-runtime/src/object/global_this/typed_array.rs index 252bca14b3..d980e62f56 100644 --- a/crates/perry-runtime/src/object/global_this/typed_array.rs +++ b/crates/perry-runtime/src/object/global_this/typed_array.rs @@ -95,6 +95,105 @@ pub(crate) extern "C" fn shared_array_buffer_slice_thunk( } } +pub(crate) extern "C" fn array_buffer_slice_thunk( + _closure: *const crate::closure::ClosureHeader, + start: f64, + end: f64, +) -> f64 { + match array_buffer_receiver_addr() { + Some(addr) => unsafe { + let args = [start, end]; + super::super::buffer_dispatch::dispatch_buffer_method(addr, "slice", args.as_ptr(), 2) + }, + None => super::super::object_ops::throw_object_type_error( + b"Method ArrayBuffer.prototype.slice called on incompatible receiver", + ), + } +} + +pub(crate) unsafe fn validate_array_buffer_species_constructor(addr: usize) { + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(addr as i64)); + let receiver_value = || receiver.get_nanbox_f64(); + let receiver_addr = || crate::value::js_nanbox_get_pointer(receiver_value()) as usize; + + // SpeciesConstructor starts with ordinary Get(O, "constructor"). Buffer + // own data/accessor properties live in side tables; absent own state falls + // through the receiver's recorded custom prototype chain before using the + // intrinsic constructor. + let value = if let Some(accessor) = + super::super::get_accessor_descriptor(receiver_addr(), "constructor") + { + if accessor.get == 0 { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + f64::from_bits( + super::super::invoke_accessor_getter(accessor.get, receiver_value()).bits(), + ) + } + } else if let Some(value) = crate::buffer::buffer_get_own_prop(receiver_addr(), "constructor") { + value + } else { + let inherited = + if super::super::prototype_chain::object_static_prototype(receiver_addr()).is_some() { + let key = crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); + super::super::prototype_chain::resolve_inherited_field(receiver_addr(), key) + .map(|value| f64::from_bits(value.bits())) + } else { + None + }; + inherited.unwrap_or_else(|| { + let name: &[u8] = if crate::buffer::is_shared_array_buffer(receiver_addr()) { + b"SharedArrayBuffer" + } else { + b"ArrayBuffer" + }; + super::js_get_global_this_builtin_value(name.as_ptr(), name.len()) + }) + }; + let value = JSValue::from_bits(value.to_bits()); + // `null` is NaN-boxed in the pointer-shaped value family, but it is not + // an Object and therefore cannot supply @@species. + if !value.is_undefined() + && (value.is_null() + || (!value.is_pointer() + && !super::super::js_value_is_constructor(f64::from_bits(value.bits()))) + // Symbols use the pointer-shaped NaN-box family, but remain + // primitives for SpeciesConstructor's Type(C) check. + || crate::symbol::js_is_symbol(f64::from_bits(value.bits())) != 0) + { + super::super::object_ops::throw_object_type_error( + b"ArrayBuffer species constructor is not an object", + ); + } + if value.is_undefined() { + return; + } + + // Complete SpeciesConstructor validation: Get(C, @@species), accept + // undefined/null as the default constructor, otherwise require an actual + // constructable value. The constructor is rooted because a species getter + // can execute arbitrary user code and collect. + let constructor = scope.root_nanbox_f64(f64::from_bits(value.bits())); + let species = crate::symbol::well_known_symbol("species"); + if species.is_null() { + return; + } + let species_value = crate::symbol::js_object_get_symbol_property( + constructor.get_nanbox_f64(), + f64::from_bits(JSValue::pointer(species as *const u8).bits()), + ); + let species_value = JSValue::from_bits(species_value.to_bits()); + if species_value.is_undefined() || species_value.is_null() { + return; + } + if !super::super::js_value_is_constructor(f64::from_bits(species_value.bits())) { + super::super::object_ops::throw_object_type_error( + b"ArrayBuffer species is not a constructor", + ); + } +} + pub(crate) extern "C" fn array_buffer_is_view_thunk( _closure: *const crate::closure::ClosureHeader, value: f64, @@ -111,6 +210,7 @@ pub(crate) extern "C" fn array_buffer_is_view_thunk( && !crate::buffer::is_any_array_buffer(addr) && (crate::buffer::is_uint8array_buffer(addr) || crate::buffer::is_data_view(addr))) || jsvalue_extends_data_view(value) + || jsvalue_extends_typed_array(value) || crate::typedarray::lookup_typed_array_kind(addr).is_some(); f64::from_bits(crate::value::JSValue::bool(is_view).bits()) } @@ -121,12 +221,13 @@ fn jsvalue_extends_data_view(value: f64) -> bool { return false; } let ptr = v.as_pointer::(); - if ptr.is_null() || !crate::object::is_valid_obj_ptr(ptr) { + let Some(gc_header) = + (unsafe { crate::value::addr_class::try_read_tracked_gc_header(ptr as usize) }) + else { return false; - } + }; unsafe { - let gc_header = ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type != crate::gc::GC_TYPE_OBJECT { + if (*gc_header.as_ptr()).obj_type != crate::gc::GC_TYPE_OBJECT { return false; } let obj = ptr as *const ObjectHeader; @@ -135,6 +236,26 @@ fn jsvalue_extends_data_view(value: f64) -> bool { } } +fn jsvalue_extends_typed_array(value: f64) -> bool { + let v = JSValue::from_bits(value.to_bits()); + if !v.is_pointer() { + return false; + } + let ptr = v.as_pointer::(); + let Some(gc_header) = + (unsafe { crate::value::addr_class::try_read_tracked_gc_header(ptr as usize) }) + else { + return false; + }; + unsafe { + if (*gc_header.as_ptr()).obj_type != crate::gc::GC_TYPE_OBJECT { + return false; + } + let class_id = (*(ptr as *const ObjectHeader)).class_id; + class_id != 0 && crate::object::extends_builtin_typed_array(class_id) + } +} + /// Resolve the `IMPLICIT_THIS` receiver to a `(typed-array ptr, kind)` if it /// is a typed array, else `None`. Backs the `%TypedArray%.prototype` accessor /// getters installed for reflection (#2060) — these fire when user code does @@ -396,6 +517,11 @@ fn install_typed_array_iterator_symbol(proto_obj: *mut ObjectHeader) { iter_value, f64::from_bits(values.bits()), ); + crate::symbol::set_symbol_property_attrs( + proto_obj as usize, + iter as usize, + crate::object::PropertyAttrs::new(true, false, true), + ); } } } @@ -462,6 +588,17 @@ pub(crate) fn ensure_typed_array_intrinsic( "prototype".to_string(), super::super::PropertyAttrs::new(false, false, false), ); + let constructor_key = crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); + js_object_set_field_by_name( + proto, + constructor_key, + crate::value::js_nanbox_pointer(ctor as i64), + ); + super::super::set_builtin_property_attrs( + proto as usize, + "constructor".to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); // #2060: the four reflectable `length`/`byteLength`/`byteOffset`/`buffer` // accessor descriptors are own properties of `%TypedArray%.prototype` per // spec, NOT of the per-kind proto. Pre-#2145 they were installed on each diff --git a/crates/perry-runtime/src/object/iterator_prototypes.rs b/crates/perry-runtime/src/object/iterator_prototypes.rs index e52ca22366..dd1e82195e 100644 --- a/crates/perry-runtime/src/object/iterator_prototypes.rs +++ b/crates/perry-runtime/src/object/iterator_prototypes.rs @@ -235,6 +235,11 @@ fn install_symbol_iterator(shared: *mut ObjectHeader) { crate::value::js_nanbox_pointer(closure as i64), ); } + crate::symbol::set_symbol_property_attrs( + shared as usize, + sym as usize, + PropertyAttrs::new(true, false, true), + ); } /// Allocate one family prototype with an own `next` method (spec descriptor), @@ -308,3 +313,57 @@ pub(crate) fn attach_iterator_prototype(obj_ptr: *mut ObjectHeader, class_id: u3 } chain_to(obj_ptr, proto_ptr as *mut ObjectHeader); } + +/// Invoke a user replacement of a built-in iterator prototype's `next` +/// method. Returns `None` while the canonical native thunk is installed. +pub(crate) unsafe fn call_overridden_iterator_next( + iter_obj: *mut ObjectHeader, + class_id: u32, +) -> Option { + let scope = crate::gc::RuntimeHandleScope::new(); + let iter = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(iter_obj as i64)); + let previous = scope.root_nanbox_f64(super::js_implicit_this_get()); + ensure_iterator_prototypes(); + let (slot, canonical): (&crate::object::RealmAtomicI64, *const u8) = match class_id { + crate::array::ARRAY_ITERATOR_CLASS_ID => ( + &ARRAY_ITERATOR_PROTOTYPE_PTR, + array_iterator_next_thunk as *const u8, + ), + crate::collection_iter_object::MAP_ITERATOR_CLASS_ID => ( + &MAP_ITERATOR_PROTOTYPE_PTR, + map_iterator_next_thunk as *const u8, + ), + crate::collection_iter_object::SET_ITERATOR_CLASS_ID => ( + &SET_ITERATOR_PROTOTYPE_PTR, + set_iterator_next_thunk as *const u8, + ), + crate::string::STRING_ITERATOR_CLASS_ID => ( + &STRING_ITERATOR_PROTOTYPE_PTR, + string_iterator_next_thunk as *const u8, + ), + _ => return None, + }; + // Building the tower above may collect. Reload its realm-owned root only + // after the build rather than retaining a pre-build raw address. + let proto = scope.root_raw_const_ptr(slot.load(Ordering::Acquire) as *const ObjectHeader); + if proto.with_const_ptr::(|proto| proto.is_null()) { + return None; + } + let key = scope.root_raw_const_ptr(crate::string::js_string_from_bytes(b"next".as_ptr(), 4)); + let method = proto.with_const_ptr::(|proto| { + key.with_const_ptr::(|key| { + f64::from_bits(super::js_object_get_field_by_name(proto, key).bits()) + }) + }); + let method_ptr = + crate::value::js_nanbox_get_pointer(method) as *const crate::closure::ClosureHeader; + if !method_ptr.is_null() && crate::closure::get_valid_func_ptr(method_ptr) == canonical { + return None; + } + + let method = scope.root_nanbox_f64(method); + super::js_implicit_this_set(iter.get_nanbox_f64()); + let result = crate::closure::js_native_call_value(method.get_nanbox_f64(), std::ptr::null(), 0); + super::js_implicit_this_set(previous.get_nanbox_f64()); + Some(result) +} diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index e28f2a0b0d..28b2d034d8 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -185,7 +185,7 @@ pub(crate) use class_gc_roots::{ }; pub use class_registry::*; pub(crate) use collection_proto_thunks::{is_builtin_map_set_value, is_builtin_set_add_value}; -pub(crate) use data_view_registry::extends_builtin_data_view; +pub(crate) use data_view_registry::{extends_builtin_data_view, extends_builtin_typed_array}; pub use delete_rest::*; pub use descriptors::*; pub use exotic_expando::scan_exotic_expando_roots_mut; @@ -195,7 +195,9 @@ pub use global_this::*; pub(crate) use global_this_tables::*; pub use groupby::*; pub use instanceof::*; -pub(crate) use iterator_prototypes::{attach_iterator_prototype, iterator_prototype_for_class_id}; +pub(crate) use iterator_prototypes::{ + attach_iterator_prototype, call_overridden_iterator_next, iterator_prototype_for_class_id, +}; pub use namespace_create::*; pub use native_call_method::*; pub use native_module::*; @@ -341,6 +343,8 @@ crate::perry_thread_local! { static OS_CONSTANTS_DLOPEN_CACHE_SLOT: AtomicU64 = const { AtomicU64::new(0) }; static TYPED_ARRAY_INTRINSIC_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; static TYPED_ARRAY_INTRINSIC_PROTO_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; + static ASYNC_FUNCTION_INTRINSIC_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; + static ASYNC_FUNCTION_INTRINSIC_PROTO_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; static GENERATOR_FUNCTION_INTRINSIC_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; static GENERATOR_INTRINSIC_PROTO_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; static GENERATOR_PROTOTYPE_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; @@ -367,6 +371,10 @@ pub(crate) static TYPED_ARRAY_INTRINSIC_PTR: RealmAtomicI64 = RealmAtomicI64::new(&TYPED_ARRAY_INTRINSIC_PTR_SLOT); pub(crate) static TYPED_ARRAY_INTRINSIC_PROTO_PTR: RealmAtomicI64 = RealmAtomicI64::new(&TYPED_ARRAY_INTRINSIC_PROTO_PTR_SLOT); +pub(crate) static ASYNC_FUNCTION_INTRINSIC_PTR: RealmAtomicI64 = + RealmAtomicI64::new(&ASYNC_FUNCTION_INTRINSIC_PTR_SLOT); +pub(crate) static ASYNC_FUNCTION_INTRINSIC_PROTO_PTR: RealmAtomicI64 = + RealmAtomicI64::new(&ASYNC_FUNCTION_INTRINSIC_PROTO_PTR_SLOT); pub(crate) static GENERATOR_FUNCTION_INTRINSIC_PTR: RealmAtomicI64 = RealmAtomicI64::new(&GENERATOR_FUNCTION_INTRINSIC_PTR_SLOT); pub(crate) static GENERATOR_INTRINSIC_PROTO_PTR: RealmAtomicI64 = @@ -1186,6 +1194,8 @@ pub fn scan_object_cache_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<' for slot in [ &TYPED_ARRAY_INTRINSIC_PTR, &TYPED_ARRAY_INTRINSIC_PROTO_PTR, + &ASYNC_FUNCTION_INTRINSIC_PTR, + &ASYNC_FUNCTION_INTRINSIC_PROTO_PTR, &GENERATOR_FUNCTION_INTRINSIC_PTR, &GENERATOR_INTRINSIC_PROTO_PTR, &GENERATOR_PROTOTYPE_PTR, @@ -1200,6 +1210,7 @@ pub fn scan_object_cache_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<' }); } async_generator_queue::scan_async_generator_queue_roots_mut(visitor); + collection_proto_thunks::scan_builtin_collection_method_roots_mut(visitor); // Shared `%IteratorPrototype%`-style singletons for Array/Map/Set/String // iterator objects. Each iterator instance's `[[Prototype]]` points here, so // these must stay live for the lifetime of any iterator. @@ -1565,6 +1576,7 @@ pub(crate) fn test_realm_owned_root_snapshot() -> Vec<(&'static str, usize, u64) slot.load(Ordering::Acquire) as u64, )); } + global_this::append_async_function_root_snapshot(&mut roots); roots } diff --git a/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs index d9b39c6a5a..0a082e013d 100644 --- a/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs @@ -146,16 +146,10 @@ pub(super) unsafe fn dispatch_primitive( let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); } - let raw = crate::value::js_nanbox_get_pointer(object) as *const u8; - if !raw.is_null() && crate::object::is_valid_obj_ptr(raw) { - unsafe { - let gc = raw.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc).obj_type == crate::gc::GC_TYPE_ERROR { - let s = crate::error::js_error_to_string(raw as *mut crate::error::ErrorHeader); - return Some(f64::from_bits(JSValue::string_ptr(s).bits())); - } - } - } + // Error instances continue through ordinary method lookup. Their + // prototype's `toString` is replaceable, so hard-wiring the native + // formatter here would ignore `Error.prototype.toString = + // Object.prototype.toString`. } // Primitive-wrapper prototypes (`Number.prototype`, `Boolean.prototype`, @@ -213,7 +207,7 @@ pub(super) unsafe fn dispatch_primitive( (own.to_bits() & crate::value::POINTER_MASK) as usize, ) { - return None; + return super::call_primitive_closure_value(object, own_jsv, args_ptr, args_len); } } match method_name { diff --git a/crates/perry-runtime/src/object/object_ops/define_property.rs b/crates/perry-runtime/src/object/object_ops/define_property.rs index e247e53f98..8c5375d8c0 100644 --- a/crates/perry-runtime/src/object/object_ops/define_property.rs +++ b/crates/perry-runtime/src/object/object_ops/define_property.rs @@ -474,6 +474,268 @@ pub extern "C" fn js_object_define_property( let descriptor_value = desc_handle.get_nanbox_f64(); let key_value = key_handle.get_nanbox_f64(); + // ArrayBuffer/SharedArrayBuffer/DataView use `BufferHeader` storage, + // but are ordinary extensible objects for named properties. Keep their + // data descriptors in the existing buffer expando table instead of + // falling through and interpreting the header as an `ObjectHeader`. + let buffer_addr = crate::value::JSValue::from_bits(obj_value.to_bits()) + .is_pointer() + .then(|| crate::value::js_nanbox_get_pointer(obj_value) as usize) + .filter(|addr| crate::buffer::is_registered_buffer(*addr)); + if buffer_addr.is_some() { + let current_obj = || f64::from_bits(obj_value_handle.get_heap_word_u64()); + let current_addr = || crate::value::js_nanbox_get_pointer(current_obj()) as usize; + let current_desc = || desc_handle.get_nanbox_f64(); + let current_key = || key_handle.get_nanbox_f64(); + + // Symbol keys stay Symbols; string coercion would create an + // unrelated `"Symbol(...)"` expando that symbol lookup cannot see. + if crate::symbol::js_is_symbol(current_key()) != 0 { + let current_owner = || crate::symbol::obj_key_from_f64(current_obj()); + let current_sym = || crate::symbol::sym_key_from_f64(current_key()); + let existing_accessor_bits = + crate::symbol::symbol_accessor_descriptor_bits(current_owner(), current_sym()); + let existing_data_bits = existing_accessor_bits + .is_none() + .then(|| { + crate::symbol::symbol_property_root_bits(current_owner(), current_sym()) + }) + .flatten(); + let existing_get = + scope.root_nanbox_u64(existing_accessor_bits.map(|(get, _)| get).unwrap_or(0)); + let existing_set = + scope.root_nanbox_u64(existing_accessor_bits.map(|(_, set)| set).unwrap_or(0)); + let existing_data = scope + .root_nanbox_u64(existing_data_bits.unwrap_or(crate::value::TAG_UNDEFINED)); + let existed = existing_accessor_bits.is_some() || existing_data_bits.is_some(); + // A symbol installed by ordinary assignment has no explicit + // attrs side-table entry and therefore has the ordinary + // writable/enumerable/configurable defaults. + let existing_attrs = existed.then(|| { + crate::symbol::get_symbol_property_attrs(current_owner(), current_sym()) + .unwrap_or(PropertyAttrs::new(true, true, true)) + }); + if let Some(attrs) = existing_attrs { + if !attrs.configurable() { + validate_nonconfigurable_redefine( + "symbol", + attrs, + existing_accessor_bits.map(|_| super::super::AccessorDescriptor { + get: existing_get.get_nanbox_u64(), + set: existing_set.get_nanbox_u64(), + }), + existing_data.get_nanbox_f64(), + current_desc(), + desc_view.as_ref(), + ); + } + } + + let has_get = desc_has_field(current_desc(), b"get"); + let has_set = desc_has_field(current_desc(), b"set"); + let has_value = desc_has_field(current_desc(), b"value"); + let has_writable = desc_has_field(current_desc(), b"writable"); + if has_get || has_set { + let get = scope.root_nanbox_u64(if has_get { + let field = desc_read_field(current_desc(), b"get"); + (!field.is_undefined()) + .then(|| { + crate::closure::clone_closure_rebind_this( + field.bits(), + current_obj(), + ) + }) + .unwrap_or(0) + } else { + existing_get.get_nanbox_u64() + }); + let set = if has_set { + let field = desc_read_field(current_desc(), b"set"); + (!field.is_undefined()) + .then(|| { + crate::closure::clone_closure_rebind_this( + field.bits(), + current_obj(), + ) + }) + .unwrap_or(0) + } else { + existing_set.get_nanbox_u64() + }; + crate::symbol::set_symbol_accessor_property( + current_obj(), + current_key(), + get.get_nanbox_u64(), + set, + ); + } else if has_value || has_writable || !existed { + let value = if has_value { + f64::from_bits(desc_read_field(current_desc(), b"value").bits()) + } else if existing_accessor_bits.is_some() || existing_data_bits.is_none() { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + existing_data.get_nanbox_f64() + }; + crate::symbol::define_symbol_data_property(current_obj(), current_key(), value); + } + let read_flag = |name: &[u8]| -> Option { + desc_has_field(current_desc(), name).then(|| { + crate::value::js_is_truthy(f64::from_bits( + desc_read_field(current_desc(), name).bits(), + )) != 0 + }) + }; + crate::symbol::set_symbol_property_attrs( + current_owner(), + current_sym(), + PropertyAttrs::new( + if has_get || has_set { + false + } else { + read_flag(b"writable").unwrap_or_else(|| { + existing_attrs + .map(|attrs| attrs.writable()) + .unwrap_or(false) + }) + }, + read_flag(b"enumerable").unwrap_or_else(|| { + existing_attrs + .map(|attrs| attrs.enumerable()) + .unwrap_or(false) + }), + read_flag(b"configurable").unwrap_or_else(|| { + existing_attrs + .map(|attrs| attrs.configurable()) + .unwrap_or(false) + }), + ), + ); + return current_obj(); + } + + if let Some(name) = super::super::metadata_key_to_string(current_key()) { + // Key coercion and descriptor getters may collect. Every side + // table access below therefore re-derives the buffer owner from + // the rooted receiver instead of retaining `buffer_addr`. + let addr = current_addr(); + let existing_accessor = super::super::get_accessor_descriptor(addr, &name); + let existing_data = crate::buffer::buffer_get_own_prop(addr, &name); + let existing_get = scope + .root_nanbox_u64(existing_accessor.map(|accessor| accessor.get).unwrap_or(0)); + let existing_set = scope + .root_nanbox_u64(existing_accessor.map(|accessor| accessor.set).unwrap_or(0)); + let existing_data_value = scope.root_nanbox_f64( + existing_data.unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)), + ); + let existing_attrs = + (existing_accessor.is_some() || existing_data.is_some()).then(|| { + super::super::get_property_attrs(addr, &name) + .unwrap_or(PropertyAttrs::new(true, true, true)) + }); + if let Some(attrs) = existing_attrs { + if !attrs.configurable() { + validate_nonconfigurable_redefine( + &name, + attrs, + existing_accessor, + existing_data_value.get_nanbox_f64(), + current_desc(), + desc_view.as_ref(), + ); + } + } + + let has_get = desc_has_field(current_desc(), b"get"); + let has_set = desc_has_field(current_desc(), b"set"); + let has_value = desc_has_field(current_desc(), b"value"); + let has_writable = desc_has_field(current_desc(), b"writable"); + if has_get || has_set { + let get_field = + scope.root_nanbox_u64(desc_read_field(current_desc(), b"get").bits()); + let set_field = + scope.root_nanbox_u64(desc_read_field(current_desc(), b"set").bits()); + let get_bits = scope.root_nanbox_u64( + if has_get && get_field.get_nanbox_u64() != crate::value::TAG_UNDEFINED { + crate::closure::clone_closure_rebind_this( + get_field.get_nanbox_u64(), + current_obj(), + ) + } else if !has_get { + existing_get.get_nanbox_u64() + } else { + 0 + }, + ); + let set_bits = + if has_set && set_field.get_nanbox_u64() != crate::value::TAG_UNDEFINED { + crate::closure::clone_closure_rebind_this( + set_field.get_nanbox_u64(), + current_obj(), + ) + } else if !has_set { + existing_set.get_nanbox_u64() + } else { + 0 + }; + let addr = current_addr(); + super::super::set_accessor_descriptor( + addr, + name.clone(), + super::super::AccessorDescriptor { + get: get_bits.get_nanbox_u64(), + set: set_bits, + }, + ); + // Keep an order/enumeration placeholder; accessor-aware + // reads ignore its undefined payload. + crate::buffer::buffer_define_own_data_prop( + addr, + &name, + f64::from_bits(crate::value::TAG_UNDEFINED), + ); + } else if has_value || has_writable || existing_attrs.is_none() { + // Data descriptor (or a brand-new generic descriptor). + let addr = current_addr(); + super::super::clear_accessor_descriptor(addr, &name); + let value = if has_value { + f64::from_bits(desc_read_field(current_desc(), b"value").bits()) + } else if existing_accessor.is_some() || existing_data.is_none() { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + existing_data_value.get_nanbox_f64() + }; + crate::buffer::buffer_define_own_data_prop(current_addr(), &name, value); + } + + let read_flag = |field: &[u8]| -> Option { + desc_has_field(current_desc(), field).then(|| { + crate::value::js_is_truthy(f64::from_bits( + desc_read_field(current_desc(), field).bits(), + )) != 0 + }) + }; + let attrs = PropertyAttrs::new( + read_flag(b"writable").unwrap_or_else(|| { + existing_attrs + .map(|attrs| attrs.writable()) + .unwrap_or(false) + }), + read_flag(b"enumerable").unwrap_or_else(|| { + existing_attrs + .map(|attrs| attrs.enumerable()) + .unwrap_or(false) + }), + read_flag(b"configurable").unwrap_or_else(|| { + existing_attrs + .map(|attrs| attrs.configurable()) + .unwrap_or(false) + }), + ); + super::super::set_property_attrs(current_addr(), name, attrs); + } + return current_obj(); + } + // Date / RegExp / Error instances are exotic cells, not // `ObjectHeader`s — the ordinary define path below would bit-cast // them and corrupt memory. Route through the expando-aware diff --git a/crates/perry-runtime/src/object/object_ops/prototype.rs b/crates/perry-runtime/src/object/object_ops/prototype.rs index 7a0f09f89c..3ed6d5fdee 100644 --- a/crates/perry-runtime/src/object/object_ops/prototype.rs +++ b/crates/perry-runtime/src/object/object_ops/prototype.rs @@ -87,6 +87,20 @@ pub extern "C" fn js_object_create(proto_value: f64) -> f64 { return f64::from_bits((obj as u64) | POINTER_TAG); } + // Integer-indexed exotic objects are valid prototypes even though their + // TypedArrayHeader cannot be modeled as an ObjectHeader-backed synthetic + // class prototype. Preserve the exact object identity in the ordinary + // per-instance prototype side table so its [[Set]] intercepts canonical + // numeric keys on descendants. + if crate::typedarray_props::typed_array_addr_from_value(proto_value).is_some() { + let obj = js_object_alloc(0, 0); + crate::object::prototype_chain::object_set_static_prototype( + obj as usize, + proto_value.to_bits(), + ); + return f64::from_bits((obj as u64) | POINTER_TAG); + } + let mut class_id: u32 = 0; let proto_bits = proto_value.to_bits(); if (proto_bits & 0xFFFF_0000_0000_0000) == POINTER_TAG { diff --git a/crates/perry-runtime/src/object/polymorphic_index.rs b/crates/perry-runtime/src/object/polymorphic_index.rs index 2f483c6f76..ff14715c1d 100644 --- a/crates/perry-runtime/src/object/polymorphic_index.rs +++ b/crates/perry-runtime/src/object/polymorphic_index.rs @@ -177,6 +177,12 @@ pub extern "C" fn js_object_get_index_polymorphic(obj_handle: i64, idx: f64) -> if raw < 0x1000 { return f64::from_bits(crate::value::TAG_UNDEFINED); } + // Symbols share GC_TYPE_STRING storage for tracing, but they are primitive + // values, not String exotic objects. A numeric property access boxes the + // Symbol transiently and therefore observes no indexed property. + if crate::symbol::is_registered_symbol(raw as usize) { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } // #5525 fast path: cached typed-array kind lookup + inline load, ahead of // the thread-local `typed_array_get_numeric_index` registry dispatch. // `typed_array_fast_index_get` returns `Some(value)` for an in-bounds read diff --git a/crates/perry-runtime/src/object/primitive_proto_thunks.rs b/crates/perry-runtime/src/object/primitive_proto_thunks.rs index a39746e24b..280b0c552c 100644 --- a/crates/perry-runtime/src/object/primitive_proto_thunks.rs +++ b/crates/perry-runtime/src/object/primitive_proto_thunks.rs @@ -87,12 +87,19 @@ pub(super) fn install_primitive_proto_methods( ); } "BigInt" => { - ipm( + let to_string = ipm( proto_obj, "toString", bigint_proto_to_string_thunk as *const u8, 1, ); + // The optional radix does not contribute to the observable + // function length, but the native thunk still needs one ABI slot + // so a missing argument arrives as undefined rather than garbage. + super::native_module::set_builtin_closure_length( + crate::value::js_nanbox_get_pointer(to_string) as usize, + 0, + ); ipm( proto_obj, "valueOf", @@ -133,11 +140,14 @@ pub(crate) fn primitive_proto_method_value(builtin_name: &str, method_name: &str ("BigInt", "valueOf") => (bigint_proto_value_of_thunk as *const u8, 0), _ => return None, }; - Some(primitive_proto_method_closure_value( - method_name, - func_ptr, - arity, - )) + let value = primitive_proto_method_closure_value(method_name, func_ptr, arity); + if builtin_name == "BigInt" && method_name == "toString" { + super::native_module::set_builtin_closure_length( + crate::value::js_nanbox_get_pointer(value) as usize, + 0, + ); + } + Some(value) } fn primitive_proto_method_closure_value(method_name: &str, func_ptr: *const u8, arity: u32) -> f64 { diff --git a/crates/perry-runtime/src/object/prototype_chain.rs b/crates/perry-runtime/src/object/prototype_chain.rs index 63b0b37244..253d9e486f 100644 --- a/crates/perry-runtime/src/object/prototype_chain.rs +++ b/crates/perry-runtime/src/object/prototype_chain.rs @@ -139,6 +139,10 @@ fn get_object_prototypes() -> &'static Mutex> { /// always on exactly one of the two storages. pub(crate) unsafe fn meta_capable_object(obj_ptr: usize) -> Option<*mut crate::ObjectHeader> { if !crate::value::addr_class::is_above_handle_band(obj_ptr) + // ArrayBuffer / SharedArrayBuffer / DataView use BufferHeader storage. + // Some of those headers pass the legacy ObjectHeader validity probe, + // but they do not have an ObjectMeta slot at the ObjectHeader offset. + || crate::buffer::is_registered_buffer(obj_ptr) || !crate::object::is_valid_obj_ptr(obj_ptr as *const u8) { return None; diff --git a/crates/perry-runtime/src/promise/async_step.rs b/crates/perry-runtime/src/promise/async_step.rs index 5175cd8295..510cf31bee 100644 --- a/crates/perry-runtime/src/promise/async_step.rs +++ b/crates/perry-runtime/src/promise/async_step.rs @@ -184,6 +184,9 @@ pub extern "C" fn js_promise_resolved(value: f64) -> *mut Promise { } return promise; } + let scope = crate::gc::RuntimeHandleScope::new(); + let value_h = scope.root_nanbox_f64(value); + // Issue #2823: `Promise.resolve(p)` MUST return `p` itself when `p` is // already a native Promise (constructor === Promise). The spec defines // Promise.resolve to short-circuit and return the argument unchanged in @@ -192,10 +195,36 @@ pub extern "C" fn js_promise_resolved(value: f64) -> *mut Promise { // `Promise` instances, so a GC_TYPE_PROMISE value always satisfies the // "constructor is Promise" check. Return the existing pointer directly // instead of allocating a fresh wrapper and chaining to it. - if js_value_is_promise(value) != 0 { - let inner = crate::value::js_nanbox_get_pointer(value) as *mut Promise; + if js_value_is_promise(value_h.get_nanbox_f64()) != 0 { + let inner = crate::value::js_nanbox_get_pointer(value_h.get_nanbox_f64()) as *mut Promise; if !inner.is_null() { - return inner; + // PromiseResolve(%Promise%, x) performs Get(x, "constructor") + // before its identity short-cut. An own accessor can throw, and + // an own non-intrinsic constructor prevents returning x itself. + let addr = inner as usize; + let same_intrinsic_constructor = if super::promise_has_own_constructor(addr) { + let ctor = unsafe { + crate::object::exotic_expando::exotic_get_own_property( + addr, + crate::object::exotic_expando::ExoticKind::Promise, + "constructor", + value_h.get_nanbox_f64(), + ) + } + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); + let ctor_h = scope.root_nanbox_f64(ctor); + let intrinsic = crate::object::js_get_global_this_builtin_value( + b"Promise".as_ptr(), + b"Promise".len(), + ); + ctor_h.get_nanbox_f64().to_bits() == intrinsic.to_bits() + } else { + true + }; + if same_intrinsic_constructor { + return crate::value::js_nanbox_get_pointer(value_h.get_nanbox_f64()) + as *mut Promise; + } } } let promise = js_promise_new(); @@ -213,10 +242,27 @@ pub extern "C" fn js_promise_resolved(value: f64) -> *mut Promise { // steady state is untouched; only real thenables (drizzle's `QueryPromise`, // object literals with `then`) defer by one microtask — which the await // loop drains, leaving the resolved value identical. - super::assimilate::promise_resolve_assimilating(promise, value); + super::assimilate::promise_resolve_assimilating(promise, value_h.get_nanbox_f64()); promise } +/// Run the spec PromiseResolve path behind an exception boundary. Async +/// iterator/generator algorithms turn an abrupt constructor getter into a +/// rejected result promise instead of throwing synchronously to their caller. +pub fn js_promise_resolved_catching(value: f64) -> Result<*mut Promise, f64> { + let trap_buf = crate::exception::js_try_push(); + let jumped = unsafe { crate::ffi::setjmp::setjmp(trap_buf as *mut std::os::raw::c_int) }; + let result = if jumped == 0 { + Ok(js_promise_resolved(value)) + } else { + let reason = crate::exception::js_get_exception(); + crate::exception::js_clear_exception(); + Err(reason) + }; + crate::exception::js_try_end(); + result +} + /// Fused fast path for `Promise.resolve(value).then(cb_f, cb_e)` — /// the steady-state shape of the async-to-generator transform's /// per-`await` lowering. The naive sequence is: diff --git a/crates/perry-runtime/src/promise/mod.rs b/crates/perry-runtime/src/promise/mod.rs index 8d10844efe..6782f06af6 100644 --- a/crates/perry-runtime/src/promise/mod.rs +++ b/crates/perry-runtime/src/promise/mod.rs @@ -39,8 +39,8 @@ pub(crate) mod then_probe; pub use async_step::{ js_array_from_async, js_async_first_call, js_async_step_chain, js_async_step_done, - js_get_current_step_closure, js_promise_resolved, js_promise_resolved_then, - scan_async_step_thunk_cache, scan_async_step_thunk_cache_mut, + js_get_current_step_closure, js_promise_resolved, js_promise_resolved_catching, + js_promise_resolved_then, scan_async_step_thunk_cache, scan_async_step_thunk_cache_mut, }; pub use checked_dispatch::{ js_promise_catch_checked, js_promise_closure_arg, js_promise_finally_checked, diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index 465bdd9aec..4f580fa8d3 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -756,6 +756,20 @@ fn reflect_value_is_object(value: f64) -> bool { return true; } let bits = value.to_bits(); + // BufferHeader-backed values are objects even when they are not GC heap + // allocations. In particular, SharedArrayBuffer uses a process-global, + // never-moved backing from `alloc_zeroed`; depending on the surrounding + // allocation history its address can fail the generic heap-address test + // below. Consult the authoritative registries before that heuristic so + // receiver-aware Set reaches CreateDataProperty and records named + // expandos such as an own `constructor`. + let raw = extract_pointer(bits) as usize; + if raw != 0 + && (crate::buffer::is_registered_buffer(raw) + || crate::typedarray::lookup_typed_array_kind(raw).is_some()) + { + return true; + } let top16 = bits >> 48; if top16 == (POINTER_TAG >> 48) { let lower48 = bits & POINTER_MASK; @@ -1554,6 +1568,63 @@ fn call_setter_with_receiver(setter_bits: u64, receiver: f64, value: f64) -> boo true } +/// Ordinary named-property Set for the BufferHeader-backed objects that are +/// not integer-indexed exotics (ArrayBuffer, SharedArrayBuffer, DataView). +/// +/// These values have no ObjectHeader/GcHeader. The generic Set tail eventually +/// asks whether the receiver is a GC heap object before CreateDataProperty; +/// a process-global SharedArrayBuffer can therefore be rejected even though +/// the buffer registries authoritatively classify it as an Object. Walk the +/// descriptors normally, but materialize the final own data property in the +/// buffer expando table instead of the ObjectHeader store. +fn set_nonindexed_buffer_named_self(target: f64, key: f64, value: f64) -> Option { + if unsafe { crate::symbol::js_is_symbol(key) } != 0 { + return None; + } + let raw = raw_ptr_from_value(target)?; + if !crate::buffer::is_non_indexed_buffer_view(raw) { + return None; + } + let name = key_to_rust_string(key)?; + + // Existing own accessor/data descriptors take the same precedence as in + // OrdinarySetWithOwnDescriptor. Buffer expandos live in side tables, so + // inspect those tables without treating the BufferHeader as ObjectHeader. + if let Some(accessor) = crate::object::get_accessor_descriptor(raw, &name) { + return Some(call_setter_with_receiver(accessor.set, target, value)); + } + if crate::buffer::buffer_has_own_prop(raw, &name) { + if crate::object::get_property_attrs(raw, &name).is_some_and(|attrs| !attrs.writable()) { + return Some(false); + } + crate::buffer::buffer_set_own_prop(raw, &name, value); + return Some(true); + } + + // A descriptor on the prototype can reject the write or invoke a setter. + // A writable inherited data descriptor (including the standard + // `.constructor`) creates an own property on the original receiver. + let mut current = prototype_of_for_set(target); + for _ in 0..64 { + let Some(proto) = current else { + break; + }; + if let Some(desc) = own_set_descriptor(proto, key) { + match desc { + OwnSetDescriptor::Data { writable: false } => return Some(false), + OwnSetDescriptor::Data { writable: true } => break, + OwnSetDescriptor::Accessor { setter_bits } => { + return Some(call_setter_with_receiver(setter_bits, target, value)); + } + } + } + current = prototype_of_for_set(proto); + } + + crate::buffer::buffer_set_own_prop(raw, &name, value); + Some(true) +} + /// #5129: build a fresh data property descriptor /// `{ value, writable: true, enumerable: true, configurable: true }` /// (the CreateDataProperty shape) for defining a property on a Proxy receiver diff --git a/crates/perry-runtime/src/proxy/put_value.rs b/crates/perry-runtime/src/proxy/put_value.rs index 932d227f37..a0ce216b1d 100644 --- a/crates/perry-runtime/src/proxy/put_value.rs +++ b/crates/perry-runtime/src/proxy/put_value.rs @@ -185,6 +185,16 @@ pub extern "C" fn js_put_value_set( if set_integer_indexed_exotic(target, property_key, value) { return value; } + if target.to_bits() == receiver.to_bits() { + if let Some(stored) = set_nonindexed_buffer_named_self(target, property_key, value) { + if !stored && strict != 0 { + let key_name = + key_to_rust_string(property_key).unwrap_or_else(|| "property".to_string()); + crate::error::throw_immutable_write(0, &key_name); + } + return value; + } + } // Integer-Indexed exotic objects: a key that is *not* a CanonicalNumeric // index does OrdinarySet, creating/looking-up a normal own property on // the typed array (ECMA-262 §10.4.5.5). The generic diff --git a/crates/perry-runtime/src/set.rs b/crates/perry-runtime/src/set.rs index 868e81b3a6..8d381f900c 100644 --- a/crates/perry-runtime/src/set.rs +++ b/crates/perry-runtime/src/set.rs @@ -1495,18 +1495,27 @@ pub extern "C" fn js_set_from_array(arr: *const crate::array::ArrayHeader) -> *m pub extern "C" fn js_set_from_iterable(value: f64) -> *mut SetHeader { use crate::collection_iter::{constructor_iter, ConstructorIter}; + if crate::collection_iter::is_null_or_undefined(value) { + return js_set_alloc(4); + } + let scope = crate::gc::RuntimeHandleScope::new(); let value_handle = scope.root_nanbox_f64(value); + let set = js_set_alloc(4); + let set_handle = scope.root_raw_mut_ptr(set); let adder = crate::collection_iter::require_callable( - crate::collection_iter::builtin_prototype_method("Set", "add"), + set_handle.with_mut_ptr::(|set| { + crate::collection_iter::builtin_prototype_adder( + "Set", + "add", + crate::value::js_nanbox_pointer(set as i64), + ) + }), "Set.prototype.add", ); let adder = crate::collection_iter::normalize_callable_value(adder); let adder_handle = scope.root_nanbox_f64(adder); - let set = js_set_alloc(4); - let set_handle = scope.root_raw_mut_ptr(set); - let add_value = |element: f64, iter_to_close: Option| { let args = [element]; let adder = adder_handle.get_nanbox_f64(); diff --git a/crates/perry-runtime/src/string/iter_object.rs b/crates/perry-runtime/src/string/iter_object.rs index 197321b6c2..8916059879 100644 --- a/crates/perry-runtime/src/string/iter_object.rs +++ b/crates/perry-runtime/src/string/iter_object.rs @@ -36,16 +36,25 @@ use crate::StringHeader; pub const STRING_ITERATOR_CLASS_ID: u32 = 0xFFFF_0009; unsafe fn alloc_iterator(cp_array: *mut ArrayHeader) -> f64 { - let obj = js_object_alloc(STRING_ITERATOR_CLASS_ID, 2); + let scope = crate::gc::RuntimeHandleScope::new(); + let cp_h = scope.root_raw_mut_ptr(cp_array); + let obj_h = scope.root_raw_mut_ptr(js_object_alloc(STRING_ITERATOR_CLASS_ID, 2)); // Field 0: backing codepoint array (NaN-boxed pointer for the GC scanner). - js_object_set_field( - obj, - 0, - JSValue::from_bits(js_nanbox_pointer(cp_array as i64).to_bits()), - ); + obj_h.with_mut_ptr(|obj| { + cp_h.with_mut_ptr::(|cp| { + js_object_set_field( + obj, + 0, + JSValue::from_bits(js_nanbox_pointer(cp as i64).to_bits()), + ) + }) + }); // Field 1: cursor index, starts at 0. - js_object_set_field(obj, 1, JSValue::number(0.0)); - crate::object::attach_iterator_prototype(obj, STRING_ITERATOR_CLASS_ID); + obj_h.with_mut_ptr(|obj| js_object_set_field(obj, 1, JSValue::number(0.0))); + obj_h.with_mut_ptr(|obj| { + crate::object::attach_iterator_prototype(obj, STRING_ITERATOR_CLASS_ID) + }); + let (_, obj) = obj_h.across_mut::(|| ()); js_nanbox_pointer(obj as i64) } @@ -56,7 +65,11 @@ pub fn string_values_iter(s: *const StringHeader) -> f64 { return f64::from_bits(TAG_UNDEFINED); } unsafe { - let cp_array = crate::array::js_array_from_string_codepoints(s); + // Use the bounded WTF-8 iterator shared with string spread. A JS + // string may contain lone surrogates, so `str::chars()`-based + // materialization would reject the entire payload and produce an + // empty iterator. + let cp_array = crate::string::js_string_to_char_array(s as i64) as *mut ArrayHeader; alloc_iterator(cp_array) } } @@ -71,24 +84,33 @@ pub unsafe fn dispatch_string_iterator_method( iter_obj: *mut ObjectHeader, method_name: &str, ) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let iter_h = scope.root_nanbox_f64(js_nanbox_pointer(iter_obj as i64)); + let iter_obj = || js_nanbox_get_pointer(iter_h.get_nanbox_f64()) as *mut ObjectHeader; match method_name { "next" => { - let backing = f64::from_bits(js_object_get_field(iter_obj, 0).bits()); - let arr = js_nanbox_get_pointer(backing) as *const ArrayHeader; - let idx = f64::from_bits(js_object_get_field(iter_obj, 1).bits()) as u32; - let len = if arr.is_null() { + if let Some(result) = + crate::object::call_overridden_iterator_next(iter_obj(), STRING_ITERATOR_CLASS_ID) + { + return result; + } + let backing = f64::from_bits(js_object_get_field(iter_obj(), 0).bits()); + let arr_h = scope.root_nanbox_f64(backing); + let arr = || js_nanbox_get_pointer(arr_h.get_nanbox_f64()) as *const ArrayHeader; + let idx = f64::from_bits(js_object_get_field(iter_obj(), 1).bits()) as u32; + let len = if arr().is_null() { 0 } else { - crate::array::js_array_length(arr) + crate::array::js_array_length(arr()) }; if idx >= len { return make_iter_result(JSValue::undefined(), true); } - js_object_set_field(iter_obj, 1, JSValue::number((idx + 1) as f64)); - let elem = crate::array::js_array_get_f64(arr, idx); + js_object_set_field(iter_obj(), 1, JSValue::number((idx + 1) as f64)); + let elem = crate::array::js_array_get_f64(arr(), idx); make_iter_result(JSValue::from_bits(elem.to_bits()), false) } - "Symbol.iterator" | "@@iterator" => js_nanbox_pointer(iter_obj as i64), + "Symbol.iterator" | "@@iterator" => js_nanbox_pointer(iter_obj() as i64), "return" | "throw" => make_iter_result(JSValue::undefined(), true), _ => f64::from_bits(TAG_UNDEFINED), } diff --git a/crates/perry-runtime/src/symbol.rs b/crates/perry-runtime/src/symbol.rs index d184854302..dffcb0ddd2 100644 --- a/crates/perry-runtime/src/symbol.rs +++ b/crates/perry-runtime/src/symbol.rs @@ -42,10 +42,11 @@ pub use constructors::{ // Symbol-keyed property side-table operations. pub(crate) use properties::{ class_static_symbol_keys_for_class, clone_symbol_entries_for_obj_ptr, - get_symbol_property_attrs, inspect_custom_symbol_ptr, js_object_define_symbol_accessor, - js_object_delete_symbol_property, js_object_has_own_symbol_property, - reflect_symbol_getter_closure_bits, set_symbol_property_attrs, symbol_accessor_descriptor_bits, - symbol_property_is_enumerable, symbol_property_is_non_writable, symbol_property_root_bits, + define_symbol_data_property, get_symbol_property_attrs, inspect_custom_symbol_ptr, + js_object_define_symbol_accessor, js_object_delete_symbol_property, + js_object_has_own_symbol_property, reflect_symbol_getter_closure_bits, + set_symbol_property_attrs, symbol_accessor_descriptor_bits, symbol_property_is_enumerable, + symbol_property_is_non_writable, symbol_property_root_bits, }; pub use properties::{ class_static_symbol_lookup, js_class_register_static_symbol, js_object_has_own_symbol, diff --git a/crates/perry-runtime/src/symbol/iterator.rs b/crates/perry-runtime/src/symbol/iterator.rs index 54809d809f..2daa02c30c 100644 --- a/crates/perry-runtime/src/symbol/iterator.rs +++ b/crates/perry-runtime/src/symbol/iterator.rs @@ -243,11 +243,7 @@ pub extern "C" fn js_get_iterator(val_f64: f64) -> f64 { if jsv.is_pointer() { let ptr = jsv.as_pointer::(); if crate::object::is_arguments_object(ptr) { - if let Some(arr) = unsafe { crate::object::arguments_object_to_array(ptr) } { - let arr_f64 = - f64::from_bits(crate::value::JSValue::pointer(arr as *const u8).bits()); - return crate::array::array_values_iter(arr_f64); - } + return crate::array::arguments_values_iter(ptr); } } } diff --git a/crates/perry-runtime/src/symbol/properties.rs b/crates/perry-runtime/src/symbol/properties.rs index 32c967cb58..b2dbfac3b1 100644 --- a/crates/perry-runtime/src/symbol/properties.rs +++ b/crates/perry-runtime/src/symbol/properties.rs @@ -58,6 +58,20 @@ pub(crate) fn symbol_property_root_bits(owner: usize, sym_key: usize) -> Option< }) } +/// Install a symbol-keyed data descriptor after the caller has completed +/// `ValidateAndApplyPropertyDescriptor`. Unlike ordinary `[[Set]]`, this must +/// not be intercepted by the property's previous writable flag or setter. +pub(crate) unsafe fn define_symbol_data_property(obj_f64: f64, sym_f64: f64, value_f64: f64) { + let obj_key = obj_key_from_f64(obj_f64); + let sym_key = sym_key_from_f64(sym_f64); + if obj_key == 0 || sym_key == 0 { + return; + } + super::note_symbol_key_installed(sym_key); + accessors::clear_symbol_accessor_property(obj_key, sym_key); + store_object_symbol_property_root(obj_key, sym_key, value_f64.to_bits()); +} + pub(crate) fn get_symbol_property_attrs( owner: usize, sym_key: usize, diff --git a/crates/perry-runtime/src/typedarray/bigint.rs b/crates/perry-runtime/src/typedarray/bigint.rs index 751418bbd5..e46ad1cef4 100644 --- a/crates/perry-runtime/src/typedarray/bigint.rs +++ b/crates/perry-runtime/src/typedarray/bigint.rs @@ -11,8 +11,7 @@ //! for the construction / `set()` paths. use super::{ - jsvalue_to_f64, store_at, throw_type_error, typed_array_alloc, TypedArrayHeader, KIND_BIGINT64, - KIND_BIGUINT64, + store_at, throw_type_error, typed_array_alloc, TypedArrayHeader, KIND_BIGINT64, KIND_BIGUINT64, }; pub(crate) fn is_bigint_kind(kind: u8) -> bool { @@ -90,7 +89,10 @@ pub(crate) fn coerce_for_kind(dst_kind: u8, raw: f64) -> f64 { if dst_kind == KIND_BIGINT64 || dst_kind == KIND_BIGUINT64 { to_bigint_for_store(raw) } else { - jsvalue_to_f64(raw) + // Typed-array element conversion is the full observable ToNumber, + // not the primitive-only numeric unboxer. In particular an object + // source element must run its @@toPrimitive/valueOf hooks. + crate::builtins::js_number_coerce(raw) } } diff --git a/crates/perry-runtime/src/typedarray/construct.rs b/crates/perry-runtime/src/typedarray/construct.rs index cba70e3c7c..85120ebe08 100644 --- a/crates/perry-runtime/src/typedarray/construct.rs +++ b/crates/perry-runtime/src/typedarray/construct.rs @@ -307,15 +307,25 @@ pub(crate) unsafe fn typed_array_from_source_raw_values(val: f64) -> Vec { /// Coerce a snapshot of raw element values per `kind` (observable, may throw) /// and store them into a freshly allocated typed array. unsafe fn typed_array_from_snapshot(kind: u8, raw: Vec) -> *mut TypedArrayHeader { - let vals: Vec = raw - .into_iter() - .map(|v| bigint::coerce_for_kind(kind, v)) - .collect(); - let ta = typed_array_alloc(kind, vals.len() as u32); - for (i, v) in vals.iter().enumerate() { - store_at(ta, i, *v); + let scope = crate::gc::RuntimeHandleScope::new(); + let rooted = scope.root_nanbox_f64_slice(&raw); + typed_array_from_rooted_snapshot(kind, &rooted) +} + +/// Root-preserving sibling used while an iterator is still being driven: every +/// value yielded so far remains live across later `next()` calls and across +/// each observable numeric/BigInt coercion. +unsafe fn typed_array_from_rooted_snapshot( + kind: u8, + raw: &[crate::gc::RuntimeHandle<'_>], +) -> *mut TypedArrayHeader { + let scope = crate::gc::RuntimeHandleScope::new(); + let ta = scope.root_raw_mut_ptr(typed_array_alloc(kind, raw.len() as u32)); + for (i, value) in raw.iter().enumerate() { + let coerced = bigint::coerce_for_kind(kind, value.get_nanbox_f64()); + ta.with_mut_ptr::(|ta| store_at(ta, i, coerced)); } - ta + ta.across_mut::(|| ()).1 } /// `Get(obj, name)` for a plain-object or function source value. @@ -407,27 +417,21 @@ pub extern "C" fn js_typed_array_new_from_array( return typed_array_alloc(kind, 0); } rooted.set_raw_const_ptr(arr); - unsafe { - let len = (*arr).length; - // Snapshot the raw source values BEFORE any coercion. Per spec the - // source list is fully collected first and only THEN are the elements - // converted (`ToNumber`/`ToBigInt`) and stored. A converting element can - // run user code (`valueOf`/`Symbol.toPrimitive`) that mutates the source - // array — `Int32Array.from([0, { valueOf() { src.length = 0; return 100 }}, 2])` - // must still yield `[0, 100, 2]`, not lose the trailing element. Reading - // raw values first also keeps the snapshot ahead of the `typed_array_alloc` - // GC point (#871). - let raw: Vec = (0..len) - .map(|i| crate::array::js_array_get_f64(rooted.get_raw_const_ptr::(), i)) - .collect(); - let vals: Vec = raw - .into_iter() - .map(|v| bigint::coerce_for_kind(kind, v)) - .collect(); - let ta = typed_array_alloc(kind, len); - for (i, v) in vals.iter().enumerate() { - store_at(ta, i, *v); - } - ta + // InitializeTypedArrayFromArrayLike obtains and drives the source's + // iterator. Going through the real iterator protocol matters even for a + // dense Array: user code can replace Array.prototype[Symbol.iterator] or + // %ArrayIteratorPrototype%.next, and construction must observe either. + // Collect the raw values first and only then coerce them, retaining the + // mutation/snapshot rule described by `typed_array_from_snapshot`. + let source = rooted + .with_const_ptr::(|source| crate::value::js_nanbox_pointer(source as i64)); + let iter = crate::symbol::js_get_iterator(source); + let iter_rooted = scope.root_nanbox_f64(iter); + let mut raw = Vec::new(); + while let Some(value) = + crate::collection_iter::iterator_next_value(iter_rooted.get_nanbox_f64()) + { + raw.push(scope.root_nanbox_f64(value)); } + unsafe { typed_array_from_rooted_snapshot(kind, &raw) } } diff --git a/crates/perry-runtime/src/typedarray_props.rs b/crates/perry-runtime/src/typedarray_props.rs index 90630e12f6..26d3a44117 100644 --- a/crates/perry-runtime/src/typedarray_props.rs +++ b/crates/perry-runtime/src/typedarray_props.rs @@ -427,6 +427,13 @@ pub(crate) unsafe fn typed_array_define_own_property( "Cannot define property {key_name}, object is not extensible" )); } + let existing = typed_array_has_ordinary_own_prop(owner, key_name); + let current_attrs = existing + .then(|| crate::object::get_property_attrs(owner, key_name)) + .flatten() + .unwrap_or(crate::object::PropertyAttrs::new( + existing, existing, existing, + )); let has_get = descriptor_has(desc_ptr, b"get"); let has_set = descriptor_has(desc_ptr, b"set"); let has_accessor = has_get || has_set; @@ -467,9 +474,15 @@ pub(crate) unsafe fn typed_array_define_own_property( }; upsert_typed_array_own_prop(owner, key_name.to_string(), value, true); } - let writable = descriptor_bool(desc_ptr, b"writable").unwrap_or(has_accessor); - let enumerable = descriptor_bool(desc_ptr, b"enumerable").unwrap_or(false); - let configurable = descriptor_bool(desc_ptr, b"configurable").unwrap_or(false); + let writable = if has_accessor { + false + } else { + descriptor_bool(desc_ptr, b"writable").unwrap_or(current_attrs.writable()) + }; + let enumerable = + descriptor_bool(desc_ptr, b"enumerable").unwrap_or(current_attrs.enumerable()); + let configurable = + descriptor_bool(desc_ptr, b"configurable").unwrap_or(current_attrs.configurable()); crate::object::set_property_attrs( owner, key_name.to_string(), @@ -1077,6 +1090,7 @@ pub(crate) fn typed_array_own_set_descriptor( /// `Some(primitive)` when a patched method produced a non-object; `None` when /// no own patch applies (caller falls back to its default coercion). pub(crate) unsafe fn typed_array_own_to_primitive_number(owner: usize, value: f64) -> Option { + let mut non_primitive_calls = 0u8; for name in ["valueOf", "toString"] { let Some(m) = typed_array_get_property_value_by_name(owner, name) else { continue; @@ -1096,6 +1110,13 @@ pub(crate) unsafe fn typed_array_own_to_primitive_number(owner: usize, value: f6 if !is_object { return Some(r); } + non_primitive_calls += 1; + } + if non_primitive_calls >= 2 { + let msg = b"Cannot convert object to primitive value"; + let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + let err = crate::error::js_typeerror_new(s); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)); } None } diff --git a/crates/perry-runtime/src/value/dyn_index.rs b/crates/perry-runtime/src/value/dyn_index.rs index 491311bdc2..34fcb6b248 100644 --- a/crates/perry-runtime/src/value/dyn_index.rs +++ b/crates/perry-runtime/src/value/dyn_index.rs @@ -157,6 +157,16 @@ pub extern "C" fn js_dyn_index_get(value: f64, index: f64) -> f64 { if bits == TAG_UNDEFINED || bits == TAG_NULL { crate::object::has_own_helpers::throw_to_object_nullish_type_error(); } + // Property access on a Symbol primitive operates on a temporary boxed + // receiver so inherited Symbol.prototype properties/accessors participate + // in ordinary Get semantics. + if unsafe { crate::symbol::js_is_symbol(value) } != 0 { + let scope = crate::gc::RuntimeHandleScope::new(); + let symbol = scope.root_nanbox_f64(value); + let index = scope.root_nanbox_f64(index); + let boxed = crate::builtins::js_boxed_symbol_new(symbol.get_nanbox_f64()); + return js_dyn_index_get(boxed, index.get_nanbox_f64()); + } let jsval = JSValue::from_bits(bits); // #5525: a Symbol *index* (`obj[Symbol.iterator]`) must resolve through the // symbol side-table, never the integer-index / stringify paths below (which @@ -529,6 +539,16 @@ pub extern "C" fn js_dyn_index_set(obj: f64, index: f64, value: f64) -> f64 { crate::proxy::js_proxy_set(obj, index, value); return value; } + // Sloppy assignment still targets only the ephemeral ToObject wrapper, + // but it must run inherited Symbol.prototype setters before disappearing. + if unsafe { crate::symbol::js_is_symbol(obj) } != 0 { + let scope = crate::gc::RuntimeHandleScope::new(); + let symbol = scope.root_nanbox_f64(obj); + let index = scope.root_nanbox_f64(index); + let value = scope.root_nanbox_f64(value); + let boxed = crate::builtins::js_boxed_symbol_new(symbol.get_nanbox_f64()); + return js_dyn_index_set(boxed, index.get_nanbox_f64(), value.get_nanbox_f64()); + } // #5525: a Symbol *index* (`obj[sym] = v`) routes to the symbol side-table, // mirroring the get side. Codegen sends all non-string-literal unknown- // receiver writes here, so the runtime owns the symbol triage. @@ -619,6 +639,19 @@ pub extern "C" fn js_dyn_index_set(obj: f64, index: f64, value: f64) -> f64 { if raw_ptr < crate::gc::GC_HEADER_SIZE + 0x1000 { return value; } + // String-named computed writes use ordinary receiver-aware [[Set]]. This + // must precede the raw buffer/view branches: DataView and ArrayBuffer carry + // ordinary expandos in a side table, and returning from the byte-index + // branch would otherwise swallow `view[name] = value`. + let idx_top16 = index.to_bits() >> 48; + if idx_top16 == 0x7FFF || idx_top16 == 0x7FF9 { + let target = if jsval.is_pointer() { + obj + } else { + f64::from_bits(crate::value::js_nanbox_pointer(raw_ptr as i64).to_bits()) + }; + return crate::proxy::js_put_value_set(target, index, value, target, 0); + } if crate::typedarray::lookup_typed_array_kind(raw_ptr).is_some() { crate::typedarray_props::js_typed_array_index_set_dynamic( raw_ptr as *mut crate::typedarray::TypedArrayHeader, @@ -660,6 +693,30 @@ pub extern "C" fn js_dyn_index_set(obj: f64, index: f64, value: f64) -> f64 { if receiver_tag.is_some_and(|(obj_type, _)| is_registered_collection(raw_ptr, obj_type)) { return value; } + // An ordinary receiver whose explicit [[Prototype]] is a TypedArray must + // consult that integer-indexed exotic for canonical but INVALID numeric + // keys. Such a write is a successful no-op and must not create an own + // property on the receiver. Valid indices, however, continue through the + // ordinary receiver path below: they create an own property (and therefore + // reject a non-extensible receiver in strict code). + if let Some(proto_bits) = crate::object::prototype_chain::object_static_prototype(raw_ptr) { + let proto_addr = crate::value::js_nanbox_get_pointer(f64::from_bits(proto_bits)) as usize; + if proto_addr != 0 && crate::typedarray::lookup_typed_array_kind(proto_addr).is_some() { + let length = unsafe { + (*(proto_addr as *const crate::typedarray::TypedArrayHeader)).length as u32 + }; + let is_valid_index = finite_nonnegative_u32_index(index) + .is_some_and(|numeric_index| numeric_index < length); + if !is_valid_index { + let target = if jsval.is_pointer() { + obj + } else { + f64::from_bits(crate::value::js_nanbox_pointer(raw_ptr as i64).to_bits()) + }; + return crate::proxy::js_put_value_set(target, index, value, target, 0); + } + } + } // #5579 / Issue #957 (set side): a STRING index (`obj["foo"] = v`) must // route through the ordinary receiver-aware `[[Set]]`, NOT the numeric // element path below. A NaN-boxed string index otherwise reached the @@ -678,17 +735,6 @@ pub extern "C" fn js_dyn_index_set(obj: f64, index: f64, value: f64) -> f64 { // `finite_nonnegative_u32_index`, so NaN/fractional keys fall through to // the ToString write instead of aliasing element 0), so the #5544 perf // win stands. - let idx_top16 = index.to_bits() >> 48; - if idx_top16 == 0x7FFF || idx_top16 == 0x7FF9 { - // `target`/`receiver` must be a tagged value, not the raw heap address - // (`obj` arrives as a module-slot raw I64 when top16 == 0). - let target = if jsval.is_pointer() { - obj - } else { - f64::from_bits(crate::value::js_nanbox_pointer(raw_ptr as i64).to_bits()) - }; - return crate::proxy::js_put_value_set(target, index, value, target, 0); - } if let Some(idx_u32) = finite_nonnegative_u32_index(index) { if unsafe { crate::object::arguments_object_set_index( diff --git a/crates/perry-runtime/src/value/dynamic_arith.rs b/crates/perry-runtime/src/value/dynamic_arith.rs index f023a465da..6c0b1fbb21 100644 --- a/crates/perry-runtime/src/value/dynamic_arith.rs +++ b/crates/perry-runtime/src/value/dynamic_arith.rs @@ -702,6 +702,18 @@ pub unsafe extern "C" fn js_dynamic_neg(a: f64) -> f64 { -a } +/// Unary plus performs ToNumber, not the explicit `Number()` conversion: +/// after ToPrimitive an Object-wrapped BigInt must therefore throw instead of +/// being lossily converted to f64. +#[no_mangle] +pub unsafe extern "C" fn js_dynamic_pos(a: f64) -> f64 { + let numeric = to_numeric(a); + if JSValue::from_bits(numeric.to_bits()).is_bigint() { + throw_add_type_error(b"Cannot convert a BigInt value to a number"); + } + numeric +} + /// Dynamic bitwise NOT: `~BigInt` stays BigInt, otherwise use JS ToInt32. #[no_mangle] pub unsafe extern "C" fn js_dynamic_bitnot(a: f64) -> f64 { diff --git a/crates/perry-runtime/src/value/mod.rs b/crates/perry-runtime/src/value/mod.rs index 6babf4534a..6b9333de75 100644 --- a/crates/perry-runtime/src/value/mod.rs +++ b/crates/perry-runtime/src/value/mod.rs @@ -105,9 +105,9 @@ pub use nanbox::{ // ----- Dynamic arithmetic dispatch (BigInt vs float) ----- pub use dynamic_arith::{ js_dynamic_add, js_dynamic_bitand, js_dynamic_bitor, js_dynamic_bitxor, js_dynamic_div, - js_dynamic_mod, js_dynamic_mul, js_dynamic_neg, js_dynamic_pow, js_dynamic_shl, js_dynamic_shr, - js_dynamic_string_or_number_add, js_dynamic_sub, js_dynamic_ushr, js_numeric_step, - js_to_numeric, + js_dynamic_mod, js_dynamic_mul, js_dynamic_neg, js_dynamic_pos, js_dynamic_pow, js_dynamic_shl, + js_dynamic_shr, js_dynamic_string_or_number_add, js_dynamic_sub, js_dynamic_ushr, + js_numeric_step, js_to_numeric, }; // ----- Dynamic index get/set + bare-NaN check ----- diff --git a/crates/perry-runtime/src/value/to_string.rs b/crates/perry-runtime/src/value/to_string.rs index b41a9b87a4..2db7306c55 100644 --- a/crates/perry-runtime/src/value/to_string.rs +++ b/crates/perry-runtime/src/value/to_string.rs @@ -706,13 +706,30 @@ unsafe fn call_method_for_primitive( } let key = crate::string::js_string_from_bytes(method_name.as_ptr(), method_name.len() as u32); let key_handle = scope.root_string_ptr(key); - let key_ptr = key_handle.get_raw_const_ptr::(); - let has_own_method_key = crate::object::own_key_present(obj_ptr as *mut _, key_ptr); - let method = crate::object::js_object_get_field_by_name(obj_ptr, key_ptr); + // Presence is independent from the value returned by Get. In particular, + // an inherited accessor may exist yet return undefined/null; that is a + // present but non-callable method, so OrdinaryToPrimitive must continue to + // the other candidate rather than synthesizing a boxed-primitive default. + // `own_key_present(receiver)` cannot see that inherited descriptor. + let key_value = key_handle.with_const_ptr::(|key_ptr| { + f64::from_bits( + crate::value::JSValue::string_ptr(key_ptr as *mut crate::string::StringHeader).bits(), + ) + }); + let has_method_key = + crate::object::js_object_has_property(value_handle.get_nanbox_f64(), key_value).to_bits() + == crate::value::TAG_TRUE; + // `HasProperty` can run a Proxy trap and collect. Refresh the receiver and + // key from their handles before the subsequent ordinary Get. + let recv = value_handle.get_nanbox_f64(); + let obj_ptr = (recv.to_bits() & POINTER_MASK) as *const crate::object::ObjectHeader; + let method = key_handle.with_const_ptr::(|key_ptr| { + crate::object::js_object_get_field_by_name(obj_ptr, key_ptr) + }); // Must be a callable closure value (POINTER_TAG + CLOSURE_MAGIC). let method_bits = method.bits(); if (method_bits & 0xFFFF_0000_0000_0000) != POINTER_TAG { - return if has_own_method_key || (!method.is_undefined() && !method.is_null()) { + return if has_method_key || (!method.is_undefined() && !method.is_null()) { MethodOutcome::NonPrimitive } else { MethodOutcome::Absent @@ -720,7 +737,7 @@ unsafe fn call_method_for_primitive( } let method_ptr = (method_bits & POINTER_MASK) as usize; if !crate::closure::is_closure_ptr(method_ptr) { - return if has_own_method_key { + return if has_method_key { MethodOutcome::NonPrimitive } else { MethodOutcome::Absent diff --git a/crates/perry-transform/src/generator/lower.rs b/crates/perry-transform/src/generator/lower.rs index 46facb2fbb..05c341c013 100644 --- a/crates/perry-transform/src/generator/lower.rs +++ b/crates/perry-transform/src/generator/lower.rs @@ -975,7 +975,7 @@ pub fn transform_generator_function_with_extra_captures( Expr::LocalGet(return_param_id), true, )))); - if !is_async_generator && has_yielding_finally { + if has_yielding_finally { // #4438 B2-finally: route `.return(v)` into the innermost enclosing // yielding finally (record the pending return + jump in), then fall // through to the continuation loop so the finally's `yield`s suspend; diff --git a/scripts/gc_runtime_root_holders.json b/scripts/gc_runtime_root_holders.json index 274f72411f..fe829e1a29 100644 --- a/scripts/gc_runtime_root_holders.json +++ b/scripts/gc_runtime_root_holders.json @@ -780,14 +780,6 @@ "file": "crates/perry-runtime/src/object/class_registry/state.rs", "name": "FUNCTION_CLASS_IDS" }, - { - "file": "crates/perry-runtime/src/object/collection_proto_thunks.rs", - "name": "BUILTIN_MAP_SET_VALUE_BITS" - }, - { - "file": "crates/perry-runtime/src/object/collection_proto_thunks.rs", - "name": "BUILTIN_SET_ADD_VALUE_BITS" - }, { "file": "crates/perry-runtime/src/object/field_get_set/field_ops.rs", "name": "WARN_NULL_PTR_STATE" @@ -830,6 +822,14 @@ "file": "crates/perry-runtime/src/object/iterator_prototypes.rs", "name": "STRING_ITERATOR_PROTOTYPE_PTR_SLOT" }, + { + "file": "crates/perry-runtime/src/object/mod.rs", + "name": "ASYNC_FUNCTION_INTRINSIC_PROTO_PTR_SLOT" + }, + { + "file": "crates/perry-runtime/src/object/mod.rs", + "name": "ASYNC_FUNCTION_INTRINSIC_PTR_SLOT" + }, { "file": "crates/perry-runtime/src/object/mod.rs", "name": "ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR_SLOT" diff --git a/scripts/raw_handle_debt_baseline.txt b/scripts/raw_handle_debt_baseline.txt index ced7b1ba5b..209fc0eb49 100644 --- a/scripts/raw_handle_debt_baseline.txt +++ b/scripts/raw_handle_debt_baseline.txt @@ -1 +1 @@ -925 +923 diff --git a/scripts/raw_handle_debt_files.txt b/scripts/raw_handle_debt_files.txt index 40becbb8ca..91bd49155a 100644 --- a/scripts/raw_handle_debt_files.txt +++ b/scripts/raw_handle_debt_files.txt @@ -141,7 +141,7 @@ 27 crates/perry-runtime/src/thread.rs 7 crates/perry-runtime/src/timer.rs 1 crates/perry-runtime/src/typed_feedback.rs -4 crates/perry-runtime/src/typedarray/construct.rs +3 crates/perry-runtime/src/typedarray/construct.rs 2 crates/perry-runtime/src/typedarray/transform.rs 4 crates/perry-runtime/src/url/node_compat.rs 7 crates/perry-runtime/src/url/search_params.rs @@ -151,7 +151,7 @@ 10 crates/perry-runtime/src/v8.rs 6 crates/perry-runtime/src/value/dyn_index.rs 6 crates/perry-runtime/src/value/dynamic_arith.rs -3 crates/perry-runtime/src/value/to_string.rs +2 crates/perry-runtime/src/value/to_string.rs 19 crates/perry-runtime/src/wasi.rs 6 crates/perry-runtime/src/weakref.rs 20 crates/perry-runtime/src/webassembly.rs