From b6f1e8532ec3277b834d7bcea506dfcb16691bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 5 Sep 2026 01:18:42 +0200 Subject: [PATCH 1/2] fix: preserve ordinary prototype property assignments --- changelog.d/9365-ordinary-prototype-stores.md | 1 + .../src/collectors/scalar_method_dispatch.rs | 7 +- .../src/expr/static_field_meta.rs | 42 +++--- .../src/runtime_decls/strings.rs | 9 +- .../tests/temp_root_operand_temporaries.rs | 22 ++++ .../src/analysis/value_types_tests.rs | 1 + crates/perry-hir/src/ir/expr.rs | 14 +- crates/perry-hir/src/lower/expr_assign.rs | 14 +- .../src/lower/lower_expr/assignment.rs | 16 +-- crates/perry-hir/src/stable_hash/expr.rs | 2 +- crates/perry-hir/src/walker/expr_mut.rs | 2 +- crates/perry-hir/src/walker/expr_ref.rs | 2 +- .../src/object/class_registry.rs | 4 +- .../class_registry/prototype_objects.rs | 64 ++++++++- crates/perry-runtime/src/proxy.rs | 23 +++- .../src/inline/exact_receivers.rs | 4 +- ...est_gap_9365_prototype_property_stores.cts | 123 ++++++++++++++++++ 17 files changed, 277 insertions(+), 73 deletions(-) create mode 100644 changelog.d/9365-ordinary-prototype-stores.md create mode 100644 test-files/test_gap_9365_prototype_property_stores.cts diff --git a/changelog.d/9365-ordinary-prototype-stores.md b/changelog.d/9365-ordinary-prototype-stores.md new file mode 100644 index 0000000000..78742a109e --- /dev/null +++ b/changelog.d/9365-ordinary-prototype-stores.md @@ -0,0 +1 @@ +Fix statically named `.prototype` assignments on ordinary objects when the receiver is a function parameter or has previously received a computed-key write (#9365). These assignments now use ordinary property semantics, including accessors, proxies, and strict-mode write failures, while preserving function prototype metadata for derived classes. Evaluate the receiver once and keep it rooted while evaluating the assigned value. diff --git a/crates/perry-codegen/src/collectors/scalar_method_dispatch.rs b/crates/perry-codegen/src/collectors/scalar_method_dispatch.rs index 2f985ef075..83a64c3739 100644 --- a/crates/perry-codegen/src/collectors/scalar_method_dispatch.rs +++ b/crates/perry-codegen/src/collectors/scalar_method_dispatch.rs @@ -606,9 +606,10 @@ fn note_prototype_effect( } // Function-classic prototypes are keyed by a synthetic class id derived // from the closure value, and `new ()` lowers to `NewDynamic`, so - // these cannot rewrite a declared class's table. `SetFunctionPrototype` - // installs a whole prototype object for such a function — same story. - Expr::RegisterFunctionPrototypeMethod { .. } | Expr::SetFunctionPrototype { .. } => {} + // these cannot rewrite a declared class's table. + Expr::RegisterFunctionPrototypeMethod { .. } => {} + // #9365: this node also performs ordinary stores on arbitrary receivers. + Expr::SetFunctionPrototype { func, .. } => note_prototype_holder(func, facts), // Any expression that so much as NAMES a prototype object: the value // can be aliased into a local and written through later. Expr::PropertyGet { diff --git a/crates/perry-codegen/src/expr/static_field_meta.rs b/crates/perry-codegen/src/expr/static_field_meta.rs index 8af687611a..e768a9685d 100644 --- a/crates/perry-codegen/src/expr/static_field_meta.rs +++ b/crates/perry-codegen/src/expr/static_field_meta.rs @@ -726,26 +726,28 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { Ok(obj_box) }) } - // Issue #711 part 2: `.prototype = ` pattern. - // Calls `js_set_function_prototype(func, proto)`, which (when - // func is a closure and proto is an object) allocates a - // synthetic class id and binds the proto object as that - // class's vtable source. Method dispatch later consults - // CLASS_PROTOTYPE_OBJECTS to resolve methods. - Expr::SetFunctionPrototype { func, proto } => { - let func_val = lower_expr(ctx, func)?; - let proto_val = lower_expr(ctx, proto)?; - // Discard the returned synthetic class id — it's stored in - // the runtime side-table keyed by func_val and consulted - // later by `js_register_class_parent_dynamic`. User code - // gets the assigned value (proto_val) as the expression - // result, matching JS semantics for `x.foo = bar`. - let _ = ctx.block().call( - crate::types::I32, - "js_set_function_prototype", - &[(DOUBLE, &func_val), (DOUBLE, &proto_val)], - ); - Ok(proto_val) + Expr::SetFunctionPrototype { + func, + proto, + strict, + } => { + with_rooted_group(ctx, 1, |ctx, group| { + let protect_receiver = any_operand_may_collect(ctx, [proto.as_ref()]); + let receiver = group.lower(ctx, func, protect_receiver)?; + let value = lower_expr(ctx, proto)?; + let receiver = group.reread(ctx, receiver)?; + // The setter can run user code and collect. Its rooted return + // supplies the assignment result after any evacuation. + Ok(ctx.block().call( + DOUBLE, + "js_set_prototype_property", + &[ + (DOUBLE, &receiver), + (DOUBLE, &value), + (I32, if *strict { "1" } else { "0" }), + ], + )) + }) } // Link a generator/async-generator instance into the spec prototype // chain. Closure bodies can use their own closure pointer to preserve diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs index b101f9cbf9..c4a02e2aba 100644 --- a/crates/perry-codegen/src/runtime_decls/strings.rs +++ b/crates/perry-codegen/src/runtime_decls/strings.rs @@ -1479,13 +1479,10 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { &[I32, PTR, I64, DOUBLE, DOUBLE], ); module.declare_function("js_array_push_spread_any", I64, &[I64, DOUBLE]); - // Issue #711 part 2: prototype-based class declaration via - // `.prototype = `. Binds an object as the function's - // prototype source; subsequent `class X extends ` lookups - // dispatch into the object's methods. Returns the synthetic - // class id allocated for the function value (or 0 on validation - // failure). Codegen discards the return. + // Retain the legacy registration ABI. New assignments use ordinary + // PutValue and synchronize function metadata only from the stored value. module.declare_function("js_set_function_prototype", I32, &[DOUBLE, DOUBLE]); + module.declare_function("js_set_prototype_property", DOUBLE, &[DOUBLE, DOUBLE, I32]); // Issue #838: JS-classic prototype-method assignment. // `Class.prototype.method = fn` (or the aliased // `let p = Class.prototype; p.method = fn` shape) registers the diff --git a/crates/perry-codegen/tests/temp_root_operand_temporaries.rs b/crates/perry-codegen/tests/temp_root_operand_temporaries.rs index 88b3201342..6bfb4f71b5 100644 --- a/crates/perry-codegen/tests/temp_root_operand_temporaries.rs +++ b/crates/perry-codegen/tests/temp_root_operand_temporaries.rs @@ -164,6 +164,28 @@ fn allocating() -> Expr { Expr::Object(Vec::new()) } +#[test] +fn prototype_assignment_receiver_survives_an_allocating_rhs() { + let ir = ir_for( + "prototype_store_9365.cts", + vec![Stmt::Expr(Expr::SetFunctionPrototype { + func: Box::new(allocating()), + proto: Box::new(allocating()), + strict: false, + })], + ); + let f = init_ir(&ir); + assert_eq!( + f.lines() + .filter(|line| line.contains("call i64 @js_object_alloc(")) + .count(), + 2, + "both operands must allocate exactly once:\n{f}", + ); + let receiver = first_call_result(f, "js_object_alloc").expect("receiver allocation"); + assert_rooted_across(f, &receiver, "js_set_prototype_property", "#9365 receiver"); +} + // ---------------------------------------------------------------- #6970 ---- /// `m.set(key, value)` where `value` allocates: `key` is finished but lives in diff --git a/crates/perry-hir/src/analysis/value_types_tests.rs b/crates/perry-hir/src/analysis/value_types_tests.rs index 3380cc7448..215b8b5a07 100644 --- a/crates/perry-hir/src/analysis/value_types_tests.rs +++ b/crates/perry-hir/src/analysis/value_types_tests.rs @@ -1276,6 +1276,7 @@ fn infers_class_prototype_and_super_meta_value_shapes() { &Expr::SetFunctionPrototype { func: Box::new(Expr::FuncRef(1)), proto: Box::new(Expr::String("proto".to_string())), + strict: false, }, &env, ), diff --git a/crates/perry-hir/src/ir/expr.rs b/crates/perry-hir/src/ir/expr.rs index 831ead8020..ed4a85ecc4 100644 --- a/crates/perry-hir/src/ir/expr.rs +++ b/crates/perry-hir/src/ir/expr.rs @@ -596,18 +596,14 @@ pub enum Expr { captured_args: Vec, }, - // Issue #711 part 2: `.prototype = ` pattern, - // used by Effect's effectable.ts to declare prototype-based - // classes. Codegen emits a call to `js_set_function_prototype` - // which stores `func_value → synthetic_class_id` in a side-table - // and binds the object as the synthetic class's prototype source. - // When `class Derived extends ` evaluates later, the dynamic - // parent registration looks up that synthetic class_id and wires - // it into CLASS_REGISTRY so method dispatch on Derived instances - // walks through to the prototype object's methods. + // Static `.prototype` assignment. Evaluates the receiver once and performs + // ordinary PutValue, including strict-mode rejection. For function receivers + // the runtime also synchronizes the synthetic class prototype used by + // dynamic `class Derived extends Base` dispatch (#711). SetFunctionPrototype { func: Box, proto: Box, + strict: bool, }, // Issue #838: `.prototype. = ` and the diff --git a/crates/perry-hir/src/lower/expr_assign.rs b/crates/perry-hir/src/lower/expr_assign.rs index 869139fb51..c07f56e2bb 100644 --- a/crates/perry-hir/src/lower/expr_assign.rs +++ b/crates/perry-hir/src/lower/expr_assign.rs @@ -1108,23 +1108,15 @@ fn lower_assignment_target( match &member.prop { ast::MemberProp::Ident(ident) => { let property = ident.sym.to_string(); - // Issue #711 part 2: route `.prototype = - // ` through SetFunctionPrototype so the - // runtime binds the proto object as the function - // value's class-prototype source. Effect's - // effectable.ts uses this to declare classes via - // prototype assignment on a plain function. The - // runtime helper is a no-op when `object` doesn't - // resolve to a function at runtime (preserves the - // baseline for arbitrary `obj.prototype = X` - // writes — those are rare and meaningless on - // non-functions in practice). + // Ordinary property assignment, with function prototype + // metadata synchronized for dynamic class parents (#711). if property == "prototype" { return Ok(wrap_assign_object_prelude( prelude.take(), Expr::SetFunctionPrototype { func: object, proto: value, + strict: ctx.current_strict, }, )); } diff --git a/crates/perry-hir/src/lower/lower_expr/assignment.rs b/crates/perry-hir/src/lower/lower_expr/assignment.rs index c4db5b3a68..be59969e7d 100644 --- a/crates/perry-hir/src/lower/lower_expr/assignment.rs +++ b/crates/perry-hir/src/lower/lower_expr/assignment.rs @@ -64,23 +64,13 @@ pub(crate) fn lower_expr_assignment( let result = match &member.prop { ast::MemberProp::Ident(ident) => { let property = ident.sym.to_string(); - // Issue #711 part 2: `.prototype = ` - // pattern (Effect's effectable.ts uses this to - // declare prototype-based classes — `function - // Base() {}; Base.prototype = CommitPrototype`). - // Route through the SetFunctionPrototype HIR node - // so codegen calls - // `js_set_function_prototype(func, proto)`, which - // allocates a synthetic class id keyed by the - // function value. The runtime helper is a no-op - // when `object` doesn't evaluate to a function - // (preserves baseline for legitimate - // `someClass.prototype = X` writes on non-function - // values). + // Ordinary property assignment, with function prototype + // metadata synchronized for dynamic class parents (#711). if property == "prototype" { Expr::SetFunctionPrototype { func: object, proto: value, + strict: ctx.current_strict, } } else { Expr::PutValueSet { diff --git a/crates/perry-hir/src/stable_hash/expr.rs b/crates/perry-hir/src/stable_hash/expr.rs index a4b1391989..4f70d84c61 100644 --- a/crates/perry-hir/src/stable_hash/expr.rs +++ b/crates/perry-hir/src/stable_hash/expr.rs @@ -650,7 +650,7 @@ impl SH for Expr { Expr::RegisterClassComputedMethod { class_name, key_expr, method_name, is_static, param_count, has_rest, definition_order } => { tag(h, 12233); class_name.hash(h); key_expr.as_ref().hash(h); method_name.hash(h); is_static.hash(h); param_count.hash(h); has_rest.hash(h); definition_order.hash(h); } Expr::RegisterClassComputedAccessor { class_name, key_expr, getter_name, setter_name, is_static, definition_order } => { tag(h, 12234); class_name.hash(h); key_expr.as_ref().hash(h); getter_name.hash(h); setter_name.hash(h); is_static.hash(h); definition_order.hash(h); } Expr::ClassExprFresh { template, evaluation_owner, named_statics, computed_keys, computed_statics, static_init_order, captured_args, } => { tag(h, 12026); template.hash(h); evaluation_owner.hash(h); for (n, v) in named_statics { n.hash(h); v.hash(h); } for (n, k) in computed_keys { n.hash(h); k.hash(h); } for (n, v) in computed_statics { n.hash(h); v.hash(h); } for step in static_init_order { match step { ClassFreshStaticInit::Named(index) => { tag(h, 0); index.hash(h); }, ClassFreshStaticInit::Computed(index) => { tag(h, 1); index.hash(h); }, ClassFreshStaticInit::Block(index) => { tag(h, 2); index.hash(h); }, } } for a in captured_args { a.hash(h); } } - Expr::SetFunctionPrototype { func, proto } => { tag(h, 448); func.as_ref().hash(h); proto.as_ref().hash(h); } + Expr::SetFunctionPrototype { func, proto, strict } => { tag(h, 448); func.as_ref().hash(h); proto.as_ref().hash(h); strict.hash(h); } Expr::RegisterPrototypeMethod { class_name, method_name, value, } => { tag(h, 463); class_name.hash(h); method_name.hash(h); value.as_ref().hash(h); } Expr::RegisterFunctionPrototypeMethod { func, method_name, value, } => { tag(h, 464); func.as_ref().hash(h); method_name.hash(h); value.as_ref().hash(h); } Expr::GetFunctionPrototypeMethod { func, method_name } => { tag(h, 1465); func.as_ref().hash(h); method_name.hash(h); } diff --git a/crates/perry-hir/src/walker/expr_mut.rs b/crates/perry-hir/src/walker/expr_mut.rs index fbc7b699e6..b4a15663b4 100644 --- a/crates/perry-hir/src/walker/expr_mut.rs +++ b/crates/perry-hir/src/walker/expr_mut.rs @@ -637,7 +637,7 @@ where f(a); } } - Expr::SetFunctionPrototype { func, proto } => { + Expr::SetFunctionPrototype { func, proto, .. } => { f(func); f(proto); } diff --git a/crates/perry-hir/src/walker/expr_ref.rs b/crates/perry-hir/src/walker/expr_ref.rs index f991d28488..dc2ca8c855 100644 --- a/crates/perry-hir/src/walker/expr_ref.rs +++ b/crates/perry-hir/src/walker/expr_ref.rs @@ -638,7 +638,7 @@ where f(a); } } - Expr::SetFunctionPrototype { func, proto } => { + Expr::SetFunctionPrototype { func, proto, .. } => { f(func); f(proto); } diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 4fcf63054f..4c7de9a7a5 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -96,7 +96,9 @@ pub(crate) use prototype_objects::{ function_value_for_class_id, resolve_proto_chain_field, resolve_proto_chain_field_with_receiver, resolve_proto_chain_symbol, }; -pub use prototype_objects::{js_set_function_prototype, NEXT_SYNTHETIC_CLASS_ID}; +pub use prototype_objects::{ + js_set_function_prototype, js_set_prototype_property, NEXT_SYNTHETIC_CLASS_ID, +}; // ── class_meta.rs ─────────────────────────────────────────────────────────── #[cfg(test)] diff --git a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs index 047a4d3e90..d5c41c34bb 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs @@ -156,9 +156,67 @@ per_test_global! { std::sync::atomic::AtomicU32::new(0x8000_0000); } -/// Register a function's prototype object. Called by codegen-emitted -/// init code whenever the HIR detects `.prototype = ` at -/// the assignment-statement level (lower_expr_assignment Member arm). +/// Perform ordinary `.prototype` assignment, then synchronize the synthetic +/// class metadata used when a class extends a function (#711, #9365). +#[no_mangle] +pub extern "C" fn js_set_prototype_property(receiver: f64, value: f64, strict: i32) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(receiver); + let value = scope.root_nanbox_f64(value); + let key = scope.root_string_ptr(crate::string::js_string_from_bytes( + b"prototype".as_ptr(), + 9, + )); + let key = key + .with_const_ptr::(|key| crate::value::js_nanbox_string(key as i64)); + crate::proxy::js_put_value_set( + receiver.get_nanbox_f64(), + key, + value.get_nanbox_f64(), + receiver.get_nanbox_f64(), + strict, + ); + + // Synchronize from the actual own property. A sloppy rejected write or an + // accessor must not install the attempted RHS as a class prototype, and + // synchronization must not reset the property's descriptor attributes. + // These probes and side-table updates have no JS/GC safepoints; the receiver + // keeps its own prototype live throughout. Ownership is checked before any + // header read so proxies and other synthetic pointer values are harmless. + let func = receiver.get_nanbox_f64(); + let func_value = JSValue::from_bits(func.to_bits()); + if func_value.is_pointer() { + let func_ptr = func_value.as_pointer::() as usize; + let header = unsafe { crate::value::addr_class::try_read_tracked_gc_header(func_ptr) }; + if header + .is_some_and(|header| unsafe { header.as_ref().obj_type == crate::gc::GC_TYPE_CLOSURE }) + && get_accessor_descriptor(func_ptr, "prototype").is_none() + { + if let Some(proto) = crate::closure::closure_get_own_dynamic_prop(func_ptr, "prototype") + { + let proto = JSValue::from_bits(proto.to_bits()); + if proto.is_pointer() { + let proto_ptr = proto.as_pointer::() as *mut ObjectHeader; + let header = unsafe { + crate::value::addr_class::try_read_tracked_gc_header(proto_ptr as usize) + }; + if header.is_some_and(|header| unsafe { + header.as_ref().obj_type == crate::gc::GC_TYPE_OBJECT + }) { + let class_id = synthetic_class_id_for_function(func); + class_prototype_object_root_store(class_id, proto_ptr); + crate::typed_feedback::invalidate_method_change(class_id); + crate::object::prop_plan::prop_plan_epoch_bump(); + } + } + } + } + } + value.get_nanbox_f64() +} + +/// Legacy function-prototype registration ABI. New codegen uses +/// `js_set_prototype_property` to preserve ordinary property semantics. /// /// Returns the synthetic class_id allocated for this function (0 if /// validation fails). The synthetic id is folded into CLASS_REGISTRY diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index 1975eedc69..66cc23fc2b 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -1469,7 +1469,10 @@ fn own_set_descriptor(target: f64, key: f64) -> Option { // The null-receiver guard stays BEFORE the coercion: `key_to_rust_string` // can run a user `toString`, and moving it earlier would make that side // effect observable on a path that previously short-circuited. - if extract_pointer(target.to_bits()) as usize == 0 { + // ClassRef constructors are non-pointer values with an own prototype. + if extract_pointer(target.to_bits()) as usize == 0 + && crate::object::class_ref_id(target).is_none() + { return None; } // #6943: `key_to_rust_string` runs the GC-capable `js_string_coerce`, and @@ -1485,6 +1488,14 @@ fn own_set_descriptor(target: f64, key: f64) -> Option { let target_handle = scope.root_heap_word_u64(target.to_bits()); let key_name = key_to_rust_string(key)?; let target = f64::from_bits(target_handle.get_heap_word_u64()); + // Class constructors have an immutable own prototype even though their + // ClassRef representation has no heap address or descriptor side table. + if key_name == "prototype" + && crate::object::class_ref_id(target).is_some() + && crate::object::class_prototype_ref_id(target).is_none() + { + return Some(OwnSetDescriptor::Data { writable: false }); + } let obj_ptr = extract_pointer(target.to_bits()) as usize; if obj_ptr == 0 { return None; @@ -1532,6 +1543,16 @@ fn own_set_descriptor(target: f64, key: f64) -> Option { } if crate::closure::is_closure_ptr(obj_ptr) { if crate::object::has_own_helpers::closure_own_key_present(obj_ptr, &key_name) { + // A function's lazily synthesized prototype is already an own + // property. Preserve its attributes when the first operation is + // an assignment, before any read materializes the default object. + if key_name == "prototype" && crate::object::function_would_have_own_prototype(target) { + crate::object::set_builtin_property_attrs( + obj_ptr, + key_name.clone(), + crate::object::PropertyAttrs::new(true, false, false), + ); + } return Some(OwnSetDescriptor::Data { writable: !matches!(key_name.as_str(), "name" | "length"), }); diff --git a/crates/perry-transform/src/inline/exact_receivers.rs b/crates/perry-transform/src/inline/exact_receivers.rs index aa2c5906e5..53b235119b 100644 --- a/crates/perry-transform/src/inline/exact_receivers.rs +++ b/crates/perry-transform/src/inline/exact_receivers.rs @@ -102,9 +102,7 @@ pub(crate) fn collect_module_prototype_facts(module: &Module) -> ModulePrototype facts.touched_classes.insert(class_name.clone()); } Expr::SetFunctionPrototype { func, .. } => { - if let Expr::ClassRef(name) = func.as_ref() { - facts.touched_classes.insert(name.clone()); - } + note_holder(func, facts); } Expr::PropertyGet { object, property, .. diff --git a/test-files/test_gap_9365_prototype_property_stores.cts b/test-files/test_gap_9365_prototype_property_stores.cts new file mode 100644 index 0000000000..99876f51c5 --- /dev/null +++ b/test-files/test_gap_9365_prototype_property_stores.cts @@ -0,0 +1,123 @@ +// #9365: a property named "prototype" is an ordinary property on non-functions. +function assign(target: any, value: any): any { + return (target.prototype = value); +} +function assignStrict(target: any, value: any): any { + "use strict"; + return (target.prototype = value); +} +function rejected(target: any): boolean { + try { + assignStrict(target, 99); + return false; + } catch (error) { + return error instanceof TypeError; + } +} + +const payload = { marker: 7 }; +const parameter: any = {}; +console.log("parameter", assign(parameter, payload) === payload); +console.log("own", Object.hasOwn(parameter, "prototype"), parameter.prototype === payload); +const descriptor = Object.getOwnPropertyDescriptor(parameter, "prototype"); +console.log("descriptor", descriptor.writable, descriptor.enumerable, descriptor.configurable); +console.log("keys", Object.keys(parameter).join(",")); + +for (let count = 0; count < 4; count++) { + const dynamic: any = {}; + for (let i = 0; i < count; i++) dynamic["x" + i] = i; + dynamic.prototype = payload; + console.log("dynamic", count, Object.hasOwn(dynamic, "prototype"), dynamic.prototype === payload); +} + +const primitiveValues: any[] = [17, "text", true, null, undefined]; +for (const value of primitiveValues) { + const target: any = {}; + console.log("value", assign(target, value) === value, target.prototype === value); +} +const arrayValue = [1, 2]; +const arrayTarget: any = []; +console.log("array", assign(arrayTarget, arrayValue) === arrayValue, arrayTarget.prototype === arrayValue); +const computed: any = {}; +computed["prototype"] = payload; +console.log("computed", computed.prototype === payload); + +let setterCalls = 0; +let setterThis: any; +let setterValue: any; +const accessor: any = {}; +Object.defineProperty(accessor, "prototype", { + set(value) { setterCalls++; setterThis = this; setterValue = value; }, + configurable: true, +}); +console.log("setter-result", assign(accessor, payload) === payload); +console.log("setter", setterCalls, setterThis === accessor, setterValue === payload); +const inherited: any = Object.create(accessor); +assign(inherited, arrayValue); +console.log("inherited", setterCalls, setterThis === inherited, setterValue === arrayValue, + Object.hasOwn(inherited, "prototype")); + +let proxyCalls = 0; +const proxyTarget: any = {}; +let proxy: any; +proxy = new Proxy(proxyTarget, { + set(target, key, value, receiver) { + proxyCalls++; + console.log("trap", key, receiver === proxy); + return Reflect.set(target, key, value, receiver); + }, +}); +console.log("proxy-result", assign(proxy, payload) === payload, proxyTarget.prototype === payload, proxyCalls); +const rejectingProxy = new Proxy({}, { set() { return false; } }); +console.log("proxy-reject", assign(rejectingProxy, payload) === payload, rejected(rejectingProxy)); + +const readonly: any = {}; +Object.defineProperty(readonly, "prototype", { value: 12, writable: false }); +console.log("readonly", assign(readonly, 13), readonly.prototype, rejected(readonly)); +const frozen = Object.freeze({}); +console.log("frozen", assign(frozen, payload) === payload, Object.hasOwn(frozen, "prototype"), rejected(frozen)); +console.log("primitive", assign(42, payload) === payload, rejected(42)); +console.log("nullish", rejected(null), rejected(undefined)); + +let receiverCalls = 0; +let rhsCalls = 0; +let order = ""; +const ordered: any = {}; +function receiver(): any { receiverCalls++; order += "r"; return ordered; } +function rhs(): any { rhsCalls++; order += "v"; return payload; } +console.log("order-result", (receiver().prototype = rhs()) === payload); +console.log("order", receiverCalls, rhsCalls, order, ordered.prototype === payload); +const holder = { get target(): any { receiverCalls++; return ordered; } }; +holder.target.prototype = arrayValue; +console.log("getter-once", receiverCalls, ordered.prototype === arrayValue); +(receiverCalls > 0 ? receiver() : holder.target).prototype = payload; +console.log("conditional-once", receiverCalls, ordered.prototype === payload); + +function Base() {} +const basePrototype = { method() { return 23; } }; +assign(Base, basePrototype); +class Derived extends Base {} +console.log("function", Base.prototype === basePrototype, new Derived().method()); +const functionDescriptor = Object.getOwnPropertyDescriptor(Base, "prototype"); +console.log("function-descriptor", functionDescriptor.writable, functionDescriptor.enumerable, functionDescriptor.configurable); +Object.defineProperty(Base, "prototype", { writable: false }); +console.log("function-readonly", assign(Base, payload) === payload, Base.prototype === basePrototype, rejected(Base)); +class StillDerived extends Base {} +console.log("function-retained", new StillDerived().method()); +const arrow: any = () => 1; +assign(arrow, payload); +const arrowDescriptor = Object.getOwnPropertyDescriptor(arrow, "prototype"); +console.log("arrow", arrow.prototype === payload, arrowDescriptor.writable, + arrowDescriptor.enumerable, arrowDescriptor.configurable); + +class Carrier { + prototype() { return 10; } + read() { return this.prototype(); } +} +const carrier = new Carrier(); +console.log("class-method-before", carrier.read()); +assign(Carrier.prototype, function() { return 42; }); +console.log("class-method-after", carrier.read()); +const carrierPrototype = Carrier.prototype; +console.log("class-readonly", assign(Carrier, payload) === payload, + Carrier.prototype === carrierPrototype, rejected(Carrier), Reflect.set(Carrier, "prototype", payload)); From 05d04a4ce98ae6557a27fbbef57396a0ffdf8fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 5 Sep 2026 01:19:45 +0200 Subject: [PATCH 2/2] docs: number changelog fragment for PR 9757 --- ...nary-prototype-stores.md => 9757-ordinary-prototype-stores.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{9365-ordinary-prototype-stores.md => 9757-ordinary-prototype-stores.md} (100%) diff --git a/changelog.d/9365-ordinary-prototype-stores.md b/changelog.d/9757-ordinary-prototype-stores.md similarity index 100% rename from changelog.d/9365-ordinary-prototype-stores.md rename to changelog.d/9757-ordinary-prototype-stores.md