diff --git a/changelog.d/11787-function-constructors.md b/changelog.d/11787-function-constructors.md new file mode 100644 index 0000000000..185a99511d --- /dev/null +++ b/changelog.d/11787-function-constructors.md @@ -0,0 +1,15 @@ +Plain function constructors are cheap (#10507). A function body the compiler +emitted now carries `FN_COMPILED_BODY` in its info, so `new F()` and +`x instanceof F` decide once per body that `F` is an ordinary function and skip +the built-in, bound, proxy and native-module probes. A construction replays the +birth record kept on `F.prototype` (class id and birth ShapeId) instead of +three hash lookups and two shape interns; `F.prototype` is read through the +function's ShapeId. `x instanceof F` is one shape compare when `x`'s ShapeId +names `F.prototype`, and otherwise OrdinaryHasInstance's prototype walk, so an +object created before `F.prototype` was reassigned is no longer reported as an +instance. `instanceof` on a bound function now answers for its target. A +method call whose name a class also declares sends receivers of no such class +to the method site rather than the by-name dispatcher, so methods on a +function's prototype are called directly. Repro (instructions per op): +`new F()` 5,048 -> ~1,150, `x instanceof F` 2,809 -> ~430, decimal.js-shaped +`x.plus(i)` 15,848 -> ~4,300. diff --git a/crates/perry-abi/src/lib.rs b/crates/perry-abi/src/lib.rs index 351bf74fd9..86932e5d9f 100644 --- a/crates/perry-abi/src/lib.rs +++ b/crates/perry-abi/src/lib.rs @@ -348,6 +348,13 @@ pub const FN_HAS_DECLARED: u32 = 1 << 11; /// Dylib bodies omit this bit: a shape must not retain their info address /// beyond `dlclose` or mistake a reused address for the same body. pub const FN_PERMANENT_IMAGE: u32 = 1 << 12; +/// The compiler emitted this body from JavaScript source (every info +/// `perry-codegen` renders carries it; no runtime-native info does). A +/// function object on such a body is never a built-in, bound, native-module +/// or class constructor, so its `[[Construct]]` and `instanceof` are the +/// ordinary ones: the runtime decides that from this bit, once per body, +/// instead of probing the callee against every built-in on each use. +pub const FN_COMPILED_BODY: u32 = 1 << 13; /// Byte offsets of the fields codegen emits and emitted code reads. pub const JS_FUNCTION_INFO_CODE_OFFSET: usize = 0; diff --git a/crates/perry-codegen/src/fn_info.rs b/crates/perry-codegen/src/fn_info.rs index 8d4d70cb38..70e3c87c7c 100644 --- a/crates/perry-codegen/src/fn_info.rs +++ b/crates/perry-codegen/src/fn_info.rs @@ -25,9 +25,9 @@ use std::collections::{BTreeMap, BTreeSet}; use crate::runtime_abi::{ - FN_ARROW, FN_ASYNC, FN_ASYNC_GENERATOR, FN_GENERATOR, FN_HAS_DECLARED, FN_HAS_LENGTH, - FN_PERMANENT_IMAGE, FN_REST_SYNTHETIC_ARGUMENTS, FN_REST_USER, FN_REST_USER_AND_ARGUMENTS, - FN_STRICT, + FN_ARROW, FN_ASYNC, FN_ASYNC_GENERATOR, FN_COMPILED_BODY, FN_GENERATOR, FN_HAS_DECLARED, + FN_HAS_LENGTH, FN_PERMANENT_IMAGE, FN_REST_SYNTHETIC_ARGUMENTS, FN_REST_USER, + FN_REST_USER_AND_ARGUMENTS, FN_STRICT, }; /// The LLVM type of a `JsFunctionInfo`, field for field (perry-abi's @@ -235,7 +235,9 @@ fn render_definition( ty = INFO_TYPE, params = saturate_u16(def.params as u64), rest = facts.rest_fixed, + // Every body this renders is compiled source (`FN_COMPILED_BODY`). flags = facts.flags + | FN_COMPILED_BODY | if permanent_image { FN_PERMANENT_IMAGE } else { @@ -288,7 +290,7 @@ mod tests { vec![format!( "@perry_closure_m__3$info = internal constant {INFO_TYPE} {{ ptr @perry_closure_m__3, \ i16 2, i16 1, i32 {}, i32 1, i32 0, ptr null, i64 0, ptr null, i32 0, i16 0, i16 0, i64 0 }}", - FN_REST_USER | FN_HAS_LENGTH | FN_ARROW + FN_REST_USER | FN_HAS_LENGTH | FN_ARROW | FN_COMPILED_BODY )] ); } @@ -299,8 +301,11 @@ mod tests { state.request("perry_closure_m__3"); let transient = state.render_globals(|_| defined(0, "internal"), [], false); let permanent = state.render_globals(|_| defined(0, "internal"), [], true); - assert!(transient[0].contains("i32 0, i32 0")); - assert!(permanent[0].contains(&format!("i32 {FN_PERMANENT_IMAGE}, i32 0"))); + assert!(transient[0].contains(&format!("i32 {FN_COMPILED_BODY}, i32 0"))); + assert!(permanent[0].contains(&format!( + "i32 {}, i32 0", + FN_PERMANENT_IMAGE | FN_COMPILED_BODY + ))); } #[test] @@ -323,7 +328,7 @@ mod tests { ); assert!(lines[0].starts_with(&format!("@{} = hidden constant", info_symbol(body)))); assert!(lines[0].contains(&format!("ptr @{body}"))); - assert!(lines[0].contains(&format!("i32 {FN_PERMANENT_IMAGE}"))); + assert!(lines[0].contains(&format!("i32 {}", FN_PERMANENT_IMAGE | FN_COMPILED_BODY))); } #[test] diff --git a/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs b/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs index e8f83fd666..6a62d5e59a 100644 --- a/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs +++ b/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs @@ -432,6 +432,7 @@ pub(crate) fn try_lower_instance_method_call( Vec::new() }; let mut shape_probe_cid: Option = None; + let mut site_arm: Option<(String, String)> = None; if !shape_probe_arms.is_empty() { let (cid, shape_id) = crate::lower_call::method_override::emit_inline_direct_method_shape_probe( @@ -464,6 +465,43 @@ pub(crate) fn try_lower_instance_method_call( blk.cond_br(&exact, &probe_dispatch_label, &miss_label); } ctx.current_block = own_idx; + // #10507: a receiver whose class id names no implementor + // (a plain object, a function-constructed instance, a + // primitive) can only reach the tower's default. It takes the + // universal method site instead — own and inherited entries + // call the body directly, and its miss is the same by-name + // dispatch — with no own-property probe call first. + let site_idx = ctx.new_block("idisp.site"); + let own_call_idx = ctx.new_block("idisp.own_probe_call"); + let site_label = ctx.block_label(site_idx); + let own_call_label = ctx.block_label(own_call_idx); + { + let blk = ctx.block(); + let mut implementor_hit: Option = None; + for (class_id, _) in implementors.iter() { + let eq = blk.icmp_eq(I32, &cid, &class_id.to_string()); + implementor_hit = Some(match implementor_hit { + None => eq, + Some(prev) => blk.or(I1, &prev, &eq), + }); + } + let hit = implementor_hit.unwrap_or_else(|| "false".to_string()); + blk.cond_br(&hit, &own_call_label, &site_label); + } + ctx.current_block = site_idx; + let v_site = crate::lower_call::console_promise::emit_native_method_str_dispatch( + ctx, + property, + call_byte_offset, + &recv_box, + &static_user_args, + ); + let after_site = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&probe_outer_merge_label); + } + site_arm = Some((v_site, after_site)); + ctx.current_block = own_call_idx; } let own_method_probe = ctx.block().call( @@ -792,13 +830,14 @@ pub(crate) fn try_lower_instance_method_call( // Outer merge: phi over override and dispatch values. ctx.current_block = probe_outer_merge_idx; - let v_probe_phi = ctx.block().phi( - DOUBLE, - &[ - (v_override_probe.as_str(), after_override_probe.as_str()), - (v_dispatch_phi.as_str(), after_dispatch_phi.as_str()), - ], - ); + let mut outer_inputs: Vec<(&str, &str)> = vec![ + (v_override_probe.as_str(), after_override_probe.as_str()), + (v_dispatch_phi.as_str(), after_dispatch_phi.as_str()), + ]; + if let Some((v_site, after_site)) = site_arm.as_ref() { + outer_inputs.push((v_site.as_str(), after_site.as_str())); + } + let v_probe_phi = ctx.block().phi(DOUBLE, &outer_inputs); // The release has to post-dominate BOTH arms of the override probe // and every case block of the tower, which is why this group is // `open_rooted_group` and the release sits in the outer merge. diff --git a/crates/perry-runtime/src/closure/dispatch.rs b/crates/perry-runtime/src/closure/dispatch.rs index 75e79d9dfb..e291fbf27a 100644 --- a/crates/perry-runtime/src/closure/dispatch.rs +++ b/crates/perry-runtime/src/closure/dispatch.rs @@ -42,7 +42,7 @@ pub use calln::{ }; pub use direct::{DirectCall1, DirectCall2, DirectCall3, DirectCall4}; -pub(crate) use value_call::native_call_value_this; +pub(crate) use value_call::{call_compiled_closure_this, native_call_value_this}; pub use value_call::{ js_closure_call_apply_with_spread, js_closure_call_array, js_native_call_value, }; diff --git a/crates/perry-runtime/src/closure/dispatch/value_call.rs b/crates/perry-runtime/src/closure/dispatch/value_call.rs index 02e91d1c59..959dce5d6a 100644 --- a/crates/perry-runtime/src/closure/dispatch/value_call.rs +++ b/crates/perry-runtime/src/closure/dispatch/value_call.rs @@ -206,6 +206,39 @@ unsafe fn native_call_value_this_impl( } return crate::object::js_new_function_construct(func_value, args_ptr, args_len); } + call_closure_body(closure, info, func_ptr, this, args_ptr, args_len) +} + +/// Call `closure` — a compiled ordinary function body (`FN_COMPILED_BODY`), +/// which none of the exotic callees [`js_native_call_value`] tests first +/// (class refs, proxies, bound native exports, no-op-backed built-ins, +/// class objects) can carry — with receiver `this` (#10507). +/// +/// # Safety +/// `closure` is a live closure whose info carries `FN_COMPILED_BODY`; +/// `args_ptr` holds `args_len` values. +pub(crate) unsafe fn call_compiled_closure_this( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + args_ptr: *const f64, + args_len: usize, +) -> f64 { + let info = crate::closure::closure_info(closure); + let func_ptr = info.map_or(std::ptr::null(), |info| info.code); + call_closure_body(closure, info, func_ptr, this, args_ptr, args_len) +} + +/// The arity-padding / rest-bundling tail of a value call, once the callee is +/// known to be a closure with a body. +#[inline(always)] +unsafe fn call_closure_body( + closure: *const ClosureHeader, + info: Option<&crate::closure::JsFunctionInfo>, + func_ptr: *const u8, + this: crate::closure::JsThis, + args_ptr: *const f64, + args_len: usize, +) -> f64 { let dispatch_args_len = match info { Some(info) if crate::closure::info_rest(info).is_none() => { args_len.max(usize::from(info.params)) diff --git a/crates/perry-runtime/src/closure/mod.rs b/crates/perry-runtime/src/closure/mod.rs index d3fd0c7680..d90b97c63f 100644 --- a/crates/perry-runtime/src/closure/mod.rs +++ b/crates/perry-runtime/src/closure/mod.rs @@ -61,12 +61,12 @@ pub(crate) use registry::{ info_versioned_loop_direct, }; -pub(crate) use dispatch::native_call_value_this; pub(crate) use dispatch::{ bound_function_lazy_name, bound_function_length, bound_method_source_func_ptr, coerce_call_this, rebind_explicit_this, rebind_explicit_this_allocates, reify_function_method_value, reset_throw_not_callable_counter, }; +pub(crate) use dispatch::{call_compiled_closure_this, native_call_value_this}; pub use dispatch::{ clean_closure_ptr, dispatch_bound_function, dispatch_bound_method, get_valid_func_ptr, get_valid_info, js_closure_call0, js_closure_call1, js_closure_call10, js_closure_call11, diff --git a/crates/perry-runtime/src/closure/shape.rs b/crates/perry-runtime/src/closure/shape.rs index aee4c8e5d0..7c4411bfc2 100644 --- a/crates/perry-runtime/src/closure/shape.rs +++ b/crates/perry-runtime/src/closure/shape.rs @@ -423,6 +423,93 @@ fn keyed_shape_lacks_key(id: u32, key: &[u8]) -> bool { } } +/// The function's own `prototype` value, read through its ShapeId (#10507): +/// `Some(Some(v))` the value, `Some(None)` no own `prototype` yet (a base +/// shape: nothing materialized it), `None` the shape does not say (a +/// FunctionDictionary or class function object, or a key outside the inline +/// slots) and the caller asks the bag. +/// +/// A keyed Function shape is minted from the bag's ordinary descriptor +/// (`refresh_closure_shape`), so its key list and live inline bound are the +/// bag's: the slot of `prototype` is a fact of the immutable id, cached per +/// agent like the intrinsic verdicts above. `prototype` of a function is a +/// non-configurable data property, so the slot always holds its value. +/// +/// # Safety +/// `closure` is a proven, live closure cell. +#[inline] +pub(crate) unsafe fn closure_own_prototype_by_shape( + closure: *const ClosureHeader, +) -> Option> { + let id = (*closure).shape_id; + let slot = (id as usize).wrapping_mul(0x9E37_79B9) >> 26 & (VERDICT_CACHE_LEN - 1); + // SAFETY: this agent's own cell; no reference to it outlives the read. + let cached = PROTOTYPE_SLOT_CACHE.with(|c| (*c.as_ptr())[slot]); + let index = if cached.0 == id && id != 0 { + cached.1 + } else { + let index = prototype_slot_of_shape(closure, id); + PROTOTYPE_SLOT_CACHE.with(|c| (*c.as_ptr())[slot] = (id, index)); + index + }; + match index { + PROTOTYPE_SLOT_UNKNOWN => None, + PROTOTYPE_SLOT_ABSENT => Some(None), + index => { + let bag = (*closure).props; + debug_assert!(!bag.is_null(), "a keyed Function shape has a bag"); + let fields = (bag as *const u8).add(std::mem::size_of::()) + as *const u64; + Some(Some(f64::from_bits(*fields.add(index as usize)))) + } + } +} + +const PROTOTYPE_SLOT_UNKNOWN: u32 = u32::MAX; +const PROTOTYPE_SLOT_ABSENT: u32 = u32::MAX - 1; + +crate::perry_thread_local! { + /// Per-agent cache of Function ShapeIds' inline slot of `prototype` + /// ([`closure_own_prototype_by_shape`]); ShapeIds are never reused, so an + /// entry can only go unused, never wrong. + static PROTOTYPE_SLOT_CACHE: std::cell::Cell<[(u32, u32); VERDICT_CACHE_LEN]> = + const { std::cell::Cell::new([(0, 0); VERDICT_CACHE_LEN]) }; +} + +/// The inline slot of `prototype` the Function ShapeId `id` describes, or +/// one of the two markers. +#[cold] +#[inline(never)] +unsafe fn prototype_slot_of_shape(closure: *const ClosureHeader, id: u32) -> u32 { + if id == function_dictionary_shape() || is_class_info((*closure).info) { + return PROTOTYPE_SLOT_UNKNOWN; + } + let Some(descriptor) = shapes::shape_descriptor_by_id(id) else { + return PROTOTYPE_SLOT_UNKNOWN; + }; + if descriptor.object_kind != ShapeObjectKind::Function { + return PROTOTYPE_SLOT_UNKNOWN; + } + if descriptor.keys == 0 || descriptor.logical_key_count == 0 { + return PROTOTYPE_SLOT_ABSENT; + } + let keys = descriptor.keys as usize as *const crate::array::ArrayHeader; + match crate::object::keys_find_slot_by_bytes_resolved( + keys, + descriptor.logical_key_count, + b"prototype", + ) { + None => PROTOTYPE_SLOT_ABSENT, + Some(index) + if (index as u32) < descriptor.live_inline_slot_count + && !crate::object::key_attrs::key_is_accessor_at(keys, index as u32) => + { + index as u32 + } + Some(_) => PROTOTYPE_SLOT_UNKNOWN, + } +} + /// Raw kind probe for a pointer the caller has already range/band-checked /// (the successor of the old `*(ptr + 12) == CLOSURE_MAGIC` read, with the /// same safety contract): the GC header's type byte says CLOSURE and the diff --git a/crates/perry-runtime/src/object/alloc_basic.rs b/crates/perry-runtime/src/object/alloc_basic.rs index f6b5f71074..9d65b65261 100644 --- a/crates/perry-runtime/src/object/alloc_basic.rs +++ b/crates/perry-runtime/src/object/alloc_basic.rs @@ -122,6 +122,42 @@ fn object_alloc_with_parent_impl( } } +/// An object born on a known birth shape: `class_id`, `field_count` live +/// inline slots (all `undefined`), stamped `shape_id` — a keyless ShapeId +/// minted for exactly that class and bound (#10507's prototype birth +/// record), so no descriptor is derived from the object. +pub(crate) fn object_alloc_born( + class_id: u32, + field_count: u32, + shape_id: u32, +) -> *mut ObjectHeader { + let alloc_field_count = std::cmp::max(field_count as usize, crate::object::INLINE_SLOT_FLOOR); + let total_size = + std::mem::size_of::() + alloc_field_count * std::mem::size_of::(); + let ptr = arena_alloc_gc(total_size, 8, crate::gc::GC_TYPE_OBJECT) as *mut ObjectHeader; + unsafe { + (*ptr).class_id = class_id; + (*ptr).parent_class_id = 0; + // GC_STORE_AUDIT(INIT): fresh object starts with no per-object meta record (#6759 B). + (*ptr).meta = ptr::null_mut(); + let fields_ptr = (ptr as *mut u8).add(std::mem::size_of::()) as *mut JSValue; + for i in 0..alloc_field_count { + // GC_STORE_AUDIT(INIT): freshly allocated object field slot is initialized pointer-free. + ptr::write(fields_ptr.add(i), JSValue::undefined()); + } + crate::gc::layout_init_pointer_free(ptr as *mut u8); + if crate::arena::pointer_in_nursery(ptr as usize) { + // A nursery newborn: no proof to retire, nobody inherits from it + // and no old-generation carrier to note — the stamp is the store. + // GC_STORE_AUDIT(POINTER_FREE): a ShapeId, never a heap reference. + (*ptr).parent_class_id = shape_id; + } else { + crate::object::shapes::stamp_object_shape_id_with_carrier_note(ptr, shape_id); + } + } + ptr +} + /// Fast object allocation using bump allocator - NO field initialization /// This is significantly faster for hot paths where constructor immediately sets all fields /// Returns a pointer to the object header with UNINITIALIZED fields diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 4824891ea0..22817b93bf 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -49,6 +49,7 @@ mod construct; #[cfg(feature = "regex-engine")] pub(crate) use construct::construct_two_rooted; pub(crate) use construct::{construct_rooted_arguments, scan_current_new_target_root_mut}; +pub(crate) use construct::{ordinary_compiled_function_has_instance, OrdinaryInstanceof}; mod decl_accessors; pub(crate) use decl_accessors::{ class_chain_getter_value, class_chain_setter_apply, decl_prototype_own_accessor, @@ -163,10 +164,10 @@ pub use prototype_methods::{ // ── construct.rs / vm_brand.rs ────────────────────────────────────────────── pub(crate) use construct::{ - extends_target_must_throw, is_callable_function_value, js_value_is_constructor, - lookup_own_prototype_method, 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, + bound_function_target_value, extends_target_must_throw, is_callable_function_value, + js_value_is_constructor, lookup_own_prototype_method, 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_new_function_construct, js_new_function_construct_apply, diff --git a/crates/perry-runtime/src/object/class_registry/construct.rs b/crates/perry-runtime/src/object/class_registry/construct.rs index 7745083a4b..a0b7d0eb20 100644 --- a/crates/perry-runtime/src/object/class_registry/construct.rs +++ b/crates/perry-runtime/src/object/class_registry/construct.rs @@ -37,6 +37,10 @@ pub extern "C" fn js_new_target_value() -> f64 { f64::from_bits(CURRENT_NEW_TARGET.with(|value| value.get())) } +mod compiled_function; +pub(crate) use compiled_function::{ + forget_birth_record_of_class, ordinary_compiled_function_has_instance, OrdinaryInstanceof, +}; mod rooted_arguments; pub(crate) use rooted_arguments::construct_rooted_arguments; #[cfg(feature = "regex-engine")] @@ -275,6 +279,15 @@ pub unsafe extern "C-unwind" fn js_new_function_construct( args_ptr: *const f64, args_len: usize, ) -> f64 { + // #10507: an ordinary compiled function is none of the exotic callees + // below — a fact of its body, read once from its info. + if let Some(closure) = compiled_function::ordinary_compiled_function(func_value) { + if let Some(result) = compiled_function::construct_ordinary_compiled_function( + func_value, closure, args_ptr, args_len, + ) { + return result; + } + } // A class value (its function object, or the legacy immediate) constructs // its class: decided first, one closure probe, before the exotic arms. if let Some(class_cid) = constructor_class_ref_id(func_value) { @@ -1213,34 +1226,13 @@ pub unsafe extern "C-unwind" fn js_new_function_construct( // result is discarded — JS `new` semantics use the receiver, // not the returned value (object returns would override, but // dayjs and siblings rely on the receiver mutation pattern). - // #7280: `nan_boxed` (the `this` this call is building) and - // the two DISPLACED new.target values are held across a call that runs a - // user constructor body — see the long note in - // `construct_registered_class_ref`. Unrooted, the evacuating minor - // moves the instance and this arm returns the pre-move address; - // reproduced by `new inst.ctor(x)` where `inst.ctor` is a plain - // function, 200/200 iterations wrong under - // `PERRY_GC_MOVING_LOOP_POLLS=1 PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1`. - let scope = crate::gc::RuntimeHandleScope::new(); - let inst_handle = scope.root_nanbox_f64(nan_boxed); - let prev_new_target = crate::object::js_new_target_get(); - let prev_new_target_handle = scope.root_nanbox_f64(prev_new_target); - crate::object::js_new_target_set(func_value); - let prev_current_new_target = - CURRENT_NEW_TARGET.with(|value| value.replace(func_value.to_bits())); - let prev_current_new_target_handle = scope.root_nanbox_u64(prev_current_new_target); - let result = crate::closure::native_call_value_this( - func_value, - crate::closure::JsThis::from_f64(inst_handle.get_nanbox_f64()), - args_ptr, - args_len, + // Reproduced unrooted by `new inst.ctor(x)` where `inst.ctor` is a + // plain function, 200/200 iterations wrong under + // `PERRY_GC_MOVING_LOOP_POLLS=1 PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1` + // (#7280): the helper roots the instance across the body. + return compiled_function::run_constructor_body( + func_value, nan_boxed, args_ptr, args_len, false, ); - CURRENT_NEW_TARGET.with(|value| value.set(prev_current_new_target_handle.get_nanbox_u64())); - crate::object::js_new_target_set(prev_new_target_handle.get_nanbox_f64()); - if constructor_return_overrides_this(result) { - return result; - } - return inst_handle.get_nanbox_f64(); } // Ordinary objects, symbols and every other non-callable heap value do not // have [[Construct]]. The historical placeholder return made `new @@ -1432,6 +1424,11 @@ pub(crate) fn is_bound_function_closure_value(value: f64) -> bool { bound_function_target_ptr(value).is_some() } +/// `value`'s `[[BoundTargetFunction]]` when it is a bound function (one layer). +pub(crate) fn bound_function_target_value(value: f64) -> Option { + bound_function_target_ptr(value).map(|ptr| crate::closure::js_closure_get_capture_f64(ptr, 0)) +} + /// Walk through any number of `Function.prototype.bind` wrapper layers to the /// ultimate non-bound target. Returns `value` unchanged when it isn't a bound /// closure (including when it isn't a closure at all) — cheap no-op on the diff --git a/crates/perry-runtime/src/object/class_registry/construct/compiled_function.rs b/crates/perry-runtime/src/object/class_registry/construct/compiled_function.rs new file mode 100644 index 0000000000..967a2ad056 --- /dev/null +++ b/crates/perry-runtime/src/object/class_registry/construct/compiled_function.rs @@ -0,0 +1,341 @@ +//! #10507: `new F()` and `x instanceof F` for an ordinary compiled function. +//! +//! A function object whose body the compiler emitted (`FN_COMPILED_BODY`) +//! and that is not an arrow, async, generator or class body is an ordinary +//! function: its `[[Construct]]` is OrdinaryCreateFromConstructor plus the +//! body, and its `@@hasInstance` is Function.prototype's (an own one needs a +//! symbol key, which moves the function object to FunctionDictionary). That +//! is a fact of the body, read from the function object's info, so these +//! paths skip every built-in, bound, proxy and native-module probe. +//! +//! The object a construction creates is described by `F.prototype`: its +//! class id and birth ShapeId are the prototype object's birth record +//! (`ObjectMeta::instance_birth`), minted on the first construction by the +//! ordinary allocate-then-link sequence and replayed afterwards — the same +//! class id, the same ShapeId (its `proto_id` is the prototype's serial) and +//! the same `meta.prototype` that sequence produces, with no hash lookup. A +//! reassigned `F.prototype` is read on the next construction; objects already +//! created keep the prototype their meta records. +use super::*; + +use crate::closure::ClosureHeader; +use crate::codegen_abi::{ + FN_ARROW, FN_ASYNC, FN_ASYNC_GENERATOR, FN_BUILTIN, FN_COMPILED_BODY, FN_GENERATOR, + FN_NON_CONSTRUCTOR, +}; + +/// Body kinds that make a compiled function something other than an +/// ordinary constructor. +const NOT_ORDINARY: u32 = + FN_ARROW | FN_ASYNC | FN_GENERATOR | FN_ASYNC_GENERATOR | FN_NON_CONSTRUCTOR | FN_BUILTIN; + +/// The function object `value` names when it is an ordinary compiled +/// function (see the module docs), else `None`. +#[inline] +pub(crate) fn ordinary_compiled_function(value: f64) -> Option { + let bits = value.to_bits(); + if bits & crate::value::TAG_MASK != crate::value::POINTER_TAG { + return None; + } + let ptr = (bits & crate::value::POINTER_MASK) as usize; + if !crate::closure::is_closure_ptr(ptr) { + return None; + } + // SAFETY: a proven, live closure cell; a non-null info is a static one. + let info = unsafe { (*(ptr as *const ClosureHeader)).info.as_ref()? }; + if info.flags & FN_COMPILED_BODY == 0 || info.flags & NOT_ORDINARY != 0 { + return None; + } + Some(ptr) +} + +/// The function's own `prototype` when it holds an ordinary object. +/// +/// `prototype` of a function is a non-configurable data property, so the +/// value in the function's own-property bag is authoritative. +/// +/// # Safety +/// `closure` is a proven, live closure cell. +#[inline] +unsafe fn own_prototype_value(closure: usize) -> Option { + match crate::closure::shape::closure_own_prototype_by_shape(closure as *const ClosureHeader) { + Some(value) => value, + None => crate::closure::props::bag_get(closure, b"prototype"), + } +} + +/// [`own_prototype_value`] when it is an ordinary object. +/// +/// # Safety +/// `closure` is a proven, live closure cell. +#[inline] +unsafe fn own_prototype_object(closure: usize) -> Option<*mut ObjectHeader> { + let value = own_prototype_value(closure)?; + let value = JSValue::from_bits(value.to_bits()); + if !value.is_pointer() { + return None; + } + let proto = value.as_pointer::() as usize; + let header = crate::value::addr_class::try_read_gc_header(proto)?; + (header.obj_type == crate::gc::GC_TYPE_OBJECT).then_some(proto as *mut ObjectHeader) +} + +/// `proto`'s birth record `(class id, birth ShapeId)`, when one was minted +/// and still names the inline size its class has learned. +/// +/// # Safety +/// `proto` is a live `ObjectHeader`. +#[inline] +unsafe fn birth_record(proto: *const ObjectHeader) -> Option<(u32, u32, u32)> { + let meta = (*proto).meta; + if meta.is_null() { + return None; + } + let word = (*meta).instance_birth; + if word == 0 { + return None; + } + let class_id = word as u32; + let shape_id = (word >> 32) as u32; + // The birth shape carries the birth live-slot bound, which is the inline + // size the class has learned; a class that has since learned a larger + // size gets a new record. + let slots = crate::object::learned_inline_field_count(class_id); + (crate::object::shapes::shape_live_inline_slot_count_by_id(shape_id) == Some(slots)) + .then_some((class_id, shape_id, slots)) +} + +/// Mint `proto`'s birth record from the first construction, which takes the +/// ordinary sequence: an object of `F`'s synthetic class, linked to `proto` +/// as its class-default prototype. The record is that class id and the +/// ShapeId the link left, pinned for the agent's life so the record can never +/// name a retired id. Returns the constructed object. +/// +/// The class is the minting function's; a class whose registered prototype is +/// later moved off `proto` clears the record +/// ([`forget_birth_record_of_class`]), so a record never outlives the pairing +/// it was minted from. +#[cold] +#[inline(never)] +unsafe fn mint_birth_record(func_value: f64, proto: *mut ObjectHeader) -> *mut ObjectHeader { + let scope = crate::gc::RuntimeHandleScope::new(); + let proto_handle = scope.root_raw_mut_ptr(proto); + let class_id = synthetic_class_id_for_function(func_value); + let slots = crate::object::learned_inline_field_count(class_id); + let obj = scope.root_raw_mut_ptr(js_object_alloc(class_id, slots)); + let proto_bits = proto_handle + .with_mut_ptr::(|proto| crate::value::js_nanbox_pointer(proto as i64)) + .to_bits(); + obj.with_mut_ptr::(|obj| { + super::super::super::prototype_chain::object_link_class_default_prototype( + obj as usize, + proto_bits, + ) + }); + let shape_id = + obj.with_mut_ptr::(|obj| crate::object::shapes::object_shape_stamp(obj)); + crate::object::shapes::note_external_shape_carrier( + crate::object::shapes::shape_descriptor_by_id(shape_id), + ); + let meta = proto_handle + .with_mut_ptr::(|proto| crate::object::object_meta_ensure(proto)); + // GC_STORE_AUDIT(POINTER_FREE): a class id and a ShapeId, never a heap + // reference. + (*meta).instance_birth = u64::from(class_id) | u64::from(shape_id) << 32; + obj.get_raw_mut_ptr::() +} + +/// The class `class_id`'s registered prototype moved from `old` to another +/// object: a birth record on `old` minted for that class no longer describes +/// a construction by its function (#10507). +/// +/// # Safety +/// `old` is the live prototype object the registry held for `class_id`. +pub(crate) unsafe fn forget_birth_record_of_class(old: *mut ObjectHeader, class_id: u32) { + if old.is_null() { + return; + } + let meta = (*old).meta; + if !meta.is_null() && (*meta).instance_birth as u32 == class_id { + // GC_STORE_AUDIT(POINTER_FREE): clears a class id / ShapeId pair. + (*meta).instance_birth = 0; + } +} + +/// An object born from `proto`'s record: class id and birth ShapeId stamped, +/// `meta.prototype` = `proto` (what the class-default link records). +/// +/// # Safety +/// `proto` is a live `ObjectHeader` marked as a prototype. +unsafe fn born_from_record( + proto: *mut ObjectHeader, + class_id: u32, + shape_id: u32, + slots: u32, +) -> *mut ObjectHeader { + let scope = crate::gc::RuntimeHandleScope::new(); + let proto_handle = scope.root_raw_mut_ptr(proto); + let obj = scope.root_raw_mut_ptr(crate::object::object_alloc_born(class_id, slots, shape_id)); + let meta = obj.with_mut_ptr::(|obj| crate::object::object_meta_ensure(obj)); + let proto_bits = proto_handle + .with_mut_ptr::(|proto| crate::value::js_nanbox_pointer(proto as i64)) + .to_bits(); + (*meta).prototype = proto_bits; + // GC_STORE_AUDIT(BARRIERED): meta-record prototype slot store — the + // record is an arena allocation, so the ordinary object-slot barrier + // applies (parent = the meta record), as in the class-default link. + crate::gc::runtime_write_barrier_slot( + meta as usize, + &(*meta).prototype as *const u64 as usize, + proto_bits, + ); + obj.get_raw_mut_ptr::() +} + +/// `new F(...args)` for an ordinary compiled function `F` (`closure`), or +/// `None` when `F.prototype` is not an ordinary object (the general path +/// handles arrays, functions and primitives there). +/// +/// # Safety +/// `closure` is the live closure `func_value` names; `args_ptr` holds +/// `args_len` values. +pub(super) unsafe fn construct_ordinary_compiled_function( + func_value: f64, + closure: usize, + args_ptr: *const f64, + args_len: usize, +) -> Option { + let scope = crate::gc::RuntimeHandleScope::new(); + let func_handle = scope.root_nanbox_f64(func_value); + let proto = match own_prototype_object(closure) { + Some(proto) => proto, + // Never read: materialize the default prototype the general path + // would, then read it back off the (possibly moved) function. + None if own_prototype_value(closure).is_none() => { + let class_id = synthetic_class_id_for_function(func_value); + ensure_function_prototype_object(func_value, class_id); + let closure = (func_handle.get_nanbox_u64() & crate::value::POINTER_MASK) as usize; + own_prototype_object(closure)? + } + None => return None, + }; + let obj = match birth_record(proto) { + Some((class_id, shape_id, slots)) => born_from_record(proto, class_id, shape_id, slots), + None => mint_birth_record(func_handle.get_nanbox_f64(), proto), + }; + let instance = crate::value::js_nanbox_pointer(obj as i64); + Some(run_constructor_body( + func_handle.get_nanbox_f64(), + instance, + args_ptr, + args_len, + true, + )) +} + +/// Run `func_value`'s body as a constructor on the fresh `instance`, with +/// `new.target` = `func_value`, and return the construction's result: an +/// object the body returns, else `instance`. `compiled`: `func_value` is an +/// ordinary compiled function ([`ordinary_compiled_function`]), whose body is +/// entered directly rather than through the generic value-call dispatcher. +/// +/// # Safety +/// `func_value` is a callable function value; `args_ptr` holds `args_len` +/// values. +pub(super) unsafe fn run_constructor_body( + func_value: f64, + instance: f64, + args_ptr: *const f64, + args_len: usize, + compiled: bool, +) -> f64 { + // #7280: the instance and the two DISPLACED new.target values are held + // across a call that runs a user constructor body — see the long note in + // `construct_registered_class_ref`. Unrooted, an evacuating minor moves the + // instance and this returns the pre-move address. + let scope = crate::gc::RuntimeHandleScope::new(); + let inst_handle = scope.root_nanbox_f64(instance); + let prev_new_target = crate::object::js_new_target_get(); + let prev_new_target_handle = scope.root_nanbox_f64(prev_new_target); + crate::object::js_new_target_set(func_value); + let prev_current_new_target = + CURRENT_NEW_TARGET.with(|value| value.replace(func_value.to_bits())); + let prev_current_new_target_handle = scope.root_nanbox_u64(prev_current_new_target); + let this = crate::closure::JsThis::from_f64(inst_handle.get_nanbox_f64()); + let result = if compiled { + let closure = (func_value.to_bits() & crate::value::POINTER_MASK) as *const ClosureHeader; + crate::closure::call_compiled_closure_this(closure, this, args_ptr, args_len) + } else { + crate::closure::native_call_value_this(func_value, this, args_ptr, args_len) + }; + CURRENT_NEW_TARGET.with(|value| value.set(prev_current_new_target_handle.get_nanbox_u64())); + crate::object::js_new_target_set(prev_new_target_handle.get_nanbox_f64()); + // Most bodies return `undefined`, which never overrides `this`. + if result.to_bits() != crate::value::TAG_UNDEFINED && constructor_return_overrides_this(result) + { + return result; + } + inst_handle.get_nanbox_f64() +} + +/// How `value instanceof F` is answered for an ordinary compiled function `F` +/// ([`ordinary_compiled_function_has_instance`]). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum OrdinaryInstanceof { + /// `value`'s ShapeId names `F.prototype` as its [[Prototype]]. + Instance, + /// OrdinaryHasInstance's prototype walk is the whole answer: `value` is an + /// ordinary object of no compiled class, whose [[Prototype]] chain is + /// exactly what its meta records and shapes say. + PrototypeWalk, +} + +/// `value instanceof F` when `F` is an ordinary compiled function, or `None` +/// for the general path (an `F` that may own `@@hasInstance`, a non-object +/// `F.prototype`, a primitive or exotic `value`, a compiled-class instance +/// whose class chain is not a prototype chain). +/// +/// A function-constructed object carries its constructor's synthetic class, +/// but the class is not the answer: `F.prototype` may have been reassigned +/// since (OrdinaryHasInstance compares prototypes, never constructors). +pub(crate) fn ordinary_compiled_function_has_instance( + value: f64, + type_ref: f64, +) -> Option { + let closure = ordinary_compiled_function(type_ref)?; + // SAFETY: `closure` is a proven, live closure cell; every pointer below is + // proven by its GC header before it is read. + unsafe { + // InstanceofOperator step 2: an own `@@hasInstance` is a symbol key, + // which only a FunctionDictionary function object can hold; the + // inherited one is Function.prototype's, which is non-writable and + // non-configurable, so OrdinaryHasInstance applies. + if !crate::closure::shape::closure_on_base_shape(closure as *const ClosureHeader) { + return None; + } + let proto = own_prototype_object(closure)?; + let bits = value.to_bits(); + if bits & crate::value::TAG_MASK != crate::value::POINTER_TAG { + return None; + } + let obj = (bits & crate::value::POINTER_MASK) as usize; + let header = crate::value::addr_class::try_read_gc_header(obj)?; + if header.obj_type != crate::gc::GC_TYPE_OBJECT { + return None; + } + let meta = (*proto).meta; + if !meta.is_null() && (*meta).proto_serial != 0 { + let shape_id = crate::object::shapes::object_shape_stamp(obj as *const ObjectHeader); + if crate::object::shapes::shape_proto_id(shape_id) == Some((*meta).proto_serial) { + return Some(OrdinaryInstanceof::Instance); + } + } + let class_id = (*(obj as *const ObjectHeader)).class_id; + (class_id == 0 || class_id >= super::super::prototype_objects::SYNTHETIC_CLASS_ID_BASE) + .then_some(OrdinaryInstanceof::PrototypeWalk) + } +} + +#[cfg(test)] +#[path = "compiled_function_tests.rs"] +mod tests; diff --git a/crates/perry-runtime/src/object/class_registry/construct/compiled_function_tests.rs b/crates/perry-runtime/src/object/class_registry/construct/compiled_function_tests.rs new file mode 100644 index 0000000000..799bd8d822 --- /dev/null +++ b/crates/perry-runtime/src/object/class_registry/construct/compiled_function_tests.rs @@ -0,0 +1,165 @@ +//! #10507: ordinary compiled function construction and `instanceof`. +use super::*; + +extern "C" fn empty_body(_closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +fn compiled_function() -> f64 { + let closure = crate::closure::js_closure_alloc( + crate::fn_info!(empty_body, 0; with_declared(0), with_flags(FN_COMPILED_BODY)), + 0, + ); + crate::value::js_nanbox_pointer(closure as i64) +} + +fn construct(func: f64) -> *mut ObjectHeader { + let value = unsafe { js_new_function_construct(func, std::ptr::null(), 0) }; + (value.to_bits() & crate::value::POINTER_MASK) as *mut ObjectHeader +} + +fn own_prototype(func: f64) -> *mut ObjectHeader { + let ptr = (func.to_bits() & crate::value::POINTER_MASK) as usize; + let value = crate::closure::closure_get_own_dynamic_prop(ptr, "prototype") + .expect("constructing materializes F.prototype"); + (value.to_bits() & crate::value::POINTER_MASK) as *mut ObjectHeader +} + +fn prototype_of(obj: *mut ObjectHeader) -> *mut ObjectHeader { + let value = + crate::object::js_object_get_prototype_of(crate::value::js_nanbox_pointer(obj as i64)); + (value.to_bits() & crate::value::POINTER_MASK) as *mut ObjectHeader +} + +fn is_instance(obj: *mut ObjectHeader, func: f64) -> bool { + crate::object::js_instanceof_dynamic(crate::value::js_nanbox_pointer(obj as i64), func) + .to_bits() + == crate::value::TAG_TRUE +} + +#[test] +fn construction_is_born_from_the_prototype_birth_record() { + let _global = crate::gc::global_side_table_test_lock(); + let _no_move = crate::gc::GcSuppressScope::new(); + let func = compiled_function(); + assert!(ordinary_compiled_function(func).is_some()); + let first = construct(func); + let second = construct(func); + let proto = own_prototype(func); + unsafe { + let word = (*(*proto).meta).instance_birth; + assert_ne!(word, 0, "the first construction mints F.prototype's record"); + assert_eq!((*first).class_id, word as u32); + assert_eq!((*second).class_id, word as u32); + assert_eq!( + crate::object::shapes::object_shape_stamp(second), + (word >> 32) as u32, + "a construction is stamped with the record's birth shape" + ); + assert_eq!( + (*second).class_id, + synthetic_class_id_for_function(func), + "a construction carries its function's class" + ); + let second_meta = (*second).meta; + assert!( + !second_meta.is_null(), + "the class-default link's meta record" + ); + assert_eq!( + (*second_meta).prototype, + crate::value::js_nanbox_pointer(proto as i64).to_bits() + ); + assert_eq!( + crate::object::shapes::object_shape_stamp(first), + crate::object::shapes::object_shape_stamp(second), + "the replayed birth is the minted one" + ); + } + assert_eq!(prototype_of(second), proto); + assert!(is_instance(second, func)); + assert_eq!( + ordinary_compiled_function_has_instance( + crate::value::js_nanbox_pointer(second as i64), + func + ), + Some(OrdinaryInstanceof::Instance), + "the shape answers `instanceof` for a construction" + ); +} + +#[test] +fn a_reassigned_prototype_leaves_earlier_constructions_alone() { + let _global = crate::gc::global_side_table_test_lock(); + let _no_move = crate::gc::GcSuppressScope::new(); + let func = compiled_function(); + let before = construct(func); + let old_proto = own_prototype(func); + let new_proto = crate::object::js_object_alloc(0, 0); + crate::object::js_set_function_prototype( + func, + crate::value::js_nanbox_pointer(new_proto as i64), + ); + let after = construct(func); + assert_eq!(prototype_of(before), old_proto); + assert_eq!(prototype_of(after), new_proto); + unsafe { + assert_eq!( + (*(*old_proto).meta).instance_birth, + 0, + "moving the class's prototype retires the old prototype's record" + ); + assert_ne!( + crate::object::shapes::object_shape_stamp(before), + crate::object::shapes::object_shape_stamp(after), + "the ShapeId names the prototype" + ); + } + assert!( + !is_instance(before, func), + "F.prototype moved away from `before`" + ); + assert!(is_instance(after, func)); + crate::object::js_set_function_prototype( + func, + crate::value::js_nanbox_pointer(old_proto as i64), + ); + assert!(is_instance(before, func)); + assert!(!is_instance(after, func)); +} + +#[test] +fn an_own_has_instance_is_never_answered_from_the_shape() { + let _global = crate::gc::global_side_table_test_lock(); + let _no_move = crate::gc::GcSuppressScope::new(); + let func = compiled_function(); + let obj = construct(func); + let obj_value = crate::value::js_nanbox_pointer(obj as i64); + assert_eq!( + ordinary_compiled_function_has_instance(obj_value, func), + Some(OrdinaryInstanceof::Instance) + ); + let has_instance = crate::symbol::well_known_symbol("hasInstance"); + let sym = f64::from_bits(crate::value::JSValue::pointer(has_instance as *const u8).bits()); + let reject = compiled_function(); + unsafe { crate::symbol::js_object_set_symbol_property(func, sym, reject) }; + assert_eq!( + ordinary_compiled_function_has_instance(obj_value, func), + None, + "a function owning @@hasInstance must reach InstanceofOperator" + ); +} + +#[test] +fn only_compiled_ordinary_bodies_take_the_lane() { + let runtime_native = + crate::closure::js_closure_alloc(crate::fn_info!(empty_body, 0; with_declared(0)), 0); + let runtime_native = crate::value::js_nanbox_pointer(runtime_native as i64); + assert!(ordinary_compiled_function(runtime_native).is_none()); + let arrow = crate::closure::js_closure_alloc( + crate::fn_info!(empty_body, 0; with_declared(0), with_flags(FN_COMPILED_BODY | FN_ARROW)), + 0, + ); + let arrow = crate::value::js_nanbox_pointer(arrow as i64); + assert!(ordinary_compiled_function(arrow).is_none()); +} diff --git a/crates/perry-runtime/src/object/class_registry/state.rs b/crates/perry-runtime/src/object/class_registry/state.rs index e65b367613..af99d4a049 100644 --- a/crates/perry-runtime/src/object/class_registry/state.rs +++ b/crates/perry-runtime/src/object/class_registry/state.rs @@ -732,6 +732,12 @@ pub(crate) fn class_prototype_object_root_store(class_id: u32, proto_ptr: *mut O } guard.as_mut().unwrap().insert(class_id, proto_ptr as usize) }); + if let Some(old) = old.filter(|&old| old != 0 && old != proto_ptr as usize) { + // SAFETY: the registry held `old` as a live root until this store. + unsafe { + super::construct::forget_birth_record_of_class(old as *mut ObjectHeader, class_id) + }; + } class_prototype_object_addr_index_rekey(old.unwrap_or(0), proto_ptr as usize); crate::gc::runtime_write_barrier_root_raw_ptr(proto_ptr); // A materialized prototype object can carry arbitrary later-added diff --git a/crates/perry-runtime/src/object/instanceof/dynamic_dispatch.rs b/crates/perry-runtime/src/object/instanceof/dynamic_dispatch.rs index af839762a9..3223ceddbe 100644 --- a/crates/perry-runtime/src/object/instanceof/dynamic_dispatch.rs +++ b/crates/perry-runtime/src/object/instanceof/dynamic_dispatch.rs @@ -9,6 +9,25 @@ use super::*; #[no_mangle] pub extern "C" fn js_instanceof_dynamic(value: f64, type_ref: f64) -> f64 { + // #10507: `x instanceof F` for an ordinary compiled function `F`: one + // shape compare when `x`'s ShapeId names `F.prototype`, else, for an + // object of no compiled class, OrdinaryHasInstance's prototype walk. + { + use crate::object::class_registry::OrdinaryInstanceof; + match crate::object::class_registry::ordinary_compiled_function_has_instance( + value, type_ref, + ) { + Some(OrdinaryInstanceof::Instance) => return f64::from_bits(crate::value::TAG_TRUE), + Some(OrdinaryInstanceof::PrototypeWalk) => { + return f64::from_bits(if ordinary_has_instance_prototype_walk(value, type_ref) { + crate::value::TAG_TRUE + } else { + crate::value::TAG_FALSE + }); + } + None => {} + } + } // Proxy ids are registry handles, not closure headers. Resolve their // observable @@hasInstance/prototype reads before any constructor probe // or unwrapping of the left operand (#10364). @@ -81,6 +100,11 @@ pub extern "C" fn js_instanceof_dynamic(value: f64, type_ref: f64) -> f64 { } } } + // OrdinaryHasInstance step 2: a bound function (with no own + // `@@hasInstance`, answered above) is `instanceof` exactly as its target. + if let Some(target) = crate::object::class_registry::bound_function_target_value(type_ref) { + return js_instanceof_dynamic(value, target); + } // OrdinaryHasInstance step 3 (#11261): a primitive is never an instance. // Only once the RHS is known callable — a non-callable RHS must still // reach the `TypeError` below (InstanceofOperator step 4 precedes diff --git a/crates/perry-runtime/src/object/meta_accessors.rs b/crates/perry-runtime/src/object/meta_accessors.rs index b75148d57a..0c0eaf6d68 100644 --- a/crates/perry-runtime/src/object/meta_accessors.rs +++ b/crates/perry-runtime/src/object/meta_accessors.rs @@ -43,6 +43,7 @@ pub(crate) unsafe fn object_meta_ensure_for_cell(user_ptr: usize) -> Option<*mut (*meta).elements = 0; (*meta).dictionary_keys = 0; (*meta).arguments = 0; + (*meta).instance_birth = 0; // GC_STORE_AUDIT(BARRIERED): header-slot store followed by an object-slot // barrier, exactly as `object_meta_ensure` does for an `ObjectHeader`. *slot = meta; @@ -93,6 +94,7 @@ pub(crate) unsafe fn object_meta_ensure(obj: *mut ObjectHeader) -> *mut ObjectMe (*meta).elements = 0; (*meta).dictionary_keys = 0; (*meta).arguments = 0; + (*meta).instance_birth = 0; // GC_STORE_AUDIT(BARRIERED): meta-record edge is a header-slot store // followed by an object-slot barrier, mirroring `set_object_keys_array`. (*obj).meta = meta; diff --git a/crates/perry-runtime/src/object/meta_record.rs b/crates/perry-runtime/src/object/meta_record.rs index ec677913cd..00f73c3047 100644 --- a/crates/perry-runtime/src/object/meta_record.rs +++ b/crates/perry-runtime/src/object/meta_record.rs @@ -198,6 +198,19 @@ pub struct ObjectMeta { /// /// Placed before `native_state` for the same reason as `proto_serial`. pub arguments: u64, + /// #10507: the birth record of the objects `new F()` creates while this + /// object is `F.prototype` — 0 until the first such construction. The low + /// half is the class id those objects carry (the minting function's + /// synthetic class); the high half is their birth ShapeId (no keys, this + /// object's serial as `proto_id`), the one the class-default link leaves. + /// Both are facts of THIS object, so a construction reaches them from + /// `F.prototype` with two loads. Cleared when that class's registered + /// prototype moves to another object. + /// + /// POD, never a heap edge. + /// + /// Placed before `native_state` for the same reason as `proto_serial`. + pub instance_birth: u64, /// #340/#341 honest tags: packed state for a runtime class whose instances /// are ORDINARY objects rather than small registry handles /// (`TextEncoder` / `TextDecoder` today; the other twelve families follow). diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 5096353482..b1f2b1fcc6 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -63,7 +63,7 @@ pub use alloc::{ js_object_alloc, js_object_alloc_fast, js_object_alloc_fast_with_parent, js_object_alloc_null_proto, js_object_alloc_with_parent, js_object_coerce, }; -pub(crate) use alloc_basic::object_alloc_plain; +pub(crate) use alloc_basic::{object_alloc_born, object_alloc_plain}; #[allow(unused_imports)] pub(crate) use alloc_plain::mark_object_plain_ordinary; pub use assign::*; diff --git a/test-files/test_gap_10507_function_constructors.ts b/test-files/test_gap_10507_function_constructors.ts new file mode 100644 index 0000000000..1e30535833 --- /dev/null +++ b/test-files/test_gap_10507_function_constructors.ts @@ -0,0 +1,89 @@ +// #10507: plain function constructors — `new F()` (with and without an +// object return), F.prototype replaced after instances exist, `instanceof` +// across a reassigned prototype, Symbol.hasInstance, bound functions, +// `new.target`, F called without `new`, ES5 inheritance, a shared prototype, +// closure-made constructors and many constructions surviving collections. +function A(this: any, v: number) { this.v = v; } +A.prototype.get = function (this: any) { return this.v; }; +const a1: any = new (A as any)(1); +console.log("a1", a1.v, a1.get(), a1 instanceof A, Object.getPrototypeOf(a1) === A.prototype, a1.constructor === A); +// returning an object overrides this +function R(this: any) { this.x = 1; return { y: 2 }; } +const r: any = new (R as any)(); +console.log("ret-obj", r.x, r.y, r instanceof R); +function RP(this: any) { this.x = 1; return 5; } +const rp: any = new (RP as any)(); +console.log("ret-prim", rp.x, rp instanceof RP); +// prototype replaced after instances exist +function P(this: any) { this.k = 1; } +P.prototype.m = function () { return "old"; }; +const p1: any = new (P as any)(); +const oldProto = P.prototype; +P.prototype = { m() { return "new"; } }; +const p2: any = new (P as any)(); +console.log("replaced", p1.m(), p2.m(), p1 instanceof P, p2 instanceof P, Object.getPrototypeOf(p1) === oldProto, Object.getPrototypeOf(p2) === P.prototype); +// instanceof across a reassigned prototype, back again +P.prototype = oldProto; +console.log("restored", p1 instanceof P, p2 instanceof P); +// Symbol.hasInstance on a function +function H(this: any) {} +Object.defineProperty(H, Symbol.hasInstance, { value: (v: any) => v === 42 }); +console.log("hasInstance", (42 as any) instanceof (H as any), new (H as any)() instanceof (H as any)); +// bound function +function B(this: any, a: number, b: number) { this.s = a + b; } +const BB: any = (B as any).bind(null, 10); +const bb: any = new BB(5); +console.log("bound", bb.s, bb instanceof B, bb instanceof BB); +// new.target +function NT(this: any) { this.t = new.target === NT; this.u = typeof new.target; } +const nt: any = new (NT as any)(); +console.log("new.target", nt.t, nt.u); +function NT2(this: any) { return typeof new.target; } +console.log("called", (NT2 as any)()); +// called without new +function S(this: any, v: number): any { if (!(this instanceof S)) return new (S as any)(v); this.v = v; } +const s1: any = (S as any)(3); +const s2: any = new (S as any)(4); +console.log("self-new", s1.v, s2.v, s1 instanceof S, s2 instanceof S); +// inheritance ES5 style +function Base(this: any, n: string) { this.n = n; } +Base.prototype.hi = function (this: any) { return "hi " + this.n; }; +function Child(this: any, n: string) { (Base as any).call(this, n); this.c = true; } +Child.prototype = Object.create(Base.prototype); +Child.prototype.constructor = Child; +const ch: any = new (Child as any)("z"); +console.log("inherit", ch.hi(), ch instanceof Child, ch instanceof Base, ch.constructor === Child, Object.getPrototypeOf(Object.getPrototypeOf(ch)) === Base.prototype); +// shared prototype between two functions +function F1(this: any) { this.a = 1; } +function F2(this: any) { this.b = 2; } +F2.prototype = F1.prototype; +const f1: any = new (F1 as any)(), f2: any = new (F2 as any)(); +console.log("shared", f1 instanceof F2, f2 instanceof F1, Object.getPrototypeOf(f2) === F1.prototype); +// fields, keys, JSON, class name print +function Pt(this: any, x: number, y: number) { this.x = x; this.y = y; } +const pts: any[] = []; +for (let i = 0; i < 50; i++) pts.push(new (Pt as any)(i, i * 2)); +console.log("keys", Object.keys(pts[3]).join(","), JSON.stringify(pts[49]), pts[10].x + pts[10].y); +console.log(String(Object.getPrototypeOf(pts[0]) === Pt.prototype), pts[0].hasOwnProperty("x"), "x" in pts[0], Pt.prototype.isPrototypeOf(pts[2])); +// prototype method added after construction +(Pt.prototype as any).len = function (this: any) { return Math.sqrt(this.x * this.x + this.y * this.y); }; +console.log("late", pts[3].len().toFixed(3)); +// arrow is not a constructor +try { const Arr: any = () => 1; new Arr(); console.log("arrow constructed"); } catch (e: any) { console.log("arrow", e instanceof TypeError); } +// closure-made constructors (decimal-like) +function mk() { function D(this: any, v: number): any { if (!(this instanceof D)) return new (D as any)(v); this.v = v; } D.prototype.plus = function (this: any, y: number) { return new (this.constructor as any)(this.v + y); }; D.prototype.constructor = D; return D; } +const D1: any = mk(), D2: any = mk(); +const d1 = new D1(1).plus(2), d2 = D2(5); +console.log("closure-ctor", d1.v, d2.v, d1 instanceof D1, d1 instanceof D2, d2 instanceof D2); +// getters on prototype, Object.create on a fn prototype +Object.defineProperty(Pt.prototype, "sum", { get(this: any) { return this.x + this.y; } }); +console.log("getter", pts[4].sum, Object.create(Pt.prototype) instanceof Pt); +// setPrototypeOf on an instance +const moved: any = new (Pt as any)(1, 1); +Object.setPrototypeOf(moved, A.prototype); +console.log("setproto", moved instanceof Pt, moved instanceof A); +// many instances surviving GC +let keep: any[] = []; +for (let i = 0; i < 200000; i++) { const o: any = new (Pt as any)(i, 1); if (i % 1000 === 0) keep.push(o); } +let tot = 0; for (const o of keep) tot += o.len() > 0 && o instanceof Pt ? o.x : 0; +console.log("gc", keep.length, tot);