From 117a71c3675f5fea2d3dfc944f5557f3920ebc1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 3 Oct 2026 10:55:14 +0200 Subject: [PATCH 1/4] perf: construct plain function objects from the prototype's birth record (#10507) `new F()` and `x instanceof F` on a plain `function` went through every exotic-callee probe (builtins by code pointer, bound, proxy, native-module exports), three hash tables keyed by address (FUNCTION_CLASS_IDS, CLASS_PROTOTYPE_OBJECTS, the own-symbol table) and two shape interns per construction. - perry-abi: FN_COMPILED_BODY marks every info perry-codegen renders, so an ordinary compiled function is a fact of its body, decided once. - Construction reads F.prototype through F's ShapeId (a per-agent ShapeId to slot cache) and replays the birth record kept on the prototype object (ObjectMeta::instance_birth: class id and birth ShapeId, minted by the first ordinary construction, cleared when that class's registered prototype moves), then enters the body directly. - instanceof with an ordinary compiled F is one shape compare when x's ShapeId names F.prototype, else OrdinaryHasInstance's prototype walk for an object of no compiled class: an object created before F.prototype was reassigned is no longer an instance. A bound function answers for its target. - A method call whose name a class also declares sends receivers of no such class to the method site (direct call of an inherited F.prototype method) instead of the own-property probe and the by-name dispatcher. Instructions per op (issue repro): new F 5,048 -> 1,898, instanceof 2,809 -> 425, decimal.js-shaped x.plus(i) 15,848 -> 5,003. tsc -1.43%, Zod -0.13%, outputs equal to node. --- .../PENDING-10507-function-constructors.md | 15 + crates/perry-abi/src/lib.rs | 7 + crates/perry-codegen/src/fn_info.rs | 19 +- .../property_get/dynamic_dispatch.rs | 53 ++- crates/perry-runtime/src/closure/dispatch.rs | 2 +- .../src/closure/dispatch/value_call.rs | 33 ++ crates/perry-runtime/src/closure/mod.rs | 2 +- crates/perry-runtime/src/closure/shape.rs | 87 +++++ .../perry-runtime/src/object/alloc_basic.rs | 36 ++ .../src/object/class_registry.rs | 9 +- .../src/object/class_registry/construct.rs | 51 ++- .../construct/compiled_function.rs | 341 ++++++++++++++++++ .../construct/compiled_function_tests.rs | 165 +++++++++ .../src/object/class_registry/state.rs | 6 + .../src/object/instanceof/dynamic_dispatch.rs | 24 ++ .../src/object/meta_accessors.rs | 2 + .../perry-runtime/src/object/meta_record.rs | 13 + crates/perry-runtime/src/object/mod.rs | 2 +- .../test_gap_10507_function_constructors.ts | 89 +++++ 19 files changed, 908 insertions(+), 48 deletions(-) create mode 100644 changelog.d/PENDING-10507-function-constructors.md create mode 100644 crates/perry-runtime/src/object/class_registry/construct/compiled_function.rs create mode 100644 crates/perry-runtime/src/object/class_registry/construct/compiled_function_tests.rs create mode 100644 test-files/test_gap_10507_function_constructors.ts diff --git a/changelog.d/PENDING-10507-function-constructors.md b/changelog.d/PENDING-10507-function-constructors.md new file mode 100644 index 0000000000..185a99511d --- /dev/null +++ b/changelog.d/PENDING-10507-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); From 8499c31ed7b49b541fac9eeca3e35e51a7c7890e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 3 Oct 2026 10:55:35 +0200 Subject: [PATCH 2/4] changelog: key the function-constructor fragment to PR 11787 --- ...07-function-constructors.md => 11787-function-constructors.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{PENDING-10507-function-constructors.md => 11787-function-constructors.md} (100%) diff --git a/changelog.d/PENDING-10507-function-constructors.md b/changelog.d/11787-function-constructors.md similarity index 100% rename from changelog.d/PENDING-10507-function-constructors.md rename to changelog.d/11787-function-constructors.md From d599a0ef137055fc8f415ec8b6753ee69dc77cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 3 Oct 2026 10:14:24 +0000 Subject: [PATCH 3/4] perf: serve function-valued stores from the emitted store cache A ConstFn lane (a slot holding a closure of one static body) was refused by both halves of the static-key store site: the key-add memo and the existing-key word. Every function-valued store therefore took the runtime miss (~2,800 instructions per key-add on the #10507 decimal row). The site gains a fifth word naming its one ConstFn body (claimed once by the primary agent, never changed). A key-add memo whose successor's lane is ConstFn carries a guard flag (bit 14), and an existing-key word or way for a ConstFn slot carries bit 62. The emitted hit then admits only a closure of the site body: POINTER tag above the handle band, GcHeader kind CLOSURE and not forwarded, ClosureHeader::info equal to the site word, and not a rebindable `this` clone. Any other value takes the miss, whose checked funnel deprecates the lane. The runtime memo (packed_add_try) applies the same admission. ABI: perry-abi PACKED_SET_SITE_WORDS / PACKED_SET_CONSTFN_INFO_WORD and the two flag bits, pinned by packed_set_site_layout_matches_codegen. --- .../PENDING-constfn-function-stores.md | 11 ++ crates/perry-abi/src/lib.rs | 27 ++++ .../src/expr/put_value_store_ic.rs | 149 ++++++++++++++++-- .../src/expr/write_pic_barrier_tests.rs | 85 ++++++++-- .../tests/native_proof_regressions.rs | 7 +- .../proxy/put_value/cached_constfn_tests.rs | 99 ++++++++++-- .../src/proxy/put_value/packed_add.rs | 139 ++++++++++++++-- .../src/proxy/put_value/packed_add_tests.rs | 36 ++++- .../src/proxy/put_value/packed_set.rs | 50 +++++- test-files/_helpers/constfn_store_worker.ts | 10 ++ .../test_gap_constfn_function_stores.ts | 88 +++++++++++ 11 files changed, 635 insertions(+), 66 deletions(-) create mode 100644 changelog.d/PENDING-constfn-function-stores.md create mode 100644 test-files/_helpers/constfn_store_worker.ts create mode 100644 test-files/test_gap_constfn_function_stores.ts diff --git a/changelog.d/PENDING-constfn-function-stores.md b/changelog.d/PENDING-constfn-function-stores.md new file mode 100644 index 0000000000..4e0e56656d --- /dev/null +++ b/changelog.d/PENDING-constfn-function-stores.md @@ -0,0 +1,11 @@ +Serve function-valued stores from the emitted store cache. Storing a closure +into an object (`this.constructor = F`, `this.cb = fn`, `o.m = function(){}`) +gives its slot a ConstFn lane naming the body, and both the key-add memo and +the existing-key word refused such a lane, so every store took the runtime +miss (about 2,800 instructions per key-add). A site now records one ConstFn +body (a fifth site word); a ConstFn-flagged entry hits only for a closure of +that body (a GC-header kind test, an info compare and a capture-flag test), +and any other value keeps the checked miss, which deprecates the lane as +before. Micro rows: `new C()` storing `this.k = C` 5,013 -> 2,189 +instructions/op, an `o.cb = fn` overwrite 2,026 -> 202; the decimal.js-shaped +#10507 row 5,003 -> 2,387. diff --git a/crates/perry-abi/src/lib.rs b/crates/perry-abi/src/lib.rs index 86932e5d9f..f57b782ac0 100644 --- a/crates/perry-abi/src/lib.rs +++ b/crates/perry-abi/src/lib.rs @@ -587,3 +587,30 @@ pub const METHOD_SITE_CONSTFN: u64 = 1 << 59; pub const METHOD_SITE_INDEX_MASK: u64 = (1 << 59) - 1; /// `object::ObjectMeta::spill` (the object-owned overflow buffer). pub const OBJECT_META_SPILL_OFFSET: usize = 32; + +/// `proxy::put_value::packed_add::PackedSetSite` — the static-key store +/// site (`@perry_ic_N_packed_set`) the emitted `o.k = v` reads +/// (`perry-codegen/src/expr/put_value_store_ic.rs`): the existing-key word, +/// the primary key-add memo `{shapes, guard}`, the runtime's add-way block, +/// and the site's ConstFn body. +pub const PACKED_SET_SITE_WORDS: usize = 5; +/// The site's one ConstFn body: the `JsFunctionInfo` address every +/// ConstFn-flagged entry of the site (existing-key word, existing-key way, +/// add memo) names, 0 until the first is published. Written once, before +/// the first flagged entry, and never changed: a flagged entry hits only for +/// a closure of exactly this body. +pub const PACKED_SET_CONSTFN_INFO_WORD: usize = 4; +/// The existing-key word's (and way's) bit for a slot whose lane is ConstFn +/// in the word's ShapeId: the emitted hit stores only a closure of the site's +/// body. Bit 63 is the `F64` lane bit; the slot index is below bit 62. +pub const PACKED_SET_CONSTFN_SLOT: u64 = 1 << 62; +/// The key-add guard's bit for a successor whose lane at the slot is +/// ConstFn (the slot field is the guard's low 16 bits; bit 15 is the `F64` +/// lane bit, the index is below bit 14). +pub const PACKED_ADD_CONSTFN_SLOT: u64 = 1 << 14; +/// `closure::CAPTURES_THIS_FLAG` / `closure::NO_THIS_REBIND_FLAG`, the high +/// bits of `ClosureHeader::capture_count`. A closure with the first and not +/// the second is a rebindable `this` clone, which never satisfies a ConstFn +/// claim (`field_rep_store::constfn_store_info`). +pub const CLOSURE_CAPTURES_THIS_FLAG: u32 = 0x8000_0000; +pub const CLOSURE_NO_THIS_REBIND_FLAG: u32 = 0x4000_0000; diff --git a/crates/perry-codegen/src/expr/put_value_store_ic.rs b/crates/perry-codegen/src/expr/put_value_store_ic.rs index 4e768a604b..972d6022f7 100644 --- a/crates/perry-codegen/src/expr/put_value_store_ic.rs +++ b/crates/perry-codegen/src/expr/put_value_store_ic.rs @@ -114,13 +114,22 @@ pub(crate) const PACKED_SET_INLINE_WAYS: usize = 4; const BOXED_TAG_FIRST_TOP16: &str = "32761"; const BOXED_TAG_SPAN: &str = "7"; -/// Words of a site's record `@perry_ic_N_packed_set` (`[4 x i64]`): the -/// existing-key word, the key-add memo's shapes and guard words, and the -/// runtime's pointer to further key-add memos (never read here). **Must +/// Words of a site's record `@perry_ic_N_packed_set` (`[5 x i64]`): the +/// existing-key word, the key-add memo's shapes and guard words, the +/// runtime's pointer to further key-add memos, and the site's ConstFn body +/// (`perry_abi::PACKED_SET_CONSTFN_INFO_WORD`). **Must /// equal `perry_runtime::proxy::put_value::packed_add::{PACKED_SET_SITE_WORDS, /// ADD_SHAPES_WORD, ADD_GUARD_WORD, ADD_SLOT_BITS}`**; pinned by the runtime's /// `packed_set_site_layout_matches_codegen`. -pub(crate) const PACKED_SET_SITE_WORDS: usize = 4; +pub(crate) const PACKED_SET_SITE_WORDS: usize = crate::runtime_abi::PACKED_SET_SITE_WORDS; +/// The site word naming its ConstFn body (a `JsFunctionInfo` address, 0 = +/// none); a ConstFn-flagged entry hits only for a closure whose info word +/// equals it. +const CONSTFN_INFO_WORD: usize = crate::runtime_abi::PACKED_SET_CONSTFN_INFO_WORD; +/// The existing-key word's ConstFn bit (`perry_abi::PACKED_SET_CONSTFN_SLOT`). +const PACKED_SET_CONSTFN_SLOT: u64 = crate::runtime_abi::PACKED_SET_CONSTFN_SLOT; +/// The key-add guard's ConstFn bit (`perry_abi::PACKED_ADD_CONSTFN_SLOT`). +const ADD_CONSTFN_SLOT: u64 = crate::runtime_abi::PACKED_ADD_CONSTFN_SLOT; pub(crate) const ADD_SHAPES_WORD: usize = 1; pub(crate) const ADD_GUARD_WORD: usize = 2; pub(crate) const ADD_SLOT_BITS: u32 = 16; @@ -144,10 +153,13 @@ pub(crate) const ADD_WAY_PROBES: usize = 2; /// whose lane at the slot is not `Any`. **Must equal /// `perry_runtime::proxy::put_value::packed_add::ADD_F64_SLOT`.** const ADD_F64_SLOT: u64 = 1 << (ADD_SLOT_BITS - 1); -const ADD_SLOT_MASK: u64 = ADD_F64_SLOT - 1; -/// The store word's slot half without its top bit, the runtime's -/// `packed_set::PACKED_SET_F64_SLOT` (the word's sign bit). -const PACKED_SLOT_INDEX_MASK: &str = "2147483647"; +const ADD_SLOT_MASK: u64 = ADD_CONSTFN_SLOT - 1; +const _: () = assert!(ADD_CONSTFN_SLOT == 1 << (ADD_SLOT_BITS - 2)); +/// The store word's slot half without its two flag bits: the runtime's +/// `packed_set::PACKED_SET_F64_SLOT` (the word's sign bit) and +/// `PACKED_SET_CONSTFN_SLOT` (bit 62). +const PACKED_SLOT_INDEX_MASK: &str = "1073741823"; +const _: () = assert!(PACKED_SET_CONSTFN_SLOT == 1 << 62); /// A double's exponent field: all ones = an INT32/tagged box, an infinity or /// a NaN, the values an `F64` lane refuses inline (DESIGN §3.2). const F64_EXP_MASK: &str = "9218868437227405312"; // 0x7FF0_0000_0000_0000 @@ -256,7 +268,7 @@ pub(crate) fn emit_static_store_ic( ); ctx.typed_parse_rodata.push(format!( "@{packed_name} = private global [{PACKED_SET_SITE_WORDS} x i64] \ - [i64 {PACKED_SET_EMPTY}, i64 {PACKED_SET_EMPTY}, i64 0, i64 0], align 8" + [i64 {PACKED_SET_EMPTY}, i64 {PACKED_SET_EMPTY}, i64 0, i64 0, i64 0], align 8" )); let packed_ref = format!("@{packed_name}"); @@ -435,12 +447,28 @@ pub(crate) fn emit_static_store_ic( // a finite double, stored inline as is; anything else (a box, an // infinity, a NaN) takes the miss, whose store is the checked funnel // (canonicalize, or generalize the lane with the shape word first). + // + // A word with bit 62 set names a ConstFn lane: only a closure of the + // site's one body is stored inline (`emit_constfn_value_check`). ctx.current_block = rep_idx; + let flags = ctx.block().lshr(I64, &word, "62"); + let plain = ctx.block().icmp_eq(I64, &flags, "0"); + let lane_idx = ctx.new_block(&format!("{STORE_IC_STEM}.hit.lane")); + let lane_label = ctx.block_label(lane_idx); + ctx.block().cond_br(&plain, &store_label, &lane_label); + ctx.current_block = lane_idx; let f64_slot = ctx.block().icmp_slt(I64, &word, "0"); + let f64_idx = ctx.new_block(&format!("{STORE_IC_STEM}.hit.f64")); + let f64_label = ctx.block_label(f64_idx); + let constfn_idx = ctx.new_block(&format!("{STORE_IC_STEM}.hit.constfn")); + let constfn_label = ctx.block_label(constfn_idx); + ctx.block().cond_br(&f64_slot, &f64_label, &constfn_label); + ctx.current_block = f64_idx; let exponent = ctx.block().and(I64, value_bits, F64_EXP_MASK); let boxed = ctx.block().icmp_eq(I64, &exponent, F64_EXP_MASK); - let refuse = ctx.block().and(I1, &f64_slot, &boxed); - ctx.block().cond_br(&refuse, &miss_label, &store_label); + ctx.block().cond_br(&boxed, &miss_label, &store_label); + ctx.current_block = constfn_idx; + emit_constfn_value_check(ctx, &packed_ref, value_bits, &store_label, &miss_label); // The store, then the GC's obligations for the bits actually stored. ctx.current_block = store_idx; @@ -475,6 +503,7 @@ pub(crate) fn emit_static_store_ic( }; let add_end_label = emit_key_add_hit( ctx, + &packed_ref, &shapes, &pair_ptr, &handle, @@ -540,6 +569,7 @@ pub(crate) fn emit_static_store_ic( #[allow(clippy::too_many_arguments)] fn emit_key_add_hit( ctx: &mut FnCtx<'_>, + packed_ref: &str, shapes: &str, pair_ptr: &str, handle: &str, @@ -573,13 +603,29 @@ fn emit_key_add_hit( // Charter step 5 (P2c): a memo whose successor has an `F64` lane at the // slot admits only a value whose exponent is not all ones (a finite // double); the miss serves the rest, before anything is stamped. + // A memo whose successor's lane is ConstFn admits only a closure of the + // site's one body (`emit_constfn_value_check`), also before any stamp. ctx.current_block = rep_idx; - let flag = ctx.block().and(I64, &guard, &ADD_F64_SLOT.to_string()); - let f64_slot = ctx.block().icmp_ne(I64, &flag, "0"); + let flags = ctx + .block() + .and(I64, &guard, &(ADD_F64_SLOT | ADD_CONSTFN_SLOT).to_string()); + let plain = ctx.block().icmp_eq(I64, &flags, "0"); + let lane_idx = ctx.new_block(&format!("{ADD_STEM}.lane")); + let lane_label = ctx.block_label(lane_idx); + ctx.block().cond_br(&plain, &obj_label, &lane_label); + ctx.current_block = lane_idx; + let f64_slot = ctx.block().icmp_eq(I64, &flags, &ADD_F64_SLOT.to_string()); + let f64_idx = ctx.new_block(&format!("{ADD_STEM}.f64")); + let f64_label = ctx.block_label(f64_idx); + let constfn_idx = ctx.new_block(&format!("{ADD_STEM}.constfn")); + let constfn_label = ctx.block_label(constfn_idx); + ctx.block().cond_br(&f64_slot, &f64_label, &constfn_label); + ctx.current_block = f64_idx; let exponent = ctx.block().and(I64, value_bits, F64_EXP_MASK); let boxed = ctx.block().icmp_eq(I64, &exponent, F64_EXP_MASK); - let refuse = ctx.block().and(I1, &f64_slot, &boxed); - ctx.block().cond_br(&refuse, miss_label, &obj_label); + ctx.block().cond_br(&boxed, miss_label, &obj_label); + ctx.current_block = constfn_idx; + emit_constfn_value_check(ctx, packed_ref, value_bits, &obj_label, miss_label); // The GcHeader's first word (obj_type | gc_flags << 8 | _reserved << 16). // No receiver-kind admission: the memo's pre-shape is an `Ordinary` shape @@ -646,6 +692,79 @@ fn emit_key_add_hit( end } +/// The ConstFn store check (a word or memo flagged ConstFn): `value_bits` is +/// a function object of the site's one body, so storing it keeps the +/// ShapeId's body claim. Branches to `ok_label` or `miss_label`; nothing is +/// written, nothing can collect. +/// +/// ```text +/// POINTER tag, payload above the handle band the fused receiver test +/// GcHeader (type, flags) GC_TYPE_CLOSURE, not FORWARDED +/// ClosureHeader::info == site word CONSTFN_INFO_WORD (!= 0) +/// ClosureHeader::capture_count flags not a rebindable `this` clone +/// ``` +/// +/// The same three loads as the method site's closure test +/// (`expr/method_site.rs`) and the guarded direct call +/// (`lower_call/early_branches.rs`); the shape, not the closure, owns the +/// body fact, so a factory closure (same body, other captures) is admitted. +/// An arrow capturing `this` is refused here although the runtime admits it +/// (`field_rep_store::constfn_store_info`): its miss stores it correctly. +fn emit_constfn_value_check( + ctx: &mut FnCtx<'_>, + packed_ref: &str, + value_bits: &str, + ok_label: &str, + miss_label: &str, +) { + use crate::expr::receiver_range::{emit_field_ptr, emit_fused_receiver_test}; + let header_idx = ctx.new_block(&format!("{STORE_IC_STEM}.constfn.header")); + let header_label = ctx.block_label(header_idx); + let fused = emit_fused_receiver_test(ctx.block(), value_bits); + ctx.block() + .cond_br(&fused.is_object_pointer, &header_label, miss_label); + ctx.current_block = header_idx; + let kind_mask = u16::from(crate::runtime_abi::GC_FLAG_FORWARDED) << 8 | 0xFF; + let blk = ctx.block(); + let kind_ptr = emit_field_ptr( + blk, + &fused.biased, + -(crate::runtime_abi::GC_HEADER_SIZE as i64), + ); + let kind_flags = blk.load(crate::types::I16, &kind_ptr); + let masked = blk.and(crate::types::I16, &kind_flags, &kind_mask.to_string()); + let kind_ok = blk.icmp_eq( + crate::types::I16, + &masked, + &crate::runtime_abi::GC_TYPE_CLOSURE.to_string(), + ); + let info_ptr = emit_field_ptr( + blk, + &fused.biased, + crate::runtime_abi::CLOSURE_INFO_OFFSET as i64, + ); + let info = blk.load(I64, &info_ptr); + let body_ptr = blk.gep(I64, packed_ref, &[(I64, &CONSTFN_INFO_WORD.to_string())]); + let body = blk.load_atomic_monotonic(I64, &body_ptr, 8); + let info_eq = blk.icmp_eq(I64, &info, &body); + let body_set = blk.icmp_ne(I64, &body, "0"); + let count_ptr = emit_field_ptr(blk, &fused.biased, 0); + let count = blk.load(I32, &count_ptr); + let this_flags = crate::runtime_abi::CLOSURE_CAPTURES_THIS_FLAG + | crate::runtime_abi::CLOSURE_NO_THIS_REBIND_FLAG; + let count_flags = blk.and(I32, &count, &(this_flags as i32).to_string()); + let rebindable = blk.icmp_eq( + I32, + &count_flags, + &(crate::runtime_abi::CLOSURE_CAPTURES_THIS_FLAG as i32).to_string(), + ); + let not_rebindable = blk.xor(I1, &rebindable, "true"); + let body_ok = blk.and(I1, &info_eq, &body_set); + let closure_ok = blk.and(I1, &kind_ok, ¬_rebindable); + let ok = blk.and(I1, &body_ok, &closure_ok); + blk.cond_br(&ok, ok_label, miss_label); +} + /// The GC obligations after an object slot store. Plain doubles and scalar /// tags need no bookkeeping; pointer-bearing values need string alias demotion /// and the generational/incremental write barrier. Object layout is now a diff --git a/crates/perry-codegen/src/expr/write_pic_barrier_tests.rs b/crates/perry-codegen/src/expr/write_pic_barrier_tests.rs index c2b7591ac1..17fc8e49e9 100644 --- a/crates/perry-codegen/src/expr/write_pic_barrier_tests.rs +++ b/crates/perry-codegen/src/expr/write_pic_barrier_tests.rs @@ -482,19 +482,41 @@ fn store_ic_hit_reads_no_per_object_receiver_fact() { !kind.contains("load i32") && !kind.contains(", 128") && !kind.contains(", 768"), "the hit reads no class id and tests no proof / ordinary-mark bit:\n{kind}" ); - // The store check (DESIGN §3.2): a word whose sign bit is set (an `F64` - // lane) refuses a value whose exponent is all ones to the miss; every - // other store goes on to the store block. + // The store check (DESIGN §3.2): a word with neither flag set (bits 63 + // and 62 clear) stores at once. A word whose sign bit is set (an `F64` + // lane) refuses a value whose exponent is all ones to the miss; a word + // with bit 62 set (a ConstFn lane) admits only a closure of the site body. let rep = block(&ir, HIT_REP).unwrap_or_else(|| panic!("rep block:\n{ir}")); assert!( - rep.contains("icmp slt i64") && rep.contains("9218868437227405312"), - "the rep block must test the word's F64 flag and the value's exponent:\n{rep}" + rep.contains("lshr i64") && rep.contains(", 62"), + "the rep block must read the word flag bits (63, 62):\n{rep}" ); let (_, r_true, r_false) = branch_targets(rep.lines().last().unwrap_or("").trim()); assert!( - label_is(&r_true, "put.pic.miss") && label_is(&r_false, HIT_STORE), - "a refused value misses, anything else stores:\n{rep}" + label_is(&r_true, HIT_STORE) && label_is(&r_false, "put.pic.hit.lane"), + "a plain word stores, a flagged word goes on to its lane check:\n{rep}" ); + let lane = block(&ir, "put.pic.hit.lane").unwrap_or_else(|| panic!("lane block:\n{ir}")); + assert!( + lane.contains("icmp slt i64"), + "the lane block must test the word's F64 flag:\n{lane}" + ); + let (_, l_true, l_false) = branch_targets(lane.lines().last().unwrap_or("").trim()); + assert!( + label_is(&l_true, "put.pic.hit.f64") && label_is(&l_false, "put.pic.hit.constfn"), + "the F64 flag picks the exponent test, otherwise the ConstFn check:\n{lane}" + ); + let f64_lane = block(&ir, "put.pic.hit.f64").unwrap_or_else(|| panic!("f64 block:\n{ir}")); + assert!( + f64_lane.contains("9218868437227405312"), + "the F64 lane must test the value's exponent:\n{f64_lane}" + ); + let (_, f_true, f_false) = branch_targets(f64_lane.lines().last().unwrap_or("").trim()); + assert!( + label_is(&f_true, "put.pic.miss") && label_is(&f_false, HIT_STORE), + "a refused value misses, anything else stores:\n{f64_lane}" + ); + assert_constfn_body_compare(&ir); // No other edge reaches the store. let into_store = ir @@ -508,8 +530,53 @@ fn store_ic_hit_reads_no_per_object_receiver_fact() { }) .count(); assert_eq!( - into_store, 1, - "only the store check may enter the store:\n{ir}" + into_store, 3, + "only the plain word, the F64 lane and the ConstFn check may enter the store:\n{ir}" + ); +} + +/// The ConstFn value check admits a closure only when the closure's +/// `ClosureHeader::info` equals the site's body word (word 4 of the packed +/// record, loaded atomically). A compare of a value with itself, or one that +/// never reads the site word, admits a closure of ANY body. Sabotage: +/// comparing the body word with itself turns this red. +fn assert_constfn_body_compare(ir: &str) { + let header = block(ir, "put.pic.constfn.header") + .unwrap_or_else(|| panic!("the ConstFn value check block:\n{ir}")); + let body_load = header + .lines() + .find(|l| l.contains("load atomic i64") && l.contains("monotonic")) + .unwrap_or_else(|| panic!("the site body word is loaded atomically:\n{header}")); + let body_reg = body_load + .trim() + .split(" = ") + .next() + .unwrap_or("") + .to_string(); + let site_gep = header.lines().any(|l| { + l.contains("getelementptr") + && l.contains("_packed_set") + && l.trim_end().ends_with(", i64 4") + }); + assert!( + site_gep, + "the body word is word 4 of the site record:\n{header}" + ); + let cmp = header + .lines() + .find(|l| l.contains("icmp eq i64") && l.contains(&body_reg)) + .unwrap_or_else(|| panic!("the body word must be compared:\n{header}")); + let ops: Vec<&str> = cmp + .trim() + .split("icmp eq i64") + .nth(1) + .unwrap_or("") + .split(',') + .map(str::trim) + .collect(); + assert!( + ops.len() == 2 && ops[0] != ops[1], + "the closure's info must be compared with the site body word, not with itself:\n{cmp}" ); } diff --git a/crates/perry-codegen/tests/native_proof_regressions.rs b/crates/perry-codegen/tests/native_proof_regressions.rs index abf737967a..f9b9e1641e 100644 --- a/crates/perry-codegen/tests/native_proof_regressions.rs +++ b/crates/perry-codegen/tests/native_proof_regressions.rs @@ -15484,11 +15484,12 @@ fn static_put_value_uses_write_pic_for_call_free_rhs() { ); assert!( ir.contains( - "_packed_set = private global [4 x i64] \ - [i64 4294967295, i64 4294967295, i64 0, i64 0]" + "_packed_set = private global [5 x i64] \ + [i64 4294967295, i64 4294967295, i64 0, i64 0, i64 0]" ), "the site record must be born EMPTY: its existing-key word and its key-add \ - pre-shape word both 0xFFFF_FFFF, which no receiver word equals:\n{ir}" + pre-shape word both 0xFFFF_FFFF, which no receiver word equals, and no \ + ConstFn body:\n{ir}" ); assert!( ir.contains("put.add.check") && ir.contains("put.add.hit.store"), diff --git a/crates/perry-runtime/src/proxy/put_value/cached_constfn_tests.rs b/crates/perry-runtime/src/proxy/put_value/cached_constfn_tests.rs index 46c9758795..921170ec5a 100644 --- a/crates/perry-runtime/src/proxy/put_value/cached_constfn_tests.rs +++ b/crates/perry-runtime/src/proxy/put_value/cached_constfn_tests.rs @@ -1,5 +1,7 @@ -//! Raw emitted stores may cache other slots of a completed ConstFn shape, -//! but a write to its SPECIAL slot must retain the checked store funnel. +//! Raw emitted stores may cache other slots of a completed ConstFn shape. +//! A write to its SPECIAL slot is published only FLAGGED, naming the site's +//! one body: the hit then admits only a closure of that body, and every +//! other value keeps the checked store funnel. use super::*; use crate::object::shapes::{object_shape_stamp, shape_descriptor_by_id, ShapeObjectKind}; @@ -41,6 +43,11 @@ fn birth() -> f64 { value } +fn body_of(value: f64) -> u64 { + unsafe { crate::object::field_rep_store::constfn_store_info(value.to_bits()) } + .expect("a permanent-image closure names its body") +} + fn object(value: f64) -> *mut crate::ObjectHeader { (value.to_bits() & POINTER_MASK) as *mut crate::ObjectHeader } @@ -82,7 +89,7 @@ fn assert_stored(receiver: f64, method: *const crate::StringHeader, value: f64) } #[test] -fn packed_set_refuses_special_but_keeps_other_slots_of_the_completed_shape() { +fn packed_set_flags_special_with_the_site_body_and_keeps_other_slots() { let _lock = crate::gc::global_side_table_test_lock(); let _no_gc = crate::gc::GcSuppressScope::new(); let method = key(b"cached_cf_packed_method"); @@ -96,12 +103,28 @@ fn packed_set_refuses_special_but_keeps_other_slots_of_the_completed_shape() { let site: &'static PackedSetSite = Box::leak(Box::new(PackedSetSite::empty())); let mut ways = packed_set_cache_empty(); let mut slot: PackedSetWaysSlot = &mut ways; - unsafe { prime_packed_set(first, method, &mut slot, &site.set) }; - assert_eq!(site.set.load(Ordering::Relaxed), PACKED_SET_EMPTY); - assert!(ways[..PACKED_SET_WAYS] + // A site without a record (the full-outline form) has no body word: + // the SPECIAL slot keeps the checked miss. + let mut bare_ways = packed_set_cache_empty(); + let mut bare_slot: PackedSetWaysSlot = &mut bare_ways; + unsafe { prime_packed_set(first, method, &mut bare_slot, std::ptr::null()) }; + assert!(bare_ways[..PACKED_SET_WAYS] .iter() .all(|w| *w == PACKED_SET_EMPTY)); + unsafe { prime_packed_set(first, method, &mut slot, &site.set) }; + let word = site.set.load(Ordering::Relaxed); + assert_eq!(word as u32, final_id, "a SPECIAL slot primes the word"); + assert_ne!(word & PACKED_SET_CONSTFN_SLOT, 0, "flagged ConstFn"); + assert_eq!(word & PACKED_SET_F64_SLOT, 0); + assert_eq!(((word & !PACKED_SET_FLAGS) >> 32) as u32, 2, "slot index"); + assert_eq!( + site.constfn_info.load(Ordering::Relaxed), + body_of(a), + "the site names the shape's body" + ); + assert_eq!(ways[0], word, "the way carries the same flagged entry"); + let same = closure(false); js_put_value_set_packed_miss(first, method, same, 0, &mut slot, &site.set); assert_eq!( @@ -110,7 +133,6 @@ fn packed_set_refuses_special_but_keeps_other_slots_of_the_completed_shape() { "generic same-body store preserves fact" ); assert_stored(first, method, same); - assert_eq!(site.set.load(Ordering::Relaxed), PACKED_SET_EMPTY); let scalar = key(b"cached_cf_scalar"); crate::object::js_object_set_field_by_name(object(first), scalar, 2.5); @@ -127,6 +149,15 @@ fn packed_set_refuses_special_but_keeps_other_slots_of_the_completed_shape() { assert_eq!(site.set.load(Ordering::Relaxed) as u32, mixed); assert_eq!(site.set.load(Ordering::Relaxed) & PACKED_SET_F64_SLOT, 0); + // A ConstFn slot of another body is never flagged at this site: the + // site's body word is claimed once and never changes. + let other_method = key(b"cached_cf_packed_other"); + let third = completed(other_method, closure(true)); + let site_word_before = site.set.load(Ordering::Relaxed); + unsafe { prime_packed_set(third, other_method, &mut slot, &site.set) }; + assert_eq!(site.set.load(Ordering::Relaxed), site_word_before); + assert_eq!(site.constfn_info.load(Ordering::Relaxed), body_of(a)); + let replacement = closure(true); js_put_value_set_packed_miss(second, method, replacement, 0, &mut slot, &site.set); assert_ne!( @@ -144,7 +175,7 @@ fn packed_set_refuses_special_but_keeps_other_slots_of_the_completed_shape() { } #[test] -fn packed_add_refuses_special_successor_before_any_memo_publication() { +fn packed_add_serves_only_the_site_body_of_a_special_successor() { let _lock = crate::gc::global_side_table_test_lock(); let _no_gc = crate::gc::GcSuppressScope::new(); let method = key(b"cached_cf_append_method"); @@ -163,23 +194,50 @@ fn packed_add_refuses_special_successor_before_any_memo_publication() { .special_constfn_mask, 1 << 1 ); - assert_eq!(site.add_shapes.load(Ordering::Relaxed), PACKED_SET_EMPTY); - assert_eq!(site.add_guard.load(Ordering::Relaxed), 0); - assert_eq!(site.add_ways.load(Ordering::Relaxed), 0); + let shapes = site.add_shapes.load(Ordering::Relaxed); + assert_eq!((shapes as u32, (shapes >> 32) as u32), (pre, final_id)); + let guard = site.add_guard.load(Ordering::Relaxed); + assert_ne!( + guard & super::super::packed_add::ADD_CONSTFN_SLOT, + 0, + "flagged" + ); + assert_eq!(guard & super::super::packed_add::ADD_F64_SLOT, 0); + assert_eq!(site.constfn_info.load(Ordering::Relaxed), body_of(a)); + + // Another body is refused before anything is stamped. + let foreign = closure(true); assert_eq!( - unsafe { super::super::packed_add::packed_add_try(site, second, a) }, + unsafe { super::super::packed_add::packed_add_try(site, second, foreign) }, None ); assert_eq!(stamp(second), pre, "declining add changes nothing"); + // So is a non-closure value. + assert_eq!( + unsafe { super::super::packed_add::packed_add_try(site, second, 1.5) }, + None + ); + assert_eq!(stamp(second), pre); + // A closure of the site's body (other captures, other address) is served + // onto exactly the successor, holding THIS closure. let b = closure(false); - js_put_value_set_packed_miss(second, method, b, 0, &mut slot, &site.set); + assert_ne!(a.to_bits(), b.to_bits()); assert_eq!( - stamp(second), - final_id, - "same-body generic append shares final id" + unsafe { super::super::packed_add::packed_add_try(site, second, b) }.map(f64::to_bits), + Some(b.to_bits()) ); + assert_eq!(stamp(second), final_id, "same-body memo shares final id"); assert_stored(second, method, b); + + // A foreign body through the miss appends on its own shape and leaves the + // site's memo and body as they were. + let fourth = birth(); + js_put_value_set_packed_miss(fourth, method, foreign, 0, &mut slot, &site.set); + assert_ne!(stamp(fourth), final_id); + assert_stored(fourth, method, foreign); + assert_eq!(site.add_shapes.load(Ordering::Relaxed), shapes); + assert_eq!(site.constfn_info.load(Ordering::Relaxed), body_of(a)); let replacement = closure(true); js_put_value_set_packed_miss(second, method, replacement, 0, &mut slot, &site.set); assert_ne!(stamp(second), final_id); @@ -190,6 +248,15 @@ fn packed_add_refuses_special_successor_before_any_memo_publication() { 0 ); assert_stored(second, method, replacement); + // The overwrite deprecated the ConstFn lane, which moved the validity + // word: the memo no longer serves its successor. + let fifth = birth(); + assert_eq!(stamp(fifth), pre); + assert_eq!( + unsafe { super::super::packed_add::packed_add_try(site, fifth, closure(false)) }, + None + ); + assert_eq!(stamp(fifth), pre); } #[test] diff --git a/crates/perry-runtime/src/proxy/put_value/packed_add.rs b/crates/perry-runtime/src/proxy/put_value/packed_add.rs index 7629ce8538..f8d7c4f691 100644 --- a/crates/perry-runtime/src/proxy/put_value/packed_add.rs +++ b/crates/perry-runtime/src/proxy/put_value/packed_add.rs @@ -76,8 +76,9 @@ use std::sync::atomic::{AtomicU64, Ordering}; use super::*; /// One static-key store site: the emitted `@perry_ic_N_packed_set` -/// (`[4 x i64]`). **The layout must equal `perry-codegen`'s -/// `PACKED_SET_SITE_WORDS` / `ADD_SHAPES_WORD` / `ADD_GUARD_WORD`**; pinned by +/// (`[5 x i64]`). **The layout must equal `perry-codegen`'s +/// `PACKED_SET_SITE_WORDS` / `ADD_SHAPES_WORD` / `ADD_GUARD_WORD`** and +/// `perry_abi::PACKED_SET_CONSTFN_INFO_WORD`; pinned by /// `packed_set_site_layout_matches_codegen`. #[repr(C)] pub struct PackedSetSite { @@ -95,6 +96,12 @@ pub struct PackedSetSite { /// ways at the pre-shape's home ([`add_way_home`]) and the one after it, /// after the primary words. pub add_ways: AtomicU64, + /// The site's ConstFn body (`perry_abi::PACKED_SET_CONSTFN_INFO_WORD`): + /// a `JsFunctionInfo` address, 0 = none. Claimed once by the first + /// ConstFn-flagged publication ([`claim_constfn_body`]) and never + /// changed, so every flagged entry of the site names this one body and + /// the emitted hit compares the stored closure's info word with it. + pub constfn_info: AtomicU64, } /// One further memo, in the primary words' format (`add_shapes`, @@ -151,6 +158,7 @@ impl PackedSetSite { add_shapes: AtomicU64::new(PACKED_SET_EMPTY), add_guard: AtomicU64::new(0), add_ways: AtomicU64::new(0), + constfn_info: AtomicU64::new(0), } } } @@ -161,7 +169,7 @@ pub const ADD_SHAPES_WORD: usize = 1; #[cfg_attr(not(test), allow(dead_code))] pub const ADD_GUARD_WORD: usize = 2; #[cfg_attr(not(test), allow(dead_code))] -pub const PACKED_SET_SITE_WORDS: usize = 4; +pub const PACKED_SET_SITE_WORDS: usize = crate::codegen_abi::PACKED_SET_SITE_WORDS; #[cfg_attr(not(test), allow(dead_code))] pub const ADD_WAYS_WORD: usize = 3; /// Words of one [`AddWay`]. @@ -175,7 +183,57 @@ pub const ADD_SLOT_BITS: u32 = 16; /// doubles the funnel canonicalizes) before it stamps anything. **Must equal /// perry-codegen `expr/put_value_store_ic.rs::ADD_F64_SLOT`.** pub const ADD_F64_SLOT: u64 = 1 << (ADD_SLOT_BITS - 1); -const ADD_SLOT_MASK: u64 = ADD_F64_SLOT - 1; +/// The guard's bit for a memo whose successor's lane at the slot is ConstFn: +/// the emitted hit admits only a closure whose info word is the site's +/// [`PackedSetSite::constfn_info`] (and that is not a rebindable `this` +/// clone), before it stamps anything. +pub const ADD_CONSTFN_SLOT: u64 = crate::codegen_abi::PACKED_ADD_CONSTFN_SLOT; +const ADD_SLOT_MASK: u64 = ADD_CONSTFN_SLOT - 1; +const _: () = assert!(ADD_CONSTFN_SLOT == 1 << (ADD_SLOT_BITS - 2)); + +/// The body a ConstFn-flagged entry of `site` may name: `info` when the site +/// has no body yet (claimed now) or already names it, else `false` and the +/// caller publishes nothing (a second body at one site keeps the miss). +/// Claimed by the primary agent before any flagged entry is published, and +/// never changed afterwards, so no reader pairs a flagged entry with another +/// body. +/// +/// # Safety +/// `site` is a live site. +pub(crate) unsafe fn claim_constfn_body(site: *const PackedSetSite, info: u64) -> bool { + if info == 0 || crate::agent::current_agent() != crate::agent::PRIMARY_AGENT { + return false; + } + let word = &(*site).constfn_info; + match word.load(Ordering::Relaxed) { + 0 => { + word.store(info, Ordering::Relaxed); + true + } + current => current == info, + } +} + +/// The ConstFn body of `post`'s inline lane `slot`, when that lane is ConstFn +/// and its lineage has nothing deprecated: the one admission fact of a +/// ConstFn-flagged memo. +fn post_constfn_body(post: u32, slot: u32) -> Option { + if slot >= crate::object::field_rep::REP_SLOTS { + return None; + } + let d = crate::object::shapes::shape_descriptor_by_id(post)?; + if d.special_constfn_mask & (1u32 << slot) == 0 + || crate::object::field_rep::slot_rep(d.rep, slot) != crate::object::field_rep::REP_SPECIAL + || crate::object::field_rep::has_deprecated(d.rep) + || d.deprecation_targets() != (0, 0) + { + return None; + } + d.constfn_infos() + .iter() + .find(|entry| u32::from(entry.slot) == slot) + .map(|entry| entry.info) +} const SPILL_FLIP: u32 = crate::object::field_get_set::PACKED_SPILL_FLIP; @@ -293,6 +351,9 @@ pub(crate) const C_REP_VALIDITY_BUMP: usize = 25; pub(crate) const C_REP_CONVERGE: usize = 26; /// A receiver on a shape with a deprecated lane moved to the normalized shape. pub(crate) const C_REP_MIGRATE: usize = 27; +/// A ConstFn key-add or overwrite was not published: the site already names +/// another body. +pub(crate) const C_PRIME_CONSTFN_OTHER_BODY: usize = 28; #[cfg_attr(test, allow(dead_code))] const CENSUS_NAMES: [&str; 48] = [ @@ -324,7 +385,7 @@ const CENSUS_NAMES: [&str; 48] = [ "rt.rep.validity_bump", "rt.rep.converge", "rt.rep.migrate", - "rt.28", + "rt.prime.constfn_other_body", "rt.29", "rt.30", "rt.31", @@ -465,8 +526,18 @@ pub(crate) unsafe fn packed_add_try( let post = (shapes >> 32) as u32; let post_d = crate::object::shapes::shape_descriptor_by_id(post)?; let slot = (guard & ADD_SLOT_MASK) as usize; - // Charter step 5 (T2): the post-shape is the class guard of the memo. - if !crate::object::field_rep_store::cached_key_add_admits( + // Charter step 5 (T2): the post-shape is the class guard of the memo. A + // ConstFn memo admits exactly a closure of the body its successor names + // (the site's body), whose lane the checked slot store below keeps. + if guard & ADD_CONSTFN_SLOT != 0 { + let body = post_constfn_body(post, slot as u32)?; + if spill + || (*site).constfn_info.load(Ordering::Relaxed) != body + || crate::object::field_rep_store::constfn_store_info(value.to_bits()) != Some(body) + { + return None; + } + } else if !crate::object::field_rep_store::cached_key_add_admits( post, slot as u32, Some(value.to_bits()), @@ -653,12 +724,42 @@ pub(crate) unsafe fn packed_add_prime( census(C_PRIME_UNVERIFIED); return; } - // The emitted add hit stamps the successor before its raw slot store. - // A SPECIAL append needs the checked slow path to prewrite the current - // closure under Any before publishing its body-specific shape. - if crate::object::field_rep::slot_rep(post_d.rep, n) == crate::object::field_rep::REP_SPECIAL { - census(C_PRIME_UNVERIFIED); - return; + // A ConstFn append: the successor names the body of the closure this + // store wrote. The memo is published only for the site's one body, and + // its hit (emitted or runtime) admits only a closure of that body; the + // stamp and the slot store are not separated by any collection point, so + // no collector sees the ConstFn lane without the closure (the slow path's + // prewrite-under-Any order exists because its mint collects). + let constfn_body = if crate::object::field_rep::slot_rep(post_d.rep, n) + == crate::object::field_rep::REP_SPECIAL + { + let fields = + (obj as *const u8).add(std::mem::size_of::()) as *const u64; + match post_constfn_body(post, n) { + Some(body) + if inline + && crate::object::field_rep_store::constfn_store_info( + *fields.add(n as usize), + ) == Some(body) => + { + Some(body) + } + _ => { + census(C_PRIME_UNVERIFIED); + return; + } + } + } else { + None + }; + if let Some(body) = constfn_body { + // Another body at this site keeps the miss; nothing is claimed for + // a site that cannot publish (see the lane and agent tests above). + let site_body = (*site).constfn_info.load(Ordering::Relaxed); + if site_body != 0 && site_body != body { + census(C_PRIME_CONSTFN_OTHER_BODY); + return; + } } // A marked prototype or exotic read receiver is on a private shape lineage // (`proto_validity::ensure_meta_for_mark`); never learn one of its shapes, @@ -709,6 +810,16 @@ pub(crate) unsafe fn packed_add_prime( census(C_PRIME_UNVERIFIED); return; } + // The body is claimed before the flagged memo is published (the claim + // re-checks it after the collecting verdict above). + let constfn_slot = match constfn_body { + Some(body) if claim_constfn_body(site, body) => ADD_CONSTFN_SLOT, + Some(_) => { + census(C_PRIME_CONSTFN_OTHER_BODY); + return; + } + None => 0, + }; // Own both ShapeIds before they can be stamped from the site. crate::object::shape_carriers::note_shape_id(pre); crate::object::shape_carriers::note_shape_id(post); @@ -726,7 +837,7 @@ pub(crate) unsafe fn packed_add_prime( } else { 0 }; - let guard = (generation << ADD_SLOT_BITS) | f64_slot | u64::from(n); + let guard = (generation << ADD_SLOT_BITS) | f64_slot | constfn_slot | u64::from(n); let same_pre = |word: u64| word != PACKED_SET_EMPTY && unflip(word as u32) == pre; // A way that holds this pre-shape (a stale guard) is superseded. if let Some(ways) = site_ways(site_ptr) { diff --git a/crates/perry-runtime/src/proxy/put_value/packed_add_tests.rs b/crates/perry-runtime/src/proxy/put_value/packed_add_tests.rs index e4124a3eee..ad584a0747 100644 --- a/crates/perry-runtime/src/proxy/put_value/packed_add_tests.rs +++ b/crates/perry-runtime/src/proxy/put_value/packed_add_tests.rs @@ -10,8 +10,42 @@ fn packed_set_site_layout_matches_codegen() { std::mem::size_of::(), 8 * PACKED_SET_SITE_WORDS ); - assert_eq!(PACKED_SET_SITE_WORDS, 4); + assert_eq!(PACKED_SET_SITE_WORDS, 5); assert_eq!(std::mem::offset_of!(PackedSetSite, add_ways), 24); + // perry_abi::PACKED_SET_CONSTFN_INFO_WORD: the site's ConstFn body, + // compared by the emitted ConstFn store check. + assert_eq!( + std::mem::offset_of!(PackedSetSite, constfn_info), + 8 * crate::codegen_abi::PACKED_SET_CONSTFN_INFO_WORD + ); + assert_eq!(crate::codegen_abi::PACKED_SET_CONSTFN_INFO_WORD, 4); + // The guard's flag bits sit above the slot index: F64 = bit 15, + // ConstFn = bit 14 (perry-codegen ADD_F64_SLOT / ADD_CONSTFN_SLOT). + assert_eq!((ADD_F64_SLOT, ADD_CONSTFN_SLOT), (1 << 15, 1 << 14)); + assert_eq!(ADD_SLOT_MASK, (1 << 14) - 1); + // The existing-key word: F64 = bit 63, ConstFn = bit 62, the index below. + assert_eq!( + ( + super::super::packed_set::PACKED_SET_F64_SLOT, + super::super::packed_set::PACKED_SET_CONSTFN_SLOT + ), + (1 << 63, 1 << 62) + ); + // The closure facts the emitted ConstFn check reads. + assert_eq!( + ( + crate::codegen_abi::CLOSURE_CAPTURES_THIS_FLAG, + crate::codegen_abi::CLOSURE_NO_THIS_REBIND_FLAG + ), + ( + crate::closure::CAPTURES_THIS_FLAG, + crate::closure::NO_THIS_REBIND_FLAG + ) + ); + assert_eq!( + std::mem::offset_of!(crate::closure::ClosureHeader, info), + crate::codegen_abi::CLOSURE_INFO_OFFSET + ); assert_eq!(std::mem::offset_of!(PackedSetSite, set), 0); assert_eq!( std::mem::offset_of!(PackedSetSite, add_shapes), diff --git a/crates/perry-runtime/src/proxy/put_value/packed_set.rs b/crates/perry-runtime/src/proxy/put_value/packed_set.rs index 543d136bf5..ddf33abe92 100644 --- a/crates/perry-runtime/src/proxy/put_value/packed_set.rs +++ b/crates/perry-runtime/src/proxy/put_value/packed_set.rs @@ -84,6 +84,16 @@ pub const PACKED_SET_EMPTY: u64 = 0xFFFF_FFFF; /// `expr/put_value_store_ic.rs` (the word's sign bit).** pub const PACKED_SET_F64_SLOT: u64 = 1 << 63; +/// The word's (and a way's) bit for an inline slot whose lane is ConstFn in +/// the word's ShapeId (`perry_abi::PACKED_SET_CONSTFN_SLOT`). The emitted hit +/// then stores only a closure whose info word is the site's ConstFn body +/// (`PackedSetSite::constfn_info`), which keeps the shape's body claim true; +/// any other value takes the miss, whose checked funnel deprecates the lane. +/// Published only with a site record (`packed` non-null) whose body it is. +pub const PACKED_SET_CONSTFN_SLOT: u64 = crate::codegen_abi::PACKED_SET_CONSTFN_SLOT; +/// The flag bits of a word's slot half. +const PACKED_SET_FLAGS: u64 = PACKED_SET_F64_SLOT | PACKED_SET_CONSTFN_SLOT; + /// Ways in a site's cache. The first [`PACKED_SET_INLINE_WAYS`] are compared by /// the emitted code (**must equal `PACKED_SET_INLINE_WAYS` in /// `perry-codegen/src/expr/put_value_store_ic.rs`**); the rest by this entry. @@ -328,7 +338,7 @@ unsafe fn packed_ways_store_impl( for (way, word) in ways.iter().enumerate() { let word = word.load(Ordering::Relaxed); let stamp = word as u32; - let index = ((word & !PACKED_SET_F64_SLOT) >> 32) as u32; + let index = ((word & !PACKED_SET_FLAGS) >> 32) as u32; if stamp == sid && way >= first_way { // Charter step 3: the matched id is an `Ordinary` shape (the only // kind `prime_packed_set` publishes), which proves the receiver @@ -443,13 +453,37 @@ unsafe fn prime_packed_set( let Some(idx) = own_idx else { return; }; - // The emitted hit stores raw bits and cannot invalidate a ConstFn body - // fact. Keep only this SPECIAL slot on the checked miss path; other - // Any/F64 slots in the same completed shape remain cacheable. - if crate::object::field_rep::slot_rep(shape.rep, idx) == crate::object::field_rep::REP_SPECIAL { - return; - } let inline = idx < shape.live_inline_slot_count; + // The emitted hit stores raw bits, so a ConstFn slot is published only + // flagged: the hit then admits only a closure of the site's one body, + // which keeps the shape's body claim (any other value takes the miss and + // its checked funnel). Without a site record, or with another body + // already claimed by the site, the slot keeps the checked miss path. + let constfn_slot = if crate::object::field_rep::slot_rep(shape.rep, idx) + == crate::object::field_rep::REP_SPECIAL + { + let body = shape + .constfn_infos() + .iter() + .find(|entry| u32::from(entry.slot) == idx) + .map(|entry| entry.info); + let site = packed as *const super::packed_add::PackedSetSite; + match body { + Some(body) + if inline + && !site.is_null() + && shape.special_constfn_mask & (1u32 << idx) != 0 + && !crate::object::field_rep::has_deprecated(shape.rep) + && shape.deprecation_targets() == (0, 0) + && super::packed_add::claim_constfn_body(site, body) => + { + PACKED_SET_CONSTFN_SLOT + } + _ => return, + } + } else { + 0 + }; if !inline && !(idx < key_count && idx < IC_SLOT_OVERFLOW_BIT) { return; } @@ -474,7 +508,7 @@ unsafe fn prime_packed_set( } else { 0 }; - let entry = (u64::from(index) << 32) | u64::from(key32) | f64_slot; + let entry = (u64::from(index) << 32) | u64::from(key32) | f64_slot | constfn_slot; // The way cache: fill the first empty way, never evict. if !cache_slot.is_null() { diff --git a/test-files/_helpers/constfn_store_worker.ts b/test-files/_helpers/constfn_store_worker.ts new file mode 100644 index 0000000000..a33dc046a7 --- /dev/null +++ b/test-files/_helpers/constfn_store_worker.ts @@ -0,0 +1,10 @@ +import { parentPort } from 'node:worker_threads'; +function F(this: any) { return 3 + this.v; } +function Fb(this: any) { return 4 + this.v; } +function C(this: any, f: any, v: number) { this.k = C; this.m = f; this.v = v; } +function mk(i: number) { return function () { return i; }; } +let s = 0; +for (let i = 0; i < 3000; i++) { const x: any = new (C as any)(i % 1000 === 999 ? Fb : F, i % 5); s += x.m() + (x.k === C ? 1 : 0); } +const o: any = { a: 1 }; o.cb = mk(0); +for (let i = 0; i < 3000; i++) { o.cb = mk(i % 4); s += o.cb(); } +parentPort!.postMessage(String(s)); diff --git a/test-files/test_gap_constfn_function_stores.ts b/test-files/test_gap_constfn_function_stores.ts new file mode 100644 index 0000000000..74f960023f --- /dev/null +++ b/test-files/test_gap_constfn_function_stores.ts @@ -0,0 +1,88 @@ +// Function-valued stores (`this.k = F`, `this.cb = fn`, `o.m = function(){}`): +// a ConstFn lane names the body, the slot holds each object's own closure. +// The store site caches "a closure of body B moves shape S to S'" and checks +// the stored closure's body on every hit; anything else must take the slow +// path (deprecating the lane) and every later call must see the right body. +import { Worker } from 'node:worker_threads'; + +// 1. The same body stored repeatedly by a plain function constructor. +function P(this: any, v: number) { this.k = P; this.v = v; } +let same = 0; +const ps: any[] = []; +for (let i = 0; i < 3000; i++) { const x: any = new (P as any)(i); if (x.k === P) same++; ps.push(x); } +console.log('same body', same, ps[2999].v, ps[7].k === P, Object.keys(ps[5]).join(',')); + +// 2. Different closures of one body: each object keeps its own environment. +function mk(i: number) { return function (this: any) { return i * 10 + this.n; }; } +const fs: any[] = []; for (let i = 0; i < 7; i++) fs.push(mk(i)); +function H(this: any, f: any, n: number) { this.cb = f; this.n = n; } +let sum = 0, own = 0; +const hs: any[] = []; +for (let i = 0; i < 3000; i++) { + const x: any = new (H as any)(fs[i % 7], i % 3); + if (x.cb === fs[i % 7]) own++; + sum += x.cb(); + hs.push(x); +} +console.log('closures', own, sum, hs[10].cb(), hs[11].cb()); + +// 3. Another body at a warm site, then an overwrite with another body. +function A(this: any) { return 'A' + this.t; } +function B(this: any) { return 'B' + this.t; } +function Q(this: any, f: any, t: number) { this.m = f; this.t = t; } +const qs: any[] = []; +let calls = ''; +for (let i = 0; i < 2000; i++) { const x: any = new (Q as any)(A, i); qs.push(x); if (i % 500 === 0) calls += x.m(); } +for (let i = 0; i < 6; i++) { const x: any = new (Q as any)(i % 2 ? B : A, i); qs.push(x); calls += x.m(); } +console.log('other body', calls); +const old: any = qs[3]; +old.m = B; // deprecate the lane on an existing object +console.log('overwrite', old.m(), qs[4].m(), qs[2004].m(), qs[2005].m()); +for (let i = 0; i < 4; i++) { const x: any = new (Q as any)(A, 100 + i); calls = x.m() + (x.m === A); } +console.log('after deprecation', calls, new (Q as any)(B, 9).m()); + +// 4. A method called through the store; a fresh closure of one body per object. +function getV(this: any) { return this.v * 2; } +function R(this: any, v: number) { this.get = getV; this.v = v; } +let rs = 0; for (let i = 0; i < 2000; i++) rs += new (R as any)(i).get(); +const lit: any[] = []; +for (let i = 0; i < 2000; i++) { const o: any = { n: i }; o.m = function (this: any) { return this.n + 1; }; lit.push(o); } +let ls = 0; for (const o of lit) ls += o.m(); +console.log('methods', rs, ls, lit[0].m === lit[1].m); + +// 5. Overwrites of an existing function-valued property. +function G1() { return 1; } +function G2() { return 2; } +const o: any = { a: 1 }; o.cb = G1; +let os = 0; for (let i = 0; i < 3000; i++) { o.cb = G1; os += o.cb(); } +const p: any = { b: 1 }; p.cb = fs[0]; +for (let i = 0; i < 3000; i++) { p.cb = fs[i % 7]; os += p.cb.call({ n: 1 }); } +o.cb = G2; os += o.cb(); +for (let i = 0; i < 10; i++) { o.cb = i % 2 ? G1 : G2; os += o.cb(); } +o.cb = 7; console.log('overwrites', os, o.cb, typeof p.cb); + +// 6. Values that are not a plain closure of the body: a number, a bound +// function, an arrow capturing `this`, null, a string, an object. +function K(this: any, f: any) { this.k = f; } +for (let i = 0; i < 1000; i++) new (K as any)(G1); +const vals: any[] = [5, G1.bind(null), null, 'G1', { call: 1 }, undefined, G2, G1]; +console.log('values', vals.map((v) => { const x: any = new (K as any)(v); return typeof x.k + ':' + (typeof x.k === 'function' ? x.k() : String(x.k)); }).join(' ')); +function W(this: any) { this.cb = () => this.n; this.n = 4; } +let ws = 0; for (let i = 0; i < 500; i++) ws += new (W as any)().cb(); +console.log('arrow', ws); + +// 7. A setter defined on the prototype after the site is warm intercepts the add. +function S(this: any) { this.k = G1; this.d = 1; } +for (let i = 0; i < 1000; i++) new (S as any)(); +let seen = 0; +Object.defineProperty(S.prototype, 'k', { set(v: any) { seen += v(); }, get() { return G2; }, configurable: true }); +const s: any = new (S as any)(); +console.log('setter', seen, s.k(), Object.prototype.hasOwnProperty.call(s, 'k')); + +// 8. A worker running the same stores. +const worker = new Worker(new URL('./_helpers/constfn_store_worker.ts', import.meta.url)); +worker.on('message', (m: string) => { + console.log('worker', m); + worker.terminate().then(() => process.exit(0)); +}); +setTimeout(() => process.exit(2), 10000); From 88eed190df1619185cd34519cb6d53632c6da603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 3 Oct 2026 13:36:19 +0200 Subject: [PATCH 4/4] changelog: key the function-store fragment to PR 11798 --- ...onstfn-function-stores.md => 11798-constfn-function-stores.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{PENDING-constfn-function-stores.md => 11798-constfn-function-stores.md} (100%) diff --git a/changelog.d/PENDING-constfn-function-stores.md b/changelog.d/11798-constfn-function-stores.md similarity index 100% rename from changelog.d/PENDING-constfn-function-stores.md rename to changelog.d/11798-constfn-function-stores.md