From a166e8e470f673b5ffe05f88df86b23dae43eaf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 4 Oct 2026 10:05:05 +0000 Subject: [PATCH 1/2] perf(runtime): by-name class method calls are answered by prototype shapes, with a per-site chain memo A class instance's method call that compiled code does not answer inline reaches the runtime by name: an untyped receiver's method-site miss, a computed-key call `obj[k](...)`, or the miss edge of a compiled class-method arm (on every call once a prototype guard byte of that name is set, as in by (class id, name), `VTABLE_IC` and `OBJ_DISPATCH_IC`, filled from the class method table. The call is now `[[Get]](recv, name)` read off shapes (`native_call_method/class_holder.rs`): the receiver's shape lacks the name, each prototype's key list is searched, and the first holder whose shape lists it has it at the slot the shape names; a ConstFn lane names the body, so the slot is not re-validated. Accessors, dictionary or exotic holders take the ordinary [[Get]] from the holder that has the name. Private names and symbol-member aliases (not string-keyed prototype properties) keep their own path. `VTABLE_IC`, `OBJ_DISPATCH_IC`, `note_class_vtable_resolution` and `instance_class_prototype_object` are deleted; handle_methods, collection_methods and the class-ref (INT32) arm use the same walk. A walk costs a key-list search per prototype, so a computed-key call site and a compiled arm's miss edge each keep a chain memo (`method_site/chain_memo.rs`) of the walk that answered them: the receiver's `(class_id | ShapeId)` word, each prototype's word from the receiver's [[Prototype]] to the holder (strong roots, rewritten when they move), the holder's inline slot and its ConstFn body, and for a computed key its bytes. Every word is compared on every use, so a key added, deleted or redefined anywhere on the chain, a relink or a value overwrite is seen on the next call; there is no global invalidation word and nothing is keyed on a class id or a name. A hop is recorded only when its shape pins its [[Prototype]] (a serial or MIXED identity, or the realm default). Two ways per site; a site that replaces ways 16 times stops recording. A computed-key site owns a pointer global (`js_native_call_method_{str_key,value}_memo`); a class-method arm's learned word is followed by its memo slot, and the miss edge tags the site address when its guard bytes forbid learning. No memo is read or recorded once a worker exists. A method site's miss keeps no memo: its misses see every kind of receiver, and its deep class chains measured flat. Instructions (qb6, base = main 9e901a02f): obj[key]() holder depth 1/3/6 1275 -> 753/771/800 per call #10507 decimal_class 1447 -> 785 per call value_call / proto_call / bind -31 per call own/inh/poly/mega/getter/instanceof: flat; fresh +29 (+0.09%) tsc -0.11%, commander -0.17%, qs -0.01%, Zod +0.05%, hello/startup flat matrix method/inherited 110/110 identical Refs #10502 --- .../PENDING-class-method-lookup-shapes.md | 11 + .../src/gc_effects/linux-x86_64.tsv | 2 + .../src/gc_effects/macos-aarch64.tsv | 2 + .../src/gc_effects/windows-x86_64.tsv | 2 + .../src/lower_call/direct_method_guard.rs | 38 + .../src/lower_call/early_branches.rs | 8 +- .../src/lower_call/method_override.rs | 15 +- .../src/runtime_decls/strings_part2.rs | 25 +- .../perry-codegen/src/wasm32/runtime_abi.tsv | 2 + crates/perry-runtime/src/closure/dispatch.rs | 4 +- .../src/closure/dispatch/value_call.rs | 18 + crates/perry-runtime/src/closure/mod.rs | 4 +- crates/perry-runtime/src/gc/mod.rs | 4 + .../src/object/class_registry.rs | 17 +- .../src/object/class_registry/dispatch.rs | 508 +-- .../class_registry/prototype_methods.rs | 9 +- .../class_registry/prototype_objects.rs | 11 - .../parent_class_object_tests.rs | 15 +- .../perry-runtime/src/object/method_site.rs | 58 +- .../src/object/method_site/chain_memo.rs | 396 ++ .../src/object/method_site/read_holder.rs | 2 +- .../src/object/native_call_method.rs | 323 +- .../object/native_call_method/class_holder.rs | 733 ++++ .../native_call_method/collection_methods.rs | 79 +- .../object/native_call_method/direct_site.rs | 68 +- .../native_call_method/handle_methods.rs | 254 +- .../object/native_call_method/memo_entries.rs | 71 + .../vtable_guard_scan_tests.rs | 4 +- docs/src/api/reference.md | 3767 ----------------- scripts/gc_runtime_root_holders.json | 12 +- scripts/thread_exit_address_globals.json | 8 + .../test_gap_10502_class_method_chain_memo.ts | 88 + 32 files changed, 1700 insertions(+), 4858 deletions(-) create mode 100644 changelog.d/PENDING-class-method-lookup-shapes.md create mode 100644 crates/perry-runtime/src/object/method_site/chain_memo.rs create mode 100644 crates/perry-runtime/src/object/native_call_method/class_holder.rs create mode 100644 crates/perry-runtime/src/object/native_call_method/memo_entries.rs create mode 100644 test-files/test_gap_10502_class_method_chain_memo.ts diff --git a/changelog.d/PENDING-class-method-lookup-shapes.md b/changelog.d/PENDING-class-method-lookup-shapes.md new file mode 100644 index 0000000000..1d327c1dd3 --- /dev/null +++ b/changelog.d/PENDING-class-method-lookup-shapes.md @@ -0,0 +1,11 @@ +A by-name method call on a class instance (an untyped receiver, a computed +key `obj[k]()`, or a compiled class-method arm's miss edge) now finds the +method on the instance's prototype chain by shapes: each prototype's key list +names the holder and the slot, and the slot's ConstFn lane names the body. The +runtime's per-(class id, name) dispatch caches `VTABLE_IC` and +`OBJ_DISPATCH_IC` are deleted. A computed-key call site and a compiled arm's +miss edge keep a chain memo of the walk that answered them (the receiver's +word, every prototype's word, the holder's slot), compared word by word on +every use: `obj[key]()` drops from about 1,275 to 753-800 instructions per +call at holder depth 1-6, and the #10507 `decimal_class` row from 1,447 to +785. diff --git a/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv b/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv index f0e558f7bf..008ec0c960 100644 --- a/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv +++ b/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv @@ -1766,8 +1766,10 @@ js_native_call_method_nullsafe Reenters js_native_call_method_patched_proto Reenters js_native_call_method_patched_proto_apply Reenters js_native_call_method_str_key Reenters +js_native_call_method_str_key_memo Reenters js_native_call_method_value Reenters js_native_call_method_value_apply Reenters +js_native_call_method_value_memo Reenters js_native_call_value Reenters js_native_handle_dispose Reenters js_native_handle_mark_main_thread Leaf diff --git a/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv b/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv index bb66a54bfe..e2b05e7243 100644 --- a/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv +++ b/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv @@ -1764,8 +1764,10 @@ js_native_call_method_nullsafe Reenters js_native_call_method_patched_proto Reenters js_native_call_method_patched_proto_apply Reenters js_native_call_method_str_key Reenters +js_native_call_method_str_key_memo Reenters js_native_call_method_value Reenters js_native_call_method_value_apply Reenters +js_native_call_method_value_memo Reenters js_native_call_value Reenters js_native_handle_dispose Reenters js_native_handle_mark_main_thread Leaf diff --git a/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv b/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv index 41e51f8e22..d744293df3 100644 --- a/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv +++ b/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv @@ -1764,8 +1764,10 @@ js_native_call_method_nullsafe Reenters js_native_call_method_patched_proto Reenters js_native_call_method_patched_proto_apply Reenters js_native_call_method_str_key Reenters +js_native_call_method_str_key_memo Reenters js_native_call_method_value Reenters js_native_call_method_value_apply Reenters +js_native_call_method_value_memo Reenters js_native_call_value Reenters js_native_handle_dispose Reenters js_native_handle_mark_main_thread Leaf diff --git a/crates/perry-codegen/src/lower_call/direct_method_guard.rs b/crates/perry-codegen/src/lower_call/direct_method_guard.rs index fea1dbefc5..1bccf67158 100644 --- a/crates/perry-codegen/src/lower_call/direct_method_guard.rs +++ b/crates/perry-codegen/src/lower_call/direct_method_guard.rs @@ -159,6 +159,44 @@ pub(crate) fn emit_direct_method_site_word(ctx: &mut FnCtx<'_>) -> String { format!("@{slot_name}") } +/// [`emit_direct_method_site_word`] followed by the site's chain memo slot +/// (null, see [`emit_chain_memo_slot`]): `{ i64 word, ptr memo }`. The +/// emitted code reads only the word; the miss edge +/// (`js_native_call_method_by_id_learn`) finds the memo slot next to it. +pub(crate) fn emit_direct_method_site_word_with_memo(ctx: &mut FnCtx<'_>) -> String { + let site_id = ctx.ic_site_counter; + ctx.ic_site_counter += 1; + let prefix = ctx.strings.module_prefix(); + let slot_name = if prefix.is_empty() { + format!("perry_mdirect_site_{site_id}") + } else { + format!("perry_mdirect_site_{prefix}__{site_id}") + }; + ctx.typed_parse_rodata.push(format!( + "@{slot_name} = private global {{ i64, ptr }} {{ i64 -1, ptr null }}, align 8" + )); + format!("@{slot_name}") +} + +/// One pointer owned by a by-name method call site: the slot of its chain +/// memo (`perry-runtime` `object/method_site/chain_memo.rs`), which the +/// runtime allocates on the site's first class-method answer and which +/// records the prototype walk that answered it, validated by ShapeId on every +/// use. Starts null; emitted code never reads it. +pub(crate) fn emit_chain_memo_slot(ctx: &mut FnCtx<'_>) -> String { + let site_id = ctx.ic_site_counter; + ctx.ic_site_counter += 1; + let prefix = ctx.strings.module_prefix(); + let slot_name = if prefix.is_empty() { + format!("perry_cmemo_{site_id}") + } else { + format!("perry_cmemo_{prefix}__{site_id}") + }; + ctx.typed_parse_rodata + .push(format!("@{slot_name} = private global ptr null, align 8")); + format!("@{slot_name}") +} + /// `i1`: `recv_box` is a heap object of `GC_TYPE_OBJECT`, not forwarded, whose /// exact receiver word equals the site's learned word. Emits /// its own pointer gate, so it is safe for any value. diff --git a/crates/perry-codegen/src/lower_call/early_branches.rs b/crates/perry-codegen/src/lower_call/early_branches.rs index 5db37409b5..cdf1561405 100644 --- a/crates/perry-codegen/src/lower_call/early_branches.rs +++ b/crates/perry-codegen/src/lower_call/early_branches.rs @@ -299,14 +299,16 @@ pub fn try_lower_index_get_call( }; let recv_box = group.reread(ctx, recv_idx)?; let (args_ptr, args_len) = build_dispatch_args_buffer(ctx, group, &arg_idxs)?; + let memo_slot = super::direct_method_guard::emit_chain_memo_slot(ctx); return Ok(Some(ctx.block().call( DOUBLE, - "js_native_call_method_str_key", + "js_native_call_method_str_key_memo", &[ (DOUBLE, &recv_box), (I64, &name_handle), (crate::types::PTR, &args_ptr), (I64, &args_len), + (crate::types::PTR, &memo_slot), ], ))); } @@ -320,14 +322,16 @@ pub fn try_lower_index_get_call( let recv_box = group.reread(ctx, recv_idx)?; let key_box = group.reread(ctx, key_idx)?; let (args_ptr, args_len) = build_dispatch_args_buffer(ctx, group, &arg_idxs)?; + let memo_slot = super::direct_method_guard::emit_chain_memo_slot(ctx); Ok(Some(ctx.block().call( DOUBLE, - "js_native_call_method_value", + "js_native_call_method_value_memo", &[ (DOUBLE, &recv_box), (DOUBLE, &key_box), (crate::types::PTR, &args_ptr), (I64, &args_len), + (crate::types::PTR, &memo_slot), ], ))) }); diff --git a/crates/perry-codegen/src/lower_call/method_override.rs b/crates/perry-codegen/src/lower_call/method_override.rs index fc1d3895a0..e12ddc49d2 100644 --- a/crates/perry-codegen/src/lower_call/method_override.rs +++ b/crates/perry-codegen/src/lower_call/method_override.rs @@ -14,7 +14,8 @@ use crate::native_value::LoweredValue; use crate::types::{DOUBLE, I1, I32, I64, I8}; use super::direct_method_guard::{ - emit_direct_method_site_word, emit_inline_direct_method_shape_guard, emit_learned_word_hit, + emit_direct_method_site_word, emit_direct_method_site_word_with_memo, + emit_inline_direct_method_shape_guard, emit_learned_word_hit, }; pub(super) const POINTER_TAG_HI16: &str = "32765"; // 0x7FFD @@ -1042,7 +1043,7 @@ pub(super) fn emit_guarded_direct_method_call( || typed_string_direct_fn.is_some()); let learned_site: Option<(String, usize)> = (inline_single_arm || probe_before_runtime_guard || multi_arm).then(|| { - let word = emit_direct_method_site_word(ctx); + let word = emit_direct_method_site_word_with_memo(ctx); (word, ctx.new_block("method_direct.learned")) }); let learned_label = learned_site.as_ref().map(|(_, idx)| ctx.block_label(*idx)); @@ -1898,11 +1899,15 @@ pub(super) fn emit_guarded_direct_method_call( // The learned word is consulted only behind the prototype guard // bytes, so while they are set (a prototype member of this // method's name was assigned, deleted or redefined) nothing the - // runtime could learn would ever be read: the miss edge passes - // no site, and the runtime dispatches without proving anything. + // runtime could learn would ever be read: the miss edge then + // passes the site's address tagged with bit 0, and the runtime + // learns nothing. Either way the word is followed by the site's + // chain memo slot, from which a receiver the arms decline repeats + // its by-name answer instead of walking the prototype chain. let blk = ctx.block(); let prototype_ok = emit_prototype_method_guard_ok(blk, &method_guard_slot_str); - let site = blk.select(I1, &prototype_ok, crate::types::PTR, word, "null"); + let no_learn = blk.gep(I8, word, &[(I64, "1")]); + let site = blk.select(I1, &prototype_ok, crate::types::PTR, word, &no_learn); ctx.block().call( DOUBLE, "js_native_call_method_by_id_learn", diff --git a/crates/perry-codegen/src/runtime_decls/strings_part2.rs b/crates/perry-codegen/src/runtime_decls/strings_part2.rs index 9caf8b9a03..dac96d6041 100644 --- a/crates/perry-codegen/src/runtime_decls/strings_part2.rs +++ b/crates/perry-codegen/src/runtime_decls/strings_part2.rs @@ -918,15 +918,10 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) { DOUBLE, &[DOUBLE, I64, PTR, I64], ); - // Miss edge of a class-method site that keeps a learned receiver word - // (`lower_call/method_override.rs`): learns, then dispatches as above. - module.declare_function( - "js_native_call_method_by_id_learn", - DOUBLE, - &[DOUBLE, I64, PTR, I64, PTR, I32], - ); - // Miss edge of a class-method site that keeps a learned receiver word - // (): learns, then dispatches as above. + // Miss edge of a class-method site (`lower_call/method_override.rs`): + // learns the receiver word (unless the site address is tagged with bit + // 0), then dispatches as above with the chain memo slot that follows the + // word. module.declare_function( "js_native_call_method_by_id_learn", DOUBLE, @@ -953,6 +948,18 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) { DOUBLE, &[DOUBLE, I64, PTR, I64], ); + // The computed-key call site's forms that pass its chain memo slot + // (`lower_call/early_branches.rs`). + module.declare_function( + "js_native_call_method_str_key_memo", + DOUBLE, + &[DOUBLE, I64, PTR, I64, PTR], + ); + module.declare_function( + "js_native_call_method_value_memo", + DOUBLE, + &[DOUBLE, DOUBLE, PTR, I64, PTR], + ); // #321: dispatch obj[key](args) for a runtime-value key (not statically a // string). Binds `this = obj` for any key type — string keys go through // the full dispatch tower, symbol/other keys read the property then call diff --git a/crates/perry-codegen/src/wasm32/runtime_abi.tsv b/crates/perry-codegen/src/wasm32/runtime_abi.tsv index a56baa540d..e80f3fb844 100644 --- a/crates/perry-codegen/src/wasm32/runtime_abi.tsv +++ b/crates/perry-codegen/src/wasm32/runtime_abi.tsv @@ -1987,8 +1987,10 @@ js_native_call_method_nullsafe f64 f64,ptr,usize,ptr,usize js_native_call_method_patched_proto f64 f64,ptr,usize,ptr,usize js_native_call_method_patched_proto_apply f64 f64,ptr,usize,i64 js_native_call_method_str_key f64 f64,i64,ptr,usize +js_native_call_method_str_key_memo f64 f64,i64,ptr,usize,ptr js_native_call_method_value f64 f64,f64,ptr,usize js_native_call_method_value_apply f64 f64,f64,i64 +js_native_call_method_value_memo f64 f64,f64,ptr,usize,ptr js_native_call_value f64 f64,i64,ptr,usize js_native_handle_dispose i32s f64 js_native_handle_mark_main_thread void diff --git a/crates/perry-runtime/src/closure/dispatch.rs b/crates/perry-runtime/src/closure/dispatch.rs index e291fbf27a..782baa7f88 100644 --- a/crates/perry-runtime/src/closure/dispatch.rs +++ b/crates/perry-runtime/src/closure/dispatch.rs @@ -42,7 +42,9 @@ pub use calln::{ }; pub use direct::{DirectCall1, DirectCall2, DirectCall3, DirectCall4}; -pub(crate) use value_call::{call_compiled_closure_this, native_call_value_this}; +pub(crate) use value_call::{ + call_compiled_body_this, 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 ae88e46da5..fc0891f614 100644 --- a/crates/perry-runtime/src/closure/dispatch/value_call.rs +++ b/crates/perry-runtime/src/closure/dispatch/value_call.rs @@ -228,6 +228,24 @@ pub(crate) unsafe fn call_compiled_closure_this( call_closure_body(closure, info, func_ptr, this, args_ptr, args_len) } +/// [`call_compiled_closure_this`] for a caller that already holds the +/// closure's body `info` (a shape's ConstFn lane names it): no info read or +/// validation of the closure cell. +/// +/// # Safety +/// `closure` is a live closure of `info`, a compiled body (`FN_COMPILED_BODY`); +/// `args_ptr` holds `args_len` values. +#[inline] +pub(crate) unsafe fn call_compiled_body_this( + closure: *const ClosureHeader, + info: &'static crate::closure::JsFunctionInfo, + this: crate::closure::JsThis, + args_ptr: *const f64, + args_len: usize, +) -> f64 { + call_closure_body(closure, Some(info), info.code, 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)] diff --git a/crates/perry-runtime/src/closure/mod.rs b/crates/perry-runtime/src/closure/mod.rs index d90b97c63f..e963ce7766 100644 --- a/crates/perry-runtime/src/closure/mod.rs +++ b/crates/perry-runtime/src/closure/mod.rs @@ -66,7 +66,9 @@ pub(crate) use dispatch::{ 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(crate) use dispatch::{ + call_compiled_body_this, 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/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs index 502f879403..a8e4616314 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -1098,6 +1098,10 @@ pub fn gc_init() { reg_scanner!(crate::proxy::scan_setter_site_roots_mut); // An inherited method-site entry roots its direct prototype holder. reg_scanner!(crate::object::method_site::scan_method_site_roots_mut); + // A site's chain memo names every prototype from the receiver's + // [[Prototype]] to the holder of the method it answers; it compares each + // one's header word on use, so each is a STRONG root. + reg_scanner!(crate::object::method_site::chain_memo::scan_chain_memo_roots_mut); // A read site's holder entry names the object that holds the answer (and // the hops to it); the emitted hit loads through it, so each is a STRONG // root (`object::method_site::read_holder`). diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index c356e18e0e..1dfe30326f 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -3,9 +3,10 @@ //! registers its methods, getters, and setters at startup; //! `js_native_call_method` / `js_dynamic_object_get_property` look up //! the vtable by the object's `class_id` when static dispatch isn't -//! possible. Also home for the per-callsite inline cache -//! (`vtable_ic_*` / `call_vtable_method`) and the parent-chain -//! registration helpers used by codegen. +//! possible. Also home for `call_vtable_method` and the parent-chain +//! registration helpers used by codegen. A by-name call of a class +//! instance's string-keyed method does not consult the vtable: its +//! prototype chain's shapes answer it (`native_call_method::class_holder`). //! //! Split out of `object/mod.rs` (issue #1103). Pure relocation — no //! logic changes. @@ -123,10 +124,9 @@ pub use state::{ pub(crate) use prototype_objects::{ class_decl_prototype_relinked, class_prototype_object, decl_prototype_relinked, ensure_function_prototype_object, function_class_id, function_value_for_class_id, - instance_class_prototype_object, object_proto_chain_symbol_slot, relinked_class_prototype_read, - resolve_proto_chain_field, resolve_proto_chain_field_noting_miss, - resolve_proto_chain_field_with_receiver, resolve_proto_chain_symbol, - synthetic_class_prototype_object, SYNTHETIC_CLASS_ID_BASE, + object_proto_chain_symbol_slot, relinked_class_prototype_read, resolve_proto_chain_field, + resolve_proto_chain_field_noting_miss, resolve_proto_chain_field_with_receiver, + resolve_proto_chain_symbol, synthetic_class_prototype_object, SYNTHETIC_CLASS_ID_BASE, }; pub use prototype_objects::{ js_set_function_prototype, js_set_prototype_property, NEXT_SYNTHETIC_CLASS_ID, @@ -229,8 +229,7 @@ pub(crate) use dispatch::test_bump_vtable_generation; pub(crate) use dispatch::{ call_vtable_method, call_vtable_method_value, call_vtable_method_with_private_brand, class_lookup_surface_gen_bump, class_lookup_surface_generation, fetch_parent_kind_in_chain, - obj_dispatch_ic_insert, obj_dispatch_ic_lookup, vtable_generation, vtable_ic_insert, - vtable_ic_lookup, VTABLE_GEN, + vtable_generation, VTABLE_GEN, }; // ── parent_static.rs ──────────────────────────────────────────────────────── diff --git a/crates/perry-runtime/src/object/class_registry/dispatch.rs b/crates/perry-runtime/src/object/class_registry/dispatch.rs index fd5c637957..9bc42d841a 100644 --- a/crates/perry-runtime/src/object/class_registry/dispatch.rs +++ b/crates/perry-runtime/src/object/class_registry/dispatch.rs @@ -1,36 +1,22 @@ use super::*; use crate::JSValue; -use std::cell::UnsafeCell; use std::sync::atomic::{AtomicU64, Ordering}; // ============================================================================ -// Per-callsite-keyed inline cache for vtable method dispatch. +// Class-method calls through the vtable's function pointers (constructors, +// private and symbol-keyed members, the method-value trampoline), and the +// registration generation the store-plan cache keys on. // -// `js_native_call_method` is the hot dispatch tower for cross-module class -// instance method calls (e.g. `archetype.set(...)` from CommandBuffer.execute -// in the ECS workloads). Per profile, ~12% of perf-comprehensive samples land -// in `core::hash::BuildHasher` from the per-call `HashMap.get(method_name)` -// SipHash on the vtable lookup. -// -// Cache key: `(class_id, method_name_ptr)` where `method_name_ptr` is the -// rodata byte-pointer perry-codegen passes for the interned method name. The -// pointer is stable across calls within a module, so its address acts as a -// faster identity than re-hashing the bytes. Different modules may produce -// different rodata copies of the same name — the cache simply gets one entry -// per (class_id, name_pointer) pair, no correctness impact. -// -// Invalidation: a global `VTABLE_GEN` atomic is bumped on every -// `js_register_class_method` / `js_register_class_getter`. Each cache entry -// records the gen at populate time; lookups skip stale entries. Registration -// is one-shot at init in practice, so steady-state lookups never miss on -// gen. +// A by-name call of a class instance's string-keyed method is answered by its +// prototype chain's shapes (`native_call_method::class_holder`), not here: the +// per-(class, name) caches this module used to keep for it are gone. // ============================================================================ pub(crate) static VTABLE_GEN: AtomicU64 = AtomicU64::new(1); -/// Current vtable generation — consumed by caches (method IC below, the -/// store-plan cache in `object::prop_plan`) that must invalidate on any -/// class registration/mutation. +/// Current vtable generation — consumed by caches (the store-plan cache in +/// `object::prop_plan`) that must invalidate on any class +/// registration/mutation. #[inline] pub(crate) fn vtable_generation() -> u64 { VTABLE_GEN.load(Ordering::Relaxed) @@ -99,309 +85,6 @@ pub(crate) fn class_lookup_surface_gen_bump() { crate::object::proto_validity::bump_proto_validity(); } -const VTABLE_IC_SIZE: usize = 4096; -const VTABLE_IC_MASK: usize = VTABLE_IC_SIZE - 1; -/// Longest method name `VTABLE_IC` stores (and therefore caches). Longer names -/// take the uncached registry walk. -const VTABLE_IC_NAME_MAX: usize = 24; - -// #11341: the slot is chosen by the name's ADDRESS (cheap, and exact for the -// rodata strings codegen passes), but a hit also requires the name BYTES to -// match. Not every caller's name lives in rodata: the `new Function` / eval -// interpreter passes a transient Rust `String` (`dyn_eval::bridge::call_method`) -// and `js_native_call_method_value` passes the bytes of a GC string. Once such -// a buffer is freed, another method name can be allocated at the same address, -// and an address-only hit then dispatched the call to the FIRST name's method. -// mysql2's generated row parser — `result.two = packet.parseLengthCodedInt()` -// then `result.s = packet.readLengthCodedString()`, run by the interpreter — -// read `'hi'` back as the number 617 that way. -#[repr(C)] -#[derive(Copy, Clone)] -#[cfg_attr(test, derive(PartialEq, Debug))] -struct VTableICEntry { - gen: u64, - class_id: u32, - name_len: u32, - method_name_ptr: usize, - name: [u8; VTABLE_IC_NAME_MAX], - func_ptr: usize, - param_count: u32, - has_synthetic_arguments: u32, - has_rest: u32, -} - -#[cfg(test)] -const EMPTY_VTABLE_IC_ENTRY: VTableICEntry = VTableICEntry { - gen: 0, - class_id: 0, - name_len: 0, - method_name_ptr: 0, - name: [0; VTABLE_IC_NAME_MAX], - func_ptr: 0, - param_count: 0, - has_synthetic_arguments: 0, - has_rest: 0, -}; - -// SAFETY: integer fields only; `EMPTY_VTABLE_IC_ENTRY` is all-zero (#11507). -unsafe impl crate::zeroed_cache::ZeroEmpty for VTableICEntry {} - -crate::perry_thread_local! { - // arm64_32 fix: HEAP-allocate (Box) this ~160KB cache instead of inline TLS. - // Oversized `#[thread_local]` storage overflows the ILP32 TLS layout and its - // writes corrupt adjacent thread-locals. Boxing keeps only a pointer in TLS. - static VTABLE_IC: UnsafeCell> = - UnsafeCell::new(crate::zeroed_cache::new_zeroed_cache(VTABLE_IC_SIZE)); -} - -#[inline(always)] -fn vtable_ic_slot(class_id: u32, method_name_ptr: usize) -> usize { - // Mix class_id into the upper bits of the pointer to spread (class, name) - // pairs across slots. method_name_ptr is at least 1-byte aligned but - // typically 8+ for rodata strings, so shift by 3 to drop the alignment - // zeros before masking. - let key = method_name_ptr - .rotate_left(13) - .wrapping_add((class_id as usize).wrapping_mul(0x9E37_79B9)); - (key >> 3) & VTABLE_IC_MASK -} - -#[inline(always)] -pub(crate) unsafe fn vtable_ic_lookup( - class_id: u32, - method_name_ptr: usize, - name: &[u8], -) -> Option<(usize, u32, bool, bool)> { - if method_name_ptr == 0 || name.len() > VTABLE_IC_NAME_MAX { - return None; - } - let cur_gen = VTABLE_GEN.load(Ordering::Relaxed); - let slot = vtable_ic_slot(class_id, method_name_ptr); - VTABLE_IC.with(|cell| { - let cache = &**cell.get(); - let entry = &cache[slot]; - if entry.gen == cur_gen - && entry.class_id == class_id - && entry.method_name_ptr == method_name_ptr - && entry.name_len as usize == name.len() - && entry.name[..name.len()] == *name - { - Some(( - entry.func_ptr, - entry.param_count, - entry.has_synthetic_arguments != 0, - entry.has_rest != 0, - )) - } else { - None - } - }) -} - -#[inline(always)] -pub(crate) unsafe fn vtable_ic_insert( - class_id: u32, - method_name_ptr: usize, - name: &[u8], - func_ptr: usize, - param_count: u32, - has_synthetic_arguments: bool, - has_rest: bool, -) { - if method_name_ptr == 0 || name.len() > VTABLE_IC_NAME_MAX { - return; - } - let cur_gen = VTABLE_GEN.load(Ordering::Relaxed); - let slot = vtable_ic_slot(class_id, method_name_ptr); - let mut stored = [0u8; VTABLE_IC_NAME_MAX]; - stored[..name.len()].copy_from_slice(name); - VTABLE_IC.with(|cell| { - let cache = &mut **cell.get(); - cache[slot] = VTableICEntry { - gen: cur_gen, - class_id, - name_len: name.len() as u32, - method_name_ptr, - name: stored, - func_ptr, - param_count, - has_synthetic_arguments: if has_synthetic_arguments { 1 } else { 0 }, - has_rest: if has_rest { 1 } else { 0 }, - }; - }); -} - -// ============================================================================ -// #7769: object-dispatch-tower outcome cache. -// -// `js_native_call_method` resolves `obj.m(...)` by running a ~900-line tower of -// probes (native-module namespace, disposal protocol, TextDecoder handles, -// console, WeakMap/WeakSet, perf entries, own-field scan, prototype-chain -// walk) before finally consulting `CLASS_VTABLE_REGISTRY`. For an ordinary -// user-class instance every one of those probes misses, and the tail alone -// cost a `String` allocation for the method name, a GC-heap `StringHeader` -// allocation for the prototype probe, a `RwLock` read and two SipHash probes — -// per virtual call. -// -// This table records the tower's OUTCOME: an entry exists for -// `(class_id, method name)` only because a previous call for that exact pair -// ran the tower and reached a class-vtable resolution, which is the proof that -// no earlier probe claims this (class, name). -// -// There are two such resolution points and BOTH populate it: the parent-chain -// walk in `native_call_method::handle_methods` (which is where an INHERITED -// method resolves — `class Square extends Rect` calling `Rect`'s `area` — and -// therefore the common case in any real hierarchy), and the tail vtable arm of -// `js_native_call_method` (own-class methods). Populating only the tail left -// every inherited call permanently on the slow path. -// -// Writes go through `native_call_method::note_class_vtable_resolution`, which -// re-checks the receiver-shape predicate before storing, and the per-RECEIVER -// preconditions are re-verified again on every fast-path hit — see -// `native_call_method::class_vtable_fast_guard` — so a cache hit never -// substitutes for an object-specific check. -// -// Deliberately SEPARATE from `VTABLE_IC` above: that one is also written from -// the collection dispatcher, and it picks its slot by the name's ADDRESS (a hit -// there also compares the bytes since #11341). -// ============================================================================ - -// The key is the method-name BYTES, never its address. `VTABLE_IC` above keys -// its slot on the name pointer codegen passes — but -// `js_native_call_method_str_key` reaches the same tower with a name -// materialised into a CALLER-STACK scratch buffer (`str_bytes_from_jsvalue` -// with a `[u8; SHORT_STRING_MAX_LEN]`), and two different short names can land -// at the same stack address in successive calls. Comparing content makes the -// cache exact for both sources; names too long to store inline are simply not -// cached. -const OBJ_DISPATCH_IC_SIZE: usize = 1024; -const OBJ_DISPATCH_IC_MASK: usize = OBJ_DISPATCH_IC_SIZE - 1; -/// Longest method name the cache stores. Comfortably above every method name -/// in practice; longer names fall through to the tower. -const OBJ_DISPATCH_IC_NAME_MAX: usize = 24; - -#[repr(C)] -#[derive(Copy, Clone)] -#[cfg_attr(test, derive(PartialEq, Debug))] -struct ObjDispatchICEntry { - gen: u64, - class_id: u32, - name_len: u32, - name: [u8; OBJ_DISPATCH_IC_NAME_MAX], - func_ptr: usize, - param_count: u32, - has_synthetic_arguments: u32, - has_rest: u32, - _pad: u32, -} - -#[cfg(test)] -const EMPTY_OBJ_DISPATCH_IC_ENTRY: ObjDispatchICEntry = ObjDispatchICEntry { - gen: 0, - class_id: 0, - name_len: 0, - name: [0; OBJ_DISPATCH_IC_NAME_MAX], - func_ptr: 0, - param_count: 0, - has_synthetic_arguments: 0, - has_rest: 0, - _pad: 0, -}; - -// SAFETY: integer fields only; `EMPTY_OBJ_DISPATCH_IC_ENTRY` is all-zero. -unsafe impl crate::zeroed_cache::ZeroEmpty for ObjDispatchICEntry {} - -crate::perry_thread_local! { - // Boxed for the same arm64_32 reason as `VTABLE_IC`: oversized inline TLS - // storage overflows the ILP32 TLS layout. `perry_thread_local!` (#7469) - // rather than `std::thread_local!` — Darwin has no local-exec TLS, so the - // std form costs a real `_tlv_get_addr` call, which is exactly the tax the - // fast path exists to remove. - static OBJ_DISPATCH_IC: UnsafeCell> = - UnsafeCell::new(crate::zeroed_cache::new_zeroed_cache(OBJ_DISPATCH_IC_SIZE)); -} - -/// FNV-1a over the name bytes, mixed with the class id. -#[inline(always)] -fn obj_dispatch_ic_slot(class_id: u32, name: &[u8]) -> usize { - let mut h: u64 = - 0xcbf2_9ce4_8422_2325 ^ ((class_id as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15)); - for &b in name { - h ^= b as u64; - h = h.wrapping_mul(0x0100_0000_01b3); - } - ((h ^ (h >> 29)) as usize) & OBJ_DISPATCH_IC_MASK -} - -/// The vtable entry the object-dispatch tower previously resolved for this -/// `(class_id, method name)`, if it is still current. -#[inline] -pub(crate) fn obj_dispatch_ic_lookup( - class_id: u32, - name: &[u8], -) -> Option<(usize, u32, bool, bool)> { - if name.is_empty() || name.len() > OBJ_DISPATCH_IC_NAME_MAX { - return None; - } - let cur_gen = VTABLE_GEN.load(Ordering::Relaxed); - let slot = obj_dispatch_ic_slot(class_id, name); - OBJ_DISPATCH_IC.with(|cell| { - // SAFETY: the cache is thread-local and never handed out by reference - // across a call that could re-enter this module. - let entry = unsafe { &(**cell.get())[slot] }; - if entry.gen == cur_gen - && entry.class_id == class_id - && entry.name_len as usize == name.len() - && entry.name[..name.len()] == *name - { - Some(( - entry.func_ptr, - entry.param_count, - entry.has_synthetic_arguments != 0, - entry.has_rest != 0, - )) - } else { - None - } - }) -} - -/// Record that the tower resolved `(class_id, name)` to this vtable entry. -/// Only the tower's own vtable arm may call this. -#[inline] -pub(crate) fn obj_dispatch_ic_insert( - class_id: u32, - name: &[u8], - func_ptr: usize, - param_count: u32, - has_synthetic_arguments: bool, - has_rest: bool, -) { - if name.is_empty() || name.len() > OBJ_DISPATCH_IC_NAME_MAX { - return; - } - let cur_gen = VTABLE_GEN.load(Ordering::Relaxed); - let slot = obj_dispatch_ic_slot(class_id, name); - let mut stored = [0u8; OBJ_DISPATCH_IC_NAME_MAX]; - stored[..name.len()].copy_from_slice(name); - OBJ_DISPATCH_IC.with(|cell| { - // SAFETY: thread-local, no outstanding borrows (see `_lookup`). - unsafe { - (**cell.get())[slot] = ObjDispatchICEntry { - gen: cur_gen, - class_id, - name_len: name.len() as u32, - name: stored, - func_ptr, - param_count, - has_synthetic_arguments: u32::from(has_synthetic_arguments), - has_rest: u32::from(has_rest), - _pad: 0, - }; - } - }); -} - /// Maximum positional arity `call_vtable_method` can invoke directly. The /// dispatch builds a fixed-arity `extern "C"` fn signature for each arity up to /// this cap (see `vtable_call_dispatch!`). Synthesized capture-stashing @@ -690,176 +373,3 @@ pub(crate) fn fetch_parent_kind_in_chain(class_id: u32) -> Option { } None } - -#[cfg(test)] -mod obj_dispatch_ic_tests { - use super::*; - - const CID: u32 = 61_001; - - /// Run `body` on a stable vtable generation, retrying if it straddled a bump. - /// - /// Entries are keyed on `VTABLE_GEN`, and the whole point of that key is - /// that ANY class registration anywhere retires the cache. Sibling tests - /// register classes concurrently — `a_class_registration_invalidates_every_entry` - /// in THIS module calls `test_bump_vtable_generation()` on purpose — so an - /// insert/lookup pair can straddle a bump and miss for a reason that has - /// nothing to do with what is being asserted. - /// - /// #7365: the retry existed but could not fire. `body` asserted internally, - /// so a straddled pair **panicked on the setup assertion** before the loop - /// got to re-check the generation, and the retry budget was never spent. - /// That is why the failure rate INVERTED with scope: running just this - /// module (`--lib obj_dispatch_ic_tests`) put its five tests — including - /// the deliberate bumper — on threads together and failed 10 of 12 runs, - /// while the full 2000-test suite spread them out and failed about 1 in 6. - /// A filtered re-run, the standard triage move, therefore made an - /// intermittent test look reliably broken. - /// - /// So `body` now REPORTS whether its observations were valid instead of - /// asserting them: `false` means "a bump landed mid-pair, nothing was - /// learned", which is a retry rather than a failure. The assertions the - /// tests actually exist for stay assertions. - fn with_stable_gen(body: &dyn Fn() -> bool) { - for _ in 0..64 { - let before = VTABLE_GEN.load(Ordering::Acquire); - let observed = body(); - if observed && VTABLE_GEN.load(Ordering::Acquire) == before { - return; - } - } - panic!("vtable generation never stayed still long enough to assert"); - } - - #[test] - fn a_hit_requires_matching_name_bytes_not_a_matching_address() { - with_stable_gen(&|| { - // The hazard this test exists for: `js_native_call_method_str_key` - // materialises a short method name into a CALLER-STACK scratch buffer, - // so two different names genuinely do arrive at the same address in - // successive calls. An address-keyed cache would answer the second - // call with the first call's method — a silent wrong-method dispatch. - // - // Sabotage it deliberately: cache under one name, then look up a - // different name through the SAME backing storage. - let mut scratch = *b"area\0\0\0\0"; - obj_dispatch_ic_insert(CID, &scratch[..4], 0xAAAA, 1, false, false); - // Setup, not the subject: a miss here means a sibling bumped the - // generation between insert and lookup. Report it and retry. - if obj_dispatch_ic_lookup(CID, &scratch[..4]) != Some((0xAAAA, 1, false, false)) { - return false; - } - - scratch[..4].copy_from_slice(b"perim"[..4].try_into().unwrap()); - assert_eq!( - obj_dispatch_ic_lookup(CID, &scratch[..4]), - None, - "a different name at the same address must MISS" - ); - true - }); - } - - #[test] - fn vtable_ic_hit_requires_matching_name_bytes_not_only_the_address() { - // #11341: the interpreter (`dyn_eval::bridge::call_method`) and - // `js_native_call_method_value` pass method names that are NOT - // rodata, so a freed name buffer can come back holding a different - // name at the same address. Sabotage it the same way here: one - // backing buffer, two names. - with_stable_gen(&|| { - let mut scratch = *b"parseInt\0\0\0\0\0\0\0"; - let addr = scratch.as_ptr() as usize; - unsafe { vtable_ic_insert(CID, addr, &scratch[..8], 0xEEEE, 1, false, false) }; - if unsafe { vtable_ic_lookup(CID, addr, &scratch[..8]) } - != Some((0xEEEE, 1, false, false)) - { - return false; - } - scratch[..10].copy_from_slice(b"readString"); - assert_eq!( - unsafe { vtable_ic_lookup(CID, addr, &scratch[..10]) }, - None, - "a different name at the same address must MISS" - ); - true - }); - } - - #[test] - fn a_hit_requires_the_matching_class_id() { - with_stable_gen(&|| { - obj_dispatch_ic_insert(CID, b"describe", 0xBBBB, 1, false, false); - if obj_dispatch_ic_lookup(CID, b"describe") != Some((0xBBBB, 1, false, false)) { - return false; - } - assert_eq!(obj_dispatch_ic_lookup(CID + 1, b"describe"), None); - true - }); - } - - #[test] - fn a_class_registration_invalidates_every_entry() { - obj_dispatch_ic_insert(CID, b"perimeter", 0xCCCC, 1, false, false); - assert!(obj_dispatch_ic_lookup(CID, b"perimeter").is_some()); - // Registering a method anywhere bumps `VTABLE_GEN`; every cached - // resolution predates the new vtable shape and must stop being used. - test_bump_vtable_generation(); - assert_eq!(obj_dispatch_ic_lookup(CID, b"perimeter"), None); - } - - #[test] - fn names_too_long_to_store_are_never_cached() { - with_stable_gen(&|| { - let long = vec![b'x'; OBJ_DISPATCH_IC_NAME_MAX + 1]; - obj_dispatch_ic_insert(CID, &long, 0xDDDD, 1, false, false); - assert_eq!( - obj_dispatch_ic_lookup(CID, &long), - None, - "an over-long name must fall through to the tower, not alias a \ - truncated key" - ); - true - }); - } - - /// A name one byte shorter than a cached one must not hit it — the stored - /// length is part of the key, not just the prefix bytes. - #[test] - fn a_prefix_of_a_cached_name_misses() { - with_stable_gen(&|| { - obj_dispatch_ic_insert(CID, b"describe", 0xEEEE, 1, false, false); - assert_eq!(obj_dispatch_ic_lookup(CID, b"describ"), None); - true - }); - } -} - -#[cfg(test)] -mod zeroed_cache_tests { - use super::*; - - /// #11507: both ICs are zero-allocated rather than filled, so a thread's - /// first view of either must be the empty entry in every slot. - #[test] - fn fresh_thread_dispatch_ics_read_empty_everywhere() { - std::thread::spawn(|| { - VTABLE_IC.with(|cell| { - let cache = unsafe { &**cell.get() }; - assert_eq!(cache.len(), VTABLE_IC_SIZE); - for entry in cache.iter() { - assert_eq!(*entry, EMPTY_VTABLE_IC_ENTRY); - } - }); - OBJ_DISPATCH_IC.with(|cell| { - let cache = unsafe { &**cell.get() }; - assert_eq!(cache.len(), OBJ_DISPATCH_IC_SIZE); - for entry in cache.iter() { - assert_eq!(*entry, EMPTY_OBJ_DISPATCH_IC_ENTRY); - } - }); - }) - .join() - .unwrap(); - } -} diff --git a/crates/perry-runtime/src/object/class_registry/prototype_methods.rs b/crates/perry-runtime/src/object/class_registry/prototype_methods.rs index da7dc05753..c3ed653f7d 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_methods.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_methods.rs @@ -270,11 +270,10 @@ pub(crate) fn invalidate_class_prototype_fast_guards_for_method(name: &str) { /// ancestor declares — exactly the names whose resolution it can change. A /// name the class itself declares still resolves to its own body, and a /// name no declared class carries never had a direct arm; -/// * the `(class_id, method name)` dispatch caches (`VTABLE_IC`, -/// `OBJ_DISPATCH_IC`) are keyed on `VTABLE_GEN`, which the retirement bumps. -/// -/// The receiver-word site memos need nothing: the relink restamps `proto`'s -/// shape, which their hop facts compare. +/// The runtime's by-name method calls need nothing: they read the +/// prototype chain's shapes (`native_call_method::class_holder`), and the +/// relink restamps `proto`. Neither do the receiver-word site memos: the +/// relink restamps `proto`'s shape, which their hop facts compare. /// /// # Safety /// `proto` must point to a live, meta-capable object. diff --git a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs index 9505420d07..25a4d3e466 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs @@ -261,17 +261,6 @@ fn declared_parent_class_object( Some(proto_obj) } -/// [`class_prototype_object`] for a walk that serves an INSTANCE. Null where -/// that entry is a declared class's parent class object: its statics are not -/// on the instance's prototype chain (#10890). -pub(crate) fn instance_class_prototype_object(class_id: u32) -> *mut ObjectHeader { - let proto_obj = class_prototype_object(class_id); - if declared_parent_class_object(class_id, proto_obj).is_some() { - return std::ptr::null_mut(); - } - proto_obj -} - /// Perform ordinary `.prototype` assignment, then synchronize the synthetic /// class metadata used when a class extends a function (#711, #9365). #[no_mangle] diff --git a/crates/perry-runtime/src/object/class_registry/prototype_objects/parent_class_object_tests.rs b/crates/perry-runtime/src/object/class_registry/prototype_objects/parent_class_object_tests.rs index c41ee9d92b..e660ab83d6 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_objects/parent_class_object_tests.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_objects/parent_class_object_tests.rs @@ -7,9 +7,9 @@ //! read, and the heritage edge recorded by `js_register_class_parent_dynamic`. use super::super::{ - class_prototype_object, instance_class_prototype_object, is_class_object_ptr, - js_object_mark_class, js_register_class_id, js_register_class_name, - js_register_class_parent_dynamic, resolve_proto_chain_field, test_alloc_synthetic_class_id, + class_prototype_object, is_class_object_ptr, js_object_mark_class, js_register_class_id, + js_register_class_name, js_register_class_parent_dynamic, resolve_proto_chain_field, + test_alloc_synthetic_class_id, }; use crate::object::{ class_prototype_object_root_store, js_object_alloc, js_object_get_field_by_name, @@ -82,10 +82,6 @@ fn an_instance_reads_the_parent_evaluations_prototype_not_the_parent_constructor ); assert!(is_class_object_ptr(class as *const u8)); }); - assert!( - instance_class_prototype_object(CHILD).is_null(), - "a parent class object is not on an instance's prototype chain" - ); let instance = scope.root_raw_mut_ptr(js_object_alloc(CHILD, 0)); let name = instance.with_const_ptr::(|inst| read_string(inst, "name")); @@ -122,10 +118,7 @@ fn a_synthetic_prototype_that_is_a_class_object_stays_on_the_instance_chain() { assert_ne!(synthetic, 0, "the synthetic id range is exhausted"); class.with_mut_ptr::(|class| { class_prototype_object_root_store(synthetic, class); - assert_eq!( - instance_class_prototype_object(synthetic) as usize, - class as usize - ); + assert_eq!(class_prototype_object(synthetic) as usize, class as usize); }); let instance = scope.root_raw_mut_ptr(js_object_alloc(synthetic, 0)); let value = diff --git a/crates/perry-runtime/src/object/method_site.rs b/crates/perry-runtime/src/object/method_site.rs index d66d9eb895..038908ab7b 100644 --- a/crates/perry-runtime/src/object/method_site.rs +++ b/crates/perry-runtime/src/object/method_site.rs @@ -45,9 +45,7 @@ //! What the prime refuses (they keep the ordinary dispatch): non-ordinary //! receivers (class objects, native-module namespaces, dictionaries, //! `Object.prototype`, typed-array prototypes, exotic read receivers), -//! accessors, spill slots, class instances for the inherited entry (their -//! methods live in the vtable until class prototypes carry real slots, D4), -//! and any value that is not a plain closure the call can enter directly for +//! accessors, spill slots, and any value that is not a plain closure the call can enter directly for //! this site's argument count (bound functions, rest / `arguments` bodies, //! runtime thunks, class constructors, closures that capture `this`). //! @@ -92,6 +90,7 @@ use crate::object::ObjectHeader; +pub(crate) mod chain_memo; mod function_intrinsic; pub(crate) mod read_holder; use std::sync::atomic::{AtomicU64, Ordering}; @@ -301,7 +300,7 @@ pub fn method_site_stats() -> (u64, u64, u64) { } /// `js_method_site_stats(which)`: 0 own primes, 1 inherited primes, 2 misses, -/// 3 function-bag primes, 4 ConstFn own primes. +/// 3 function-bag primes, 4 ConstFn own primes, 5 chain memo ways recorded. /// Exposed so gap tests can prove a path ran. #[no_mangle] pub extern "C" fn js_method_site_stats(which: i32) -> f64 { @@ -311,6 +310,7 @@ pub extern "C" fn js_method_site_stats(which: i32) -> f64 { 1 => b, 3 => method_site_function_primes(), 4 => PRIMES_CONSTFN.load(Ordering::Relaxed), + 5 => chain_memo::chain_memo_records(), _ => c, }) as f64 } @@ -339,9 +339,10 @@ fn stats_report_enabled() -> bool { "[method-site] fn_intrinsic_primes={fp} fn_intrinsic_negative={fneg} fn_intrinsic_hits={fh}" ); eprintln!( - "[method-site] primes_own={a} primes_inherited={b} primes_function={} primes_constfn={} holder_rewrites={} misses={c} read_holder_primes={hd} read_absent_primes={ha} read_accessor_primes={ap} read_accessor_hits={ah} read_accessor_class_primes={} class_read_primes={cp} class_read_hits={ch} class_read_root_rewrites={cr} read_holder_rewrites={} read_accessor_rewrites={} read_accessor_same_shape_relinks={} read_holder_refused={hr}{refused}", + "[method-site] primes_own={a} primes_inherited={b} primes_function={} primes_constfn={} chain_memo_records={} holder_rewrites={} misses={c} read_holder_primes={hd} read_absent_primes={ha} read_accessor_primes={ap} read_accessor_hits={ah} read_accessor_class_primes={} class_read_primes={cp} class_read_hits={ch} class_read_root_rewrites={cr} read_holder_rewrites={} read_accessor_rewrites={} read_accessor_same_shape_relinks={} read_holder_refused={hr}{refused}", method_site_function_primes(), PRIMES_CONSTFN.load(Ordering::Relaxed), + chain_memo::chain_memo_records(), HOLDER_REWRITES.load(Ordering::Relaxed), read_holder::read_accessor_class_primes(), read_holder::read_holder_rewrites(), @@ -1027,27 +1028,48 @@ unsafe fn prime_inherited( refuse(11); return; } - // Class instances resolve methods through their vtable (D4: until class - // prototypes carry real slots). + // A declared-class instance's direct prototype is its class's prototype + // object (a bare CLASS identity: the class's function object keeps that + // link for the agent's life, and a relink retires the displaced + // prototype's ShapeId) or the serial a MIXED identity records. Its + // methods are real slots of that object (class prototypes hold function + // objects of their bodies, with ConstFn lanes), so the entry is the same + // holder entry as for any receiver. let class_id = (*obj).class_id; - if class_id != 0 + let class_instance = class_id != 0 && class_id < super::class_registry::prototype_objects::SYNTHETIC_CLASS_ID_BASE - && !super::is_anon_shape_class_id(class_id) - { - refuse(6); - return; - } + && !super::is_anon_shape_class_id(class_id); if key_may_be_accessor(obj, name) { refuse(8); return; } - // Only a serial or the realm-default identity pins one direct prototype. - let Some(proto_id) = read_holder::admitted_proto_id(obj) else { - refuse(7); - return; + let class_holder = if class_instance { + match read_holder::class_link(obj) { + Some(holder) => Some(holder), + None => { + refuse(6); + return; + } + } + } else { + None + }; + // Otherwise only a serial or the realm-default identity pins one direct + // prototype. + let proto_id = match class_holder { + Some(_) => super::shapes::PROTO_ID_CLASS, + None => match read_holder::admitted_proto_id(obj) { + Some(pid) => pid, + None => { + refuse(7); + return; + } + }, }; { - let next = if proto_id == super::shapes::PROTO_ID_DEFAULT { + let next = if let Some(holder) = class_holder { + holder + } else if proto_id == super::shapes::PROTO_ID_DEFAULT { crate::array::object_prototype_addr_if_resolved() as *const ObjectHeader } else { next_prototype(obj) diff --git a/crates/perry-runtime/src/object/method_site/chain_memo.rs b/crates/perry-runtime/src/object/method_site/chain_memo.rs new file mode 100644 index 0000000000..d816950a6f --- /dev/null +++ b/crates/perry-runtime/src/object/method_site/chain_memo.rs @@ -0,0 +1,396 @@ +//! The chain memo: a by-name method call site's memo of the prototype walk +//! that answered it (class-table retirement slice 3). +//! +//! A class instance's method call that the compiled code cannot answer +//! inline reaches the runtime by name: the method site's miss +//! ([`super::js_method_site_miss`]), a computed-key call `obj[k](...)`, or the +//! miss edge of a compiled class-method arm. The answer is a walk of the +//! receiver's prototype chain by SHAPES +//! (`native_call_method::class_holder`): every prototype before the holder +//! lacks the name, the holder's shape lists it at an inline slot. That walk +//! costs a key-list search per prototype; this memo lets a site answer a +//! repeat with one word compare per object instead. +//! +//! # What a way claims, and why it cannot go stale +//! +//! A way holds facts of ShapeIds only, each compared on every use, exactly +//! as the method site's inherited entry and the read site's holder entry +//! (`read_holder`) do: +//! +//! * the receiver's `(class_id | ShapeId)` word: the receiver lacks the name +//! (its key list), and its [[Prototype]] is the first hop (a declared +//! class's prototype, whose relink retires the displaced prototype's +//! ShapeId, or the serial its shape names); +//! * each hop's word, read from the hop itself: the hop lacks the name and +//! links to the next hop by the serial identity its shape records (a +//! `setPrototypeOf`, key add, delete or descriptor change restamps it); +//! * the holder's word: the name is an own inline DATA slot at `slot`, and, +//! when the shape's lane says ConstFn, the slot holds a function object of +//! `body`. Otherwise the hit re-proves the loaded value as a compiled +//! function, so a value overwrite without a shape change is seen. +//! +//! Nothing is keyed on a class id, an address or a name alone, and there is +//! no global invalidation word: a change to any object the way names moves +//! that object's word, and the next use misses and walks again. +//! +//! The memo is consulted before any other probe of the dispatch. A +//! computed-key site also records the key's bytes per way (the name is the +//! site's, otherwise). A method site's miss keeps no memo: its misses see +//! every kind of receiver, and a deep class chain there was measured flat +//! while the extra argument cost the plain-object misses. Ways are replaced in turn; after +//! [`CHAIN_MEMO_MAX_RECORDS`] replacements the site stops recording (it has +//! more receivers than ways) and its misses walk, as before the memo. +//! +//! # GC +//! +//! The hops are STRONG roots, rewritten when they move +//! ([`scan_chain_memo_roots_mut`]); every memo that recorded a way is +//! registered once. Recording runs on the walk's straight line, which +//! allocates nothing. +//! +//! # Agents +//! +//! Like every method site: once a worker exists no memo is read, recorded or +//! traced (the hops belong to the primary heap). + +use super::WORKER_AGENTS_EXIST; +use crate::object::ObjectHeader; +use std::sync::atomic::Ordering; + +/// Objects a way names: the prototypes from the receiver's [[Prototype]] to +/// the holder, inclusive. +pub const CHAIN_MEMO_MAX_HOPS: usize = 8; +/// Ways per site. +const CHAIN_MEMO_WAYS: usize = 2; +/// Longest computed key a way records. +const CHAIN_MEMO_KEY_MAX: usize = 32; +/// Replacements after which a site stops recording. +pub const CHAIN_MEMO_MAX_RECORDS: u32 = 16; + +#[repr(C)] +#[derive(Clone, Copy)] +struct Way { + /// The receiver's `(class_id | ShapeId << 32)` header word. + recv: u64, + /// Hops named (holder included); 0 = an empty way. + depth: u32, + /// The holder's inline slot. + slot: u32, + /// The `JsFunctionInfo` the holder shape's ConstFn lane names for the + /// slot, or 0. + body: usize, + /// A computed site's key length (its bytes in `key`). + key_len: u32, + _pad: u32, + key: [u8; CHAIN_MEMO_KEY_MAX], + /// Hop addresses, receiver's [[Prototype]] first (STRONG roots). + hops: [usize; CHAIN_MEMO_MAX_HOPS], + /// Each hop's header word when recorded. + words: [u64; CHAIN_MEMO_MAX_HOPS], +} + +/// One site's memo, allocated in the IC arena on its first record. All-zero +/// is the empty memo. +#[repr(C)] +pub struct ChainMemo { + ways: [Way; CHAIN_MEMO_WAYS], + records: u32, + registered: u32, +} + +/// The emitted `@perry_cmemo_N = private global ptr null` of a computed-key +/// call site, or the word after a compiled class-method site's learned word. +pub type ChainMemoSlot = *mut ChainMemo; + +/// How a by-name call names its site's memo: 0 (none), or a memo slot's +/// address, tagged [`MEMO_KEYED`] when the site's key is computed (each way +/// records the key's bytes). Every slot is 8-byte aligned, so the low bits +/// are free. +pub(crate) type MemoRef = usize; +const MEMO_KEYED: usize = 1; + +/// The memo slot `memo` names. +#[inline] +fn memo_slot(memo: MemoRef) -> *mut ChainMemoSlot { + (memo & !MEMO_KEYED) as *mut ChainMemoSlot +} + +/// The [`MemoRef`] of a memo slot. +#[inline] +pub(crate) fn memo_ref_slot(slot: *mut ChainMemoSlot, keyed: bool) -> MemoRef { + if slot.is_null() { + return 0; + } + slot as usize | if keyed { MEMO_KEYED } else { 0 } +} + +/// Every memo that recorded a way, for the primary agent's root scan. +static CHAIN_MEMOS: std::sync::Mutex> = std::sync::Mutex::new(Vec::new()); + +per_test_global! { + static MEMO_RECORDS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); +} + +/// Ways recorded (diagnostic; tests prove a path ran). +pub fn chain_memo_records() -> u64 { + MEMO_RECORDS.load(Ordering::Relaxed) +} + +#[inline(always)] +unsafe fn header_word(addr: usize) -> u64 { + std::ptr::read(addr as *const u64) +} + +/// The value a way of `memo` names for `object`, and the body its holder's +/// shape names for the slot: `object` is a heap ordinary object whose +/// metadata record, if any, holds storage only, and its header word, every +/// hop's word and (for a keyed site) `name` equal a way's. `None` otherwise. +/// Loads only. +/// +/// # Safety +/// `memo` is 0 or names a live memo slot. +#[inline] +pub(crate) unsafe fn memo_lookup_value( + memo: MemoRef, + object: f64, + name: &[u8], +) -> Option<(u64, Option<&'static crate::closure::JsFunctionInfo>)> { + let memo_ptr = crate::object::pic_slot_peek(memo_slot(memo)); + if memo_ptr.is_null() || WORKER_AGENTS_EXIST.load(Ordering::Relaxed) != 0 { + return None; + } + let bits = object.to_bits(); + if bits & !crate::value::POINTER_MASK != crate::value::POINTER_TAG { + return None; + } + let addr = (bits & crate::value::POINTER_MASK) as usize; + if !crate::value::addr_class::is_above_handle_band(addr) { + return None; + } + let header = crate::value::addr_class::try_read_gc_header(addr)?; + if header.obj_type != crate::gc::GC_TYPE_OBJECT + || header.gc_flags & crate::gc::GC_FLAG_FORWARDED != 0 + { + return None; + } + // The receiver predicate's per-object part its word does not carry: a + // metadata record only for storage (no recorded [[Prototype]], no flags, + // no brand, no dictionary keys). + let meta = (*(addr as *const ObjectHeader)).meta; + if !meta.is_null() + && ((*meta).prototype != 0 + || (*meta).flags != 0 + || (*meta).private_evaluation_brand != 0 + || (*meta).dictionary_keys != 0) + { + return None; + } + ways_lookup(&*memo_ptr, memo & MEMO_KEYED != 0, addr, name) +} + +#[inline] +unsafe fn ways_lookup( + memo: &ChainMemo, + keyed: bool, + recv: usize, + name: &[u8], +) -> Option<(u64, Option<&'static crate::closure::JsFunctionInfo>)> { + let word = header_word(recv); + 'ways: for way in memo.ways.iter() { + if way.recv != word || way.depth == 0 { + continue; + } + if keyed && (way.key_len as usize != name.len() || way.key[..name.len()] != *name) { + continue; + } + let depth = way.depth as usize; + for i in 0..depth { + if header_word(way.hops[i]) != way.words[i] { + continue 'ways; + } + } + let holder = way.hops[depth - 1]; + let value = std::ptr::read( + (holder + std::mem::size_of::() + way.slot as usize * 8) as *const u64, + ); + let body = (way.body as *const crate::closure::JsFunctionInfo).as_ref(); + return Some((value, body)); + } + None +} + +/// A hop a way may name: an ordinary, shaped object at a stable heap address +/// whose shape answers own string-keyed lookups. +#[inline] +unsafe fn hop_admitted(addr: usize) -> bool { + if !crate::value::addr_class::is_above_handle_band(addr) + || !super::address_is_prime_stable(addr) + { + return false; + } + let Some(header) = crate::value::addr_class::try_read_gc_header(addr) else { + return false; + }; + let obj = addr as *const ObjectHeader; + if header.obj_type != crate::gc::GC_TYPE_OBJECT + || header.gc_flags & crate::gc::GC_FLAG_FORWARDED != 0 + || header._reserved & crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO != 0 + || crate::closure::is_closure_ptr(addr) + || (*obj).class_id == crate::object::NATIVE_MODULE_CLASS_ID + || crate::object::dictionary::is_dictionary(obj) + || crate::object::shapes::object_shape_stamp(obj) == 0 + { + return false; + } + let meta = (*obj).meta; + meta.is_null() + || ((*meta).elements == 0 + && (*meta).dictionary_keys == 0 + && (*meta).flags & crate::object::OBJECT_META_FLAG_EXOTIC_READ_RECEIVER == 0) +} + +/// The next hop after `hop`, when `hop`'s shape pins it: a serial identity +/// (plain or MIXED with its class), or the realm's `%Object.prototype%`. +#[inline] +unsafe fn pinned_next(hop: *const ObjectHeader) -> Option<*const ObjectHeader> { + use crate::object::shapes::{ + shape_proto_id, PROTO_ID_CLASS, PROTO_ID_DEFAULT, PROTO_ID_MIXED, PROTO_ID_NULL, + PROTO_ID_UNIQUE, + }; + let pid = shape_proto_id(crate::object::shapes::object_shape_stamp(hop))?; + let (stated, word) = super::read_holder::stated_link(hop); + if stated != pid { + return None; + } + let next = if pid == PROTO_ID_DEFAULT { + crate::array::object_prototype_addr_if_resolved() as *const ObjectHeader + } else if pid == PROTO_ID_NULL { + return None; + } else if pid < PROTO_ID_CLASS || (PROTO_ID_MIXED..PROTO_ID_UNIQUE).contains(&pid) { + super::read_holder::next_from_word(hop, word) + } else { + return None; + }; + (!next.is_null() && next != hop).then_some(next) +} + +/// Is `name` an array index, which a hop's element storage (not its shape) +/// would answer? +#[inline] +fn name_is_index(name: &[u8]) -> bool { + !name.is_empty() && name.len() <= 10 && name.iter().all(u8::is_ascii_digit) +} + +/// Record the walk that answered `name` for `recv`: its chain from `start` +/// (the receiver's [[Prototype]]) to `holder`, where the name is an own +/// inline data slot `slot` whose ConstFn lane (if any) names `body`. Every +/// fact is re-read here from shapes; any object whose shape does not pin it +/// refuses, and the site keeps walking. Allocation-free on the GC heap. +/// +/// # Safety +/// `memo` is 0 or names a live memo slot; the objects are live, and no +/// collection runs between the walk and this call. +pub(crate) unsafe fn memo_record( + memo: MemoRef, + recv: *const ObjectHeader, + start: *const ObjectHeader, + holder: *const ObjectHeader, + holder_slot: u32, + body: Option<&'static crate::closure::JsFunctionInfo>, + name: &[u8], +) { + if memo == 0 || WORKER_AGENTS_EXIST.load(Ordering::Relaxed) != 0 || name_is_index(name) { + return; + } + let keyed = memo & MEMO_KEYED != 0; + if keyed && name.len() > CHAIN_MEMO_KEY_MAX { + return; + } + let existing = crate::object::pic_slot_peek(memo_slot(memo)); + if !existing.is_null() && (*existing).records >= CHAIN_MEMO_MAX_RECORDS { + return; + } + // The receiver's word pins its [[Prototype]] only as its shape states it. + if super::read_holder::class_link(recv) != Some(start) { + return; + } + let mut way = Way { + recv: header_word(recv as usize), + depth: 0, + slot: holder_slot, + body: body.map_or(0, |b| b as *const _ as usize), + key_len: 0, + _pad: 0, + key: [0; CHAIN_MEMO_KEY_MAX], + hops: [0; CHAIN_MEMO_MAX_HOPS], + words: [0; CHAIN_MEMO_MAX_HOPS], + }; + if keyed { + way.key_len = name.len() as u32; + way.key[..name.len()].copy_from_slice(name); + } + let mut hop = start; + loop { + let depth = way.depth as usize; + if depth == CHAIN_MEMO_MAX_HOPS || !hop_admitted(hop as usize) { + return; + } + way.hops[depth] = hop as usize; + way.words[depth] = header_word(hop as usize); + way.depth += 1; + if hop == holder { + break; + } + let Some(next) = pinned_next(hop) else { + return; + }; + hop = next; + } + let Some(record) = crate::object::shapes::object_shape_record(holder) else { + return; + }; + if holder_slot >= record.live_inline_slot_count() { + return; + } + let Ok(mut memos) = CHAIN_MEMOS.lock() else { + return; + }; + let memo = crate::object::pic_slot_resolve(memo_slot(memo)); + if memo.is_null() { + return; + } + let memo = &mut *memo; + let idx = memo + .ways + .iter() + .position(|w| w.depth == 0) + .unwrap_or(memo.records as usize % CHAIN_MEMO_WAYS); + memo.records += 1; + if memo.registered == 0 { + memo.registered = 1; + memos.push(memo as *mut ChainMemo as usize); + } + memo.ways[idx] = way; + MEMO_RECORDS.fetch_add(1, Ordering::Relaxed); +} + +/// Root scan: before workers exist, every recorded hop is marked and +/// rewritten when it moves. After the sticky worker gate no memo is read, so +/// the scan stops and otherwise-dead hops can collect. +pub(crate) fn scan_chain_memo_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + if crate::agent::current_agent() != crate::agent::PRIMARY_AGENT + || WORKER_AGENTS_EXIST.load(Ordering::SeqCst) != 0 + { + return; + } + if let Ok(memos) = CHAIN_MEMOS.lock() { + for &memo in memos.iter() { + // SAFETY: registered memos are IC-arena allocations, never freed. + for way in unsafe { (*(memo as *mut ChainMemo)).ways.iter_mut() } { + for i in 0..way.depth as usize { + visitor.visit_tagged_usize_slot(&mut way.hops[i], crate::value::POINTER_TAG); + } + } + } + } +} diff --git a/crates/perry-runtime/src/object/method_site/read_holder.rs b/crates/perry-runtime/src/object/method_site/read_holder.rs index 5795ba912d..bc3fabc081 100644 --- a/crates/perry-runtime/src/object/method_site/read_holder.rs +++ b/crates/perry-runtime/src/object/method_site/read_holder.rs @@ -614,7 +614,7 @@ pub(super) unsafe fn admitted_proto_id(obj: *const ObjectHeader) -> Option /// names its registry-resolved prototype: priming resolves the live /// declared-prototype pointer, and a later relink retires that pointer's /// ShapeId (see the module docs), which the hit's holder compare sees. -unsafe fn class_link(recv: *const ObjectHeader) -> Option<*const ObjectHeader> { +pub(super) unsafe fn class_link(recv: *const ObjectHeader) -> Option<*const ObjectHeader> { let pid = shape_proto_id(object_shape_stamp(recv))?; let (stated, word) = stated_link(recv); if stated != pid { diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index bf183aef58..5529573366 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -9,6 +9,7 @@ use super::*; mod bare_receiver; +mod class_holder; mod collection_methods; mod common_methods; mod direct_site; @@ -19,6 +20,7 @@ pub(crate) use function_shape::{ FunctionIntrinsicFacts, }; mod handle_methods; +mod memo_entries; mod namespace_override; mod object_proto; mod own_slot; @@ -40,7 +42,7 @@ mod probe_dispatch_tests; mod to_locale_string_tests; mod typed_array; #[cfg(test)] -/// #10724: the vtable guard's own-key scan never uses the element accessor. +/// #10724: the class receiver guard's own-key scan never uses the element accessor. mod vtable_guard_scan_tests; use bare_receiver::{ @@ -65,46 +67,9 @@ pub(crate) use proto_dispatch::{ }; pub(super) use typed_array::dispatch_typed_array_method; -/// #7769: skip the dispatch tower for an ordinary user-class instance whose -/// `(class_id, method_name)` the tower has already resolved to a vtable method. -/// -/// `js_native_call_method` is the virtual-call path for every receiver whose -/// static type does not pin the callee — which is *every* call through a -/// base-typed collection, the shape a class hierarchy is written in. Reaching -/// its vtable arm costs a `String` allocation for the method name, a -/// `RuntimeHandleScope`, ~900 lines of probes for exotic receiver kinds, a -/// GC-heap `StringHeader` allocation for the prototype-chain probe, a -/// process-global `RwLock` read and two SipHash lookups. For `shape.area()` -/// that is four heap allocations and a lock around a single multiply. -/// -/// # Why a cache hit is sound -/// -/// An [`obj_dispatch_ic`](crate::object::class_registry::obj_dispatch_ic_lookup) -/// entry exists ONLY because an earlier call with this exact -/// `(class_id, method_name_ptr)` ran the entire tower and fell through to the -/// vtable arm. That is the proof that no *name-keyed* or *class-keyed* probe in -/// the tower claims this pair. -/// -/// Everything the tower decides per RECEIVER rather than per (class, name) is -/// re-established here, on every hit: -/// -/// * the value is a NaN-boxed pointer to a real heap object above the handle -/// band (excludes every small-handle registry receiver, and every primitive); -/// * its GC type is `GC_TYPE_OBJECT` and its GcHeader carries no class-object -/// marker (excludes errors, arrays, maps, buffers, regexes, closures, and -/// class values — each of which the tower routes elsewhere); -/// * `class_id` matches the cache key; -/// * `meta` is null, so the object carries no `Object.setPrototypeOf` override, -/// no per-key descriptor state, and no exotic-kind tag — this is *stricter* -/// than the tower, which tolerates a meta record and resolves through it; -/// * no OWN key equals the method name, through the shared content-validated -/// key index (an own field shadows the vtable); -/// * no static prototype is recorded for the address, so the tower's -/// `resolve_inherited_field` probe would have found nothing to shadow with. -/// -/// A miss (`None`) is always safe: the caller falls through to the full tower. -/// The receiver-shape predicate `G` shared by the fast path and by the sites -/// that are allowed to populate its cache. +/// The receiver predicate of a class instance's fast method call +/// ([`class_holder::try_class_holder_fast_dispatch`]) and of the compiled +/// class-method sites' learned words (`direct_site`). /// /// Returns the receiver's `class_id` when `object` is an ORDINARY heap /// instance of a user class: everything the dispatch tower decides per RECEIVER @@ -125,16 +90,16 @@ pub(super) use typed_array::dispatch_typed_array_method; /// * `meta` null — no `Object.setPrototypeOf` override, no per-key descriptor /// state, no exotic-kind tag. STRICTER than the tower, which resolves /// through a meta record; -/// * no OWN key equal to the method name (an own field shadows the vtable), +/// * no OWN key equal to the method name (an own field shadows the prototype), /// using the shared content-validated key index; /// * no recorded static prototype for the address, so the tower's /// `resolve_inherited_field` probe had nothing to shadow with. #[inline] -unsafe fn class_vtable_fast_guard(object: f64, method_bytes: &[u8]) -> Option<(usize, u32)> { - class_vtable_receiver_guard::(object, method_bytes) +unsafe fn class_receiver_fast_guard(object: f64, method_bytes: &[u8]) -> Option<(usize, u32)> { + class_receiver_guard::(object, method_bytes) } -/// [`class_vtable_fast_guard`], optionally accepting a receiver that carries +/// [`class_receiver_fast_guard`], optionally accepting a receiver that carries /// an [`ObjectMeta`](crate::object::ObjectMeta) record for storage only. /// /// With `META_STORAGE_OK`, a metadata record is accepted when every field @@ -151,7 +116,7 @@ unsafe fn class_vtable_fast_guard(object: f64, method_bytes: &[u8]) -> Option<(u /// changes, whereas the `(class, name)` cache this guard otherwise feeds is /// not. #[inline] -unsafe fn class_vtable_receiver_guard( +unsafe fn class_receiver_guard( object: f64, method_bytes: &[u8], ) -> Option<(usize, u32)> { @@ -207,13 +172,13 @@ unsafe fn class_vtable_receiver_guard( return None; } - // Own fields shadow vtable methods — the same indexed lookup as the + // Own fields shadow prototype methods — the same indexed lookup as the // tower's field lookup. ShapeId supplies both the moving root and its exact // logical length; the ObjectHeader mirrors are compatibility scratch only. let descriptor = crate::object::shapes::object_shape_descriptor(obj)?; // #10868 step 2.5 stage 1: a dictionary-mode receiver's own fields are in // its `ObjectMeta`, so a null `keys` word would make this shadowing scan - // vacuously true and let a vtable method win over an own field. + // vacuously true and let a prototype method win over an own field. if crate::object::dictionary::is_dictionary(obj) { return None; } @@ -232,7 +197,7 @@ unsafe fn class_vtable_receiver_guard( return None; } // #10502: the shared shape index proves presence AND absence after - // its first build. Do not re-scan every field on a vtable cache hit. + // its first build. Do not re-scan every field on every call. if own_slot::find_method_slot(keys, key_count as u32, method_bytes).is_some() { return None; } @@ -248,7 +213,7 @@ unsafe fn class_vtable_receiver_guard( } /// True for the method names whose tower probes depend on per-object state -/// [`class_vtable_fast_guard`] does not pin. +/// [`class_receiver_fast_guard`] does not pin. /// /// * the `using` / `await using` disposal hooks read a SYMBOL-keyed own /// property (`obj[Symbol.dispose]`), which the guard's descriptor-backed @@ -265,84 +230,6 @@ pub(crate) fn method_name_is_fast_dispatch_ineligible(name: &str) -> bool { ) || crate::iterator_helpers::is_iterator_helper_method(name) } -#[inline] -unsafe fn try_class_vtable_fast_dispatch( - object: f64, - method_name_ptr: *const i8, - method_name_len: usize, - args_ptr: *const f64, - args_len: usize, -) -> Option { - if method_name_ptr.is_null() || method_name_len == 0 { - return None; - } - let method_bytes = std::slice::from_raw_parts(method_name_ptr as *const u8, method_name_len); - let (obj_addr, class_id) = class_vtable_fast_guard(object, method_bytes)?; - let (func_ptr, param_count, has_synthetic_arguments, has_rest) = - crate::object::class_registry::obj_dispatch_ic_lookup(class_id, method_bytes)?; - // A synthesized `arguments` object or a user rest param makes - // `call_vtable_method` allocate a JS array for that slot — a collection - // point. `obj_addr` is a bare local here (no handle scope: not creating one - // is most of the win), so keep the fast path free of any allocation - // between reading the receiver address and entering the callee. These two - // shapes are rare; the tower roots the receiver and handles them. - if has_synthetic_arguments || has_rest { - return None; - } - - // The recursion-depth guard is kept on the fast path. Skipping it would be - // a few instructions cheaper, but a cached dispatch is still a dispatch: - // mutually-recursive `a.m()`/`b.m()` chains reach the same unbounded stack - // growth this guard exists to stop, and once cached they would reach it - // WITHOUT ever being counted. - let _depth_guard = CallMethodDepthGuard::enter("")?; - - Some(crate::object::class_registry::call_vtable_method( - func_ptr, - obj_addr as i64, - args_ptr, - args_len, - param_count, - has_synthetic_arguments, - has_rest, - )) -} - -/// Record a class-walk resolution for the fast path, but only for a receiver -/// that satisfies [`class_vtable_fast_guard`] — the same predicate the fast -/// path re-checks — and only for a name whose tower probes are class/name -/// keyed. -/// -/// Callers are the two sites where the tower resolves an ORDINARY class -/// instance's method: the parent-chain walk in -/// `native_call_method::handle_methods` (which serves inherited methods, the -/// common case) and the tail vtable arm in `js_native_call_method`. -#[inline] -pub(crate) unsafe fn note_class_vtable_resolution( - object: f64, - method_name: &str, - func_ptr: usize, - param_count: u32, - has_synthetic_arguments: bool, - has_rest: bool, -) { - if method_name_is_fast_dispatch_ineligible(method_name) { - return; - } - let bytes = method_name.as_bytes(); - let Some((_, class_id)) = class_vtable_fast_guard(object, bytes) else { - return; - }; - crate::object::class_registry::obj_dispatch_ic_insert( - class_id, - bytes, - func_ptr, - param_count, - has_synthetic_arguments, - has_rest, - ); -} - unsafe fn call_primitive_closure_value( receiver: f64, value: JSValue, @@ -734,6 +621,21 @@ pub unsafe extern "C-unwind" fn js_native_call_method_value( key: f64, args_ptr: *const f64, args_len: usize, +) -> f64 { + native_call_method_value_memo(object, key, args_ptr, args_len, 0) +} + +/// [`js_native_call_method_value`] for a site that owns a chain memo +/// (`memo_entries`, a keyed [`class_holder::MemoRef`]), or none (0). +/// +/// # Safety +/// As [`js_native_call_method_value`]; `memo` is 0 or names a live memo slot. +pub(crate) unsafe fn native_call_method_value_memo( + object: f64, + key: f64, + args_ptr: *const f64, + args_len: usize, + memo: class_holder::MemoRef, ) -> f64 { let key_jsval = JSValue::from_bits(key.to_bits()); let is_symbol_key = crate::symbol::js_is_symbol(key) != 0; @@ -892,6 +794,11 @@ pub unsafe extern "C-unwind" fn js_native_call_method_value( if !str_ptr.is_null() { let bytes_ptr = (str_ptr as *const i8).add(std::mem::size_of::()); let bytes_len = (*str_ptr).byte_len as usize; + if memo != 0 { + return memo_entries::memo_call( + object, bytes_ptr, bytes_len, args_ptr, args_len, memo, false, + ); + } return js_native_call_method(object, bytes_ptr, bytes_len, args_ptr, args_len); } } @@ -1242,6 +1149,40 @@ pub unsafe extern "C-unwind" fn js_native_call_method( args_ptr: *const f64, args_len: usize, ) -> f64 { + native_call_method_tower( + object, + method_name_ptr, + method_name_len, + args_ptr, + args_len, + 0, + ) +} + +/// The body of [`js_native_call_method`], for a call site that may own a +/// chain memo (`memo`, see `method_site::chain_memo`; 0 for none): a repeat +/// the memo names is answered before any probe (each declines an ordinary +/// class instance), and the class instance's fast call records its walk +/// there. +/// +/// # Safety +/// As [`js_native_call_method`]; `memo` is 0 or names a live memo slot. +pub(crate) unsafe fn native_call_method_tower( + object: f64, + method_name_ptr: *const i8, + method_name_len: usize, + args_ptr: *const f64, + args_len: usize, + memo: class_holder::MemoRef, +) -> f64 { + if memo != 0 && !method_name_ptr.is_null() { + let name = std::slice::from_raw_parts(method_name_ptr as *const u8, method_name_len); + if let Some(result) = + class_holder::try_chain_memo_dispatch(object, memo, name, args_ptr, args_len) + { + return result; + } + } // #9675: a LEGACY BARE managed receiver — a real GC pointer that was never // NaN-boxed — must be reboxed under its true tag HERE, before the root // below and before the first probe. See `bare_receiver` for why the tail @@ -1314,13 +1255,18 @@ pub unsafe extern "C-unwind" fn js_native_call_method( } } } - // #7769: the tower's own previously-computed answer for this - // (class_id, method_name) pair, when the receiver still satisfies every - // per-object precondition. See `try_class_vtable_fast_dispatch`. - if let Some(result) = - try_class_vtable_fast_dispatch(object, method_name_ptr, method_name_len, args_ptr, args_len) - { - return result; + // A class instance's method: the shapes of its prototype chain name the + // holder and the slot (`class_holder`), for an ordinary receiver whose + // per-object state the tower's probes would not act on. + if !method_name_ptr.is_null() && method_name_len > 0 { + let key = class_holder::MethodKey { + bytes: std::slice::from_raw_parts(method_name_ptr as *const u8, method_name_len), + }; + if let Some(result) = + class_holder::try_class_holder_fast_dispatch(object, &key, args_ptr, args_len, memo) + { + return result; + } } // #10522: `t.unref()` & co. on a pristine timer handle; the guard proves the // tower would resolve the family's own native method (`timer::handle_object`). @@ -2356,49 +2302,18 @@ pub unsafe extern "C-unwind" fn js_native_call_method( } } - // Vtable lookup: check if this class has a registered method in the vtable - let class_id = (*obj).class_id; - if class_id != 0 - && (!class_prototype_fast_guard_invalidated_for_method( - class_prototype_method_guard_slot(method_name), - ) || !class_proto_key_deleted(class_id, method_name)) - { - if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { - if let Some(ref reg) = *registry { - if let Some(vtable) = reg.get(&class_id) { - if let Some(entry) = vtable.methods.get(method_name) { - let this_i64 = jsval().as_pointer::() as i64; - // #7769: reaching HERE is the proof that no - // name-keyed or class-keyed probe above claims - // this (class_id, method_name) — record it so the - // next call can go straight to the method. The - // per-receiver preconditions are re-checked on - // every hit; see `try_class_vtable_fast_dispatch`. - let func_ptr = entry.func_ptr; - let param_count = entry.param_count; - let has_synthetic_arguments = entry.has_synthetic_arguments; - let has_rest = entry.has_rest; - note_class_vtable_resolution( - object(), - method_name, - func_ptr, - param_count, - has_synthetic_arguments, - has_rest, - ); - return call_vtable_method( - func_ptr, - this_i64, - args_ptr, - args_len, - param_count, - has_synthetic_arguments, - has_rest, - ); - } - } - } - } + // A class instance's method: the property its prototype chain's + // shapes name (`class_holder`), the class's prototype object built + // first if no read has built it yet. + if let Some(result) = class_holder::call_class_instance_member( + &object_handle, + &arg_handles, + (*obj).class_id, + method_name, + args_ptr, + args_len, + ) { + return result; } } @@ -2463,38 +2378,28 @@ pub unsafe extern "C-unwind" fn js_native_call_method( if jsval().is_int32() { let payload = jsval().as_int32() as u32; if payload != 0 { - let guard = REGISTERED_CLASS_IDS.read().unwrap(); - if let Some(set) = guard.as_ref() { - if set.contains(&payload) { - if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { - if let Some(ref reg) = *registry { - if let Some(vtable) = reg.get(&payload) { - if let Some(entry) = vtable.methods.get(method_name) { - let undefined_this = - f64::from_bits(crate::value::TAG_UNDEFINED); - return call_vtable_method( - entry.func_ptr, - undefined_this.to_bits() as i64, - args_ptr, - args_len, - entry.param_count, - entry.has_synthetic_arguments, - entry.has_rest, - ); - } - } - } - } - if matches!(method_name, "pipe" | "annotations") { - return object(); - } - crate::error::js_throw_type_error_not_a_function( - std::ptr::null(), - 0, - method_name.as_ptr(), - method_name.len(), - ); + let registered = REGISTERED_CLASS_IDS + .read() + .unwrap() + .as_ref() + .is_some_and(|set| set.contains(&payload)); + if registered { + // The class's prototype method, called with an undefined + // `this`: the property `C.prototype` holds for the name. + if let Some(result) = + class_holder::call_class_ref_method(payload, method_name, &arg_handles) + { + return result; + } + if matches!(method_name, "pipe" | "annotations") { + return object(); } + crate::error::js_throw_type_error_not_a_function( + std::ptr::null(), + 0, + method_name.as_ptr(), + method_name.len(), + ); } } } diff --git a/crates/perry-runtime/src/object/native_call_method/class_holder.rs b/crates/perry-runtime/src/object/native_call_method/class_holder.rs new file mode 100644 index 0000000000..d8b6546dc3 --- /dev/null +++ b/crates/perry-runtime/src/object/native_call_method/class_holder.rs @@ -0,0 +1,733 @@ +//! A by-name method call on a class instance, answered by its prototype +//! chain's shapes (class-table retirement slice 3). +//! +//! A class's methods are own data properties of its prototype object (slice 1: +//! each slot holds a function object of the method's own body, a ConstFn lane +//! of the prototype's shape; slice 2: the class reaches that object through its +//! own function object). `recv.m(args)` is therefore `[[Get]](recv, "m")` +//! followed by a call, and the [[Get]] is a walk of SHAPES: the receiver's +//! shape lacks `m`, each prototype's shape lists its keys, and the first holder +//! whose shape lists `m` has it in the slot its shape names. Nothing here asks +//! the class registry which method a class id has, and nothing is cached by +//! class id or by name: a holder's key list is the only place a name is +//! compared, by atom identity first (a canonical key list holds its text's +//! atom, which is also what a compiled call site passes) and by bytes for a +//! list written before its atom existed. +//! +//! A shape fact changes with the object it describes: a method assigned, +//! deleted or redefined on a prototype, or a prototype relinked, restamps that +//! object, so the walk reads the current answer every time. Whatever the walk +//! cannot read from shapes alone (an accessor, a dictionary-mode or exotic +//! holder, an unlinked prototype identity) is handed back as [`ChainMethod::Get`] +//! for the caller's ordinary [[Get]] from that holder. + +use super::*; +pub(crate) use crate::object::method_site::chain_memo::MemoRef; + +/// The name being called. +#[derive(Clone, Copy)] +pub(crate) struct MethodKey<'a> { + pub(crate) bytes: &'a [u8], +} + +impl<'a> MethodKey<'a> { + #[inline] + pub(crate) fn bytes(bytes: &'a [u8]) -> Self { + MethodKey { bytes } + } +} + +/// What a class instance's prototype chain answers for a method name. +pub(crate) enum ChainMethod { + /// A data property: its value, and the body its holder's shape names for + /// the slot (a ConstFn lane: every carrier of that ShapeId holds a + /// function object of that body there), if it names one. + Data { + value: u64, + body: Option<&'static crate::closure::JsFunctionInfo>, + /// The object whose shape lists the name. + holder: *const ObjectHeader, + /// The holder's inline slot that holds `value`, when it is one. + slot: Option, + }, + /// `holder` (on the chain, every object before it lacks the name) needs + /// an ordinary [[Get]] with the original receiver: an accessor, or an + /// object whose shape alone does not answer. + Get { holder: *const ObjectHeader }, + /// No object on the chain has the name. + Absent, +} + +/// Prototype chains are finite and acyclic; this bounds a walk over a +/// corrupt one. +const MAX_CHAIN_DEPTH: usize = 128; + +/// Key lists at least this long are searched through the shape's shared key +/// index rather than scanned. +const INDEXED_SCAN_MIN_KEYS: u32 = 24; + +/// The position of `key` among the first `count` keys of `keys`, scanned back +/// to front (#10595: the most-derived duplicate wins): atom identity, then +/// bytes. Allocation-free. +/// +/// # Safety +/// `keys` is null or a live key list. +#[inline] +pub(crate) unsafe fn key_position( + keys: *const ArrayHeader, + count: u32, + key: &MethodKey<'_>, +) -> Option { + if keys.is_null() || count == 0 { + return None; + } + if count >= INDEXED_SCAN_MIN_KEYS { + return own_slot::find_method_slot(keys, count, key.bytes); + } + let (slots, len) = crate::object::keys_array_dense_slots_resolved(keys); + if slots.is_null() { + return crate::object::keys_find_slot_by_bytes(keys, count, key.bytes); + } + let n = (count as usize).min(len); + let want = key.bytes; + let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + for i in (0..n).rev() { + let bits = (*slots.add(i)).to_bits(); + match bits >> 48 { + 0x7FFF => { + let sp = (bits & crate::value::POINTER_MASK) as *const crate::StringHeader; + if !sp.is_null() + && (*sp).byte_len as usize == want.len() + && std::slice::from_raw_parts(crate::string::string_data(sp), want.len()) + == want + { + return Some(i as u32); + } + } + 0x7FF9 => { + if crate::string::js_string_key_bytes(crate::JSValue::from_bits(bits), &mut sso) + == Some(want) + { + return Some(i as u32); + } + } + _ => {} + } + } + None +} + +/// Is `addr` an ordinary object (no element storage the name could index, +/// no exotic read), so that its shape's key list plus its dictionary storage +/// (when it has any) are all its own string-keyed properties? +#[inline] +unsafe fn shape_answers(addr: usize) -> bool { + if !crate::value::addr_class::is_above_handle_band(addr) { + return false; + } + let Some(header) = crate::value::addr_class::try_read_gc_header(addr) else { + return false; + }; + // A function object is a GC_TYPE_CLOSURE cell, never GC_TYPE_OBJECT. + if header.obj_type != crate::gc::GC_TYPE_OBJECT + || header.gc_flags & crate::gc::GC_FLAG_FORWARDED != 0 + || header._reserved & crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO != 0 + { + return false; + } + let obj = addr as *const ObjectHeader; + if (*obj).class_id == NATIVE_MODULE_CLASS_ID { + return false; + } + let meta = (*obj).meta; + meta.is_null() + || ((*meta).elements == 0 + && (*meta).flags & crate::object::OBJECT_META_FLAG_EXOTIC_READ_RECEIVER == 0) +} + +/// The [[Prototype]] of holder `obj` as its link records it: `Ok(Some)` an +/// object, `Ok(None)` null, `Err(())` a link the shape alone does not name. +#[inline] +unsafe fn next_holder(obj: *const ObjectHeader) -> Result, ()> { + use crate::object::shapes::{PROTO_ID_DEFAULT, PROTO_ID_NULL}; + let word = crate::object::shapes::object_prototype_word(obj); + if word != 0 { + let v = crate::JSValue::from_bits(word); + if v.is_pointer() { + return Ok(Some(v.as_pointer::() as *const ObjectHeader)); + } + return if word == crate::value::TAG_NULL { + Ok(None) + } else { + Err(()) + }; + } + match crate::object::shapes::shape_proto_id(crate::object::shapes::object_shape_stamp(obj)) { + Some(PROTO_ID_NULL) => Ok(None), + Some(PROTO_ID_DEFAULT) => { + if obj as usize == crate::array::object_prototype_addr_if_resolved() { + // `%Object.prototype%` is immutable-prototype: null. + return Ok(None); + } + let p = crate::array::object_prototype_addr_if_resolved(); + if p == 0 { + Err(()) + } else { + Ok(Some(p as *const ObjectHeader)) + } + } + _ => Err(()), + } +} + +/// Look `key` up on the chain that starts at `start` (a class instance's +/// prototype): the first holder whose shape lists it answers. Allocation-free, +/// never calls user code. +/// +/// # Safety +/// `start` is a live object. +#[inline(never)] +pub(crate) unsafe fn chain_method(start: *const ObjectHeader, key: &MethodKey<'_>) -> ChainMethod { + let mut holder = start; + for _ in 0..MAX_CHAIN_DEPTH { + if !shape_answers(holder as usize) { + return ChainMethod::Get { holder }; + } + let Some(record) = crate::object::shapes::object_shape_record(holder) else { + return ChainMethod::Get { holder }; + }; + let keys = record.keys() as usize as *const ArrayHeader; + if keys.is_null() { + // No key list: an empty object, or a dictionary-mode one whose + // properties live in its meta record, which the shape does not + // describe. + let meta = (*holder).meta; + if !meta.is_null() && (*meta).dictionary_keys != 0 { + return ChainMethod::Get { holder }; + } + } else if let Some(pos) = key_position(keys, record.logical_key_count(), key) { + // An ordinary object's key list says whether a key is an accessor + // (its slot holds the pair); the descriptor tables hold nothing + // more for it. + if crate::object::key_attrs::key_is_accessor_at(keys, pos) { + return ChainMethod::Get { holder }; + } + // Below the shape's live inline count the key IS that inline + // slot; above it the general read finds the spill position. + let inline = pos < record.live_inline_slot_count(); + let value = if inline { + std::ptr::read( + (holder as *const u8) + .add(std::mem::size_of::() + pos as usize * 8) + as *const u64, + ) + } else { + crate::object::js_object_get_field(holder, pos).bits() + }; + if value == crate::value::TAG_HOLE { + return ChainMethod::Get { holder }; + } + let body = if pos < crate::object::field_rep::REP_SLOTS + && record.special_constfn_mask() & (1 << pos) != 0 + { + record.constfn_info(pos).and_then(|info| { + (info as usize as *const crate::closure::JsFunctionInfo).as_ref() + }) + } else { + None + }; + return ChainMethod::Data { + value, + body, + holder, + slot: inline.then_some(pos), + }; + } + match next_holder(holder) { + Ok(Some(next)) if next != holder => holder = next, + Ok(Some(_)) | Err(()) => return ChainMethod::Get { holder }, + Ok(None) => return ChainMethod::Absent, + } + } + ChainMethod::Get { holder: start } +} + +/// The class whose declared prototype `obj`'s shape names as its +/// [[Prototype]] (a bare CLASS identity, `shapes::class_proto_id`: the class's +/// generic origin for a specialization), or `None`. Only a declared class's +/// prototype is linked that way, and the class's function object keeps that +/// link for the agent's life (slice 2). +#[inline] +unsafe fn shape_named_class(obj: *const ObjectHeader) -> Option { + use crate::object::shapes::{PROTO_ID_CLASS, PROTO_ID_MIXED}; + let pid = + crate::object::shapes::shape_proto_id(crate::object::shapes::object_shape_stamp(obj))?; + (PROTO_ID_CLASS..PROTO_ID_MIXED) + .contains(&pid) + .then_some(pid as u32) +} + +/// The prototype a class instance's chain starts at when it already exists: +/// the receiver's recorded [[Prototype]], else the declared prototype its +/// shape names, read from the class's function object. Null when it is not +/// built yet, or the receiver's link is neither. +/// +/// # Safety +/// `obj` is a live object. +#[inline] +pub(crate) unsafe fn class_instance_prototype(obj: *const ObjectHeader) -> *const ObjectHeader { + let word = crate::object::shapes::object_prototype_word(obj); + if word != 0 { + return word_object(word); + } + match shape_named_class(obj) { + Some(class_id) => crate::object::class_value::class_decl_prototype_link(class_id), + None => std::ptr::null(), + } +} + +/// [`class_instance_prototype`] for a receiver that passed the class receiver +/// guard (an ordinary, non-dictionary class instance whose meta record, if +/// any, records no [[Prototype]]). Such a receiver's shape names its class's +/// declared prototype (`shapes::class_proto_id`): the class's own link, or, +/// for a generic specialization (whose class keeps no link of its own), the +/// generic origin's, which the shape names. +/// +/// # Safety +/// `obj` is a live object that passed [`class_receiver_guard`]. +#[inline] +unsafe fn guarded_class_instance_prototype(obj: *const ObjectHeader) -> *const ObjectHeader { + let own = crate::object::class_value::class_decl_prototype_link((*obj).class_id); + if !own.is_null() { + return own; + } + class_instance_prototype(obj) +} + +#[inline] +fn word_object(word: u64) -> *const ObjectHeader { + let v = crate::JSValue::from_bits(word); + if v.is_pointer() { + v.as_pointer::() as *const ObjectHeader + } else { + std::ptr::null() + } +} + +/// [`class_instance_prototype`], building the class's prototype object first +/// when it does not exist yet (as any `C.prototype` read does). Allocates: the +/// caller roots what it holds. +/// +/// # Safety +/// `obj` is a live object. +#[inline(never)] +pub(crate) unsafe fn class_instance_prototype_built( + obj: *const ObjectHeader, +) -> *const ObjectHeader { + let existing = class_instance_prototype(obj); + if !existing.is_null() { + return existing; + } + if crate::object::shapes::object_prototype_word(obj) != 0 { + return std::ptr::null(); + } + let Some(class_id) = shape_named_class(obj) else { + return std::ptr::null(); + }; + let v = crate::object::class_registry::class_decl_prototype_value(class_id); + let v = crate::JSValue::from_bits(v.to_bits()); + if v.is_pointer() { + v.as_pointer::() as *const ObjectHeader + } else { + std::ptr::null() + } +} + +/// The iterator-helper names (`crate::iterator_helpers::is_iterator_helper_method`), +/// whose call the tower routes by whether the receiver is an iterator, which +/// the receiver predicate does not pin. (The `using` hooks are internal +/// `__perry_` names, never prototype properties.) +#[inline] +fn is_iterator_helper_name(name: &[u8]) -> bool { + matches!( + name, + b"map" + | b"filter" + | b"take" + | b"drop" + | b"flatMap" + | b"toArray" + | b"forEach" + | b"reduce" + | b"some" + | b"every" + | b"find" + ) +} + +/// Names that are not string-keyed properties of a class prototype: private +/// names and the internal aliases of symbol-keyed members +/// ([`call_non_property_member`]). +#[inline] +pub(crate) fn name_is_not_a_prototype_method(name: &[u8]) -> bool { + name.is_empty() || is_non_property_member_name(name) +} + +/// The fast form of a class instance's method call at the top of the tower: +/// an ordinary class instance (the receiver predicate the tower's per-receiver +/// probes reduce to, [`class_receiver_fast_guard`]) whose prototype exists, a +/// chain whose shapes name a DATA property for the name, holding a compiled +/// user function that takes the call's arguments as they are. Anything else +/// returns `None` and the tower runs. +/// +/// # Safety +/// `args_ptr` holds `args_len` values. +#[inline] +pub(super) unsafe fn try_class_holder_fast_dispatch( + object: f64, + key: &MethodKey<'_>, + args_ptr: *const f64, + args_len: usize, + memo: MemoRef, +) -> Option { + let name = key.bytes; + let (obj_addr, _class_id) = class_receiver_guard(object, key)?; + // `constructor` is the class itself, which the tower's call path + // reports as not callable without `new`. + if name_is_not_a_prototype_method(name) || name == b"constructor" { + return None; + } + if is_iterator_helper_name(name) { + return None; + } + let start = guarded_class_instance_prototype(obj_addr as *const ObjectHeader); + if start.is_null() { + return None; + } + let ChainMethod::Data { + value, + body, + holder, + slot, + } = chain_method(start, key) + else { + return None; + }; + let (closure, info) = compiled_user_method(value, body)?; + if let Some(slot) = slot { + // The walk allocated nothing: the objects it read are where it read + // them. + crate::object::method_site::chain_memo::memo_record( + memo, + obj_addr as *const ObjectHeader, + start, + holder, + slot, + body, + name, + ); + } + let _depth_guard = CallMethodDepthGuard::enter("")?; + Some(crate::closure::call_compiled_body_this( + closure, + info, + crate::closure::JsThis::from_f64(object), + args_ptr, + args_len, + )) +} + +/// A repeat of a call [`try_class_holder_fast_dispatch`] answered at a call +/// site with its own chain memo (`method_site::chain_memo`): the memo names +/// the receiver's word, every hop's word and the holder's slot, and the slot +/// holds a compiled function the fast call may enter. `None` otherwise. +/// +/// # Safety +/// `memo` is 0 or names a live memo slot; `args_ptr` holds `args_len` values. +#[inline] +pub(super) unsafe fn try_chain_memo_dispatch( + object: f64, + memo: MemoRef, + name: &[u8], + args_ptr: *const f64, + args_len: usize, +) -> Option { + let (value, body) = + crate::object::method_site::chain_memo::memo_lookup_value(memo, object, name)?; + let (closure, info) = compiled_user_method(value, body)?; + let _depth_guard = CallMethodDepthGuard::enter("")?; + Some(crate::closure::call_compiled_body_this( + closure, + info, + crate::closure::JsThis::from_f64(object), + args_ptr, + args_len, + )) +} + +/// `value`, and its body, when it is a compiled function the fast call may +/// enter with the receiver as `this` and the arguments as given: a function +/// object with a compiled body (`FN_COMPILED_BODY`: user code, never a +/// borrowed built-in), no rest or `arguments` bundling (which allocates while +/// the receiver is an unrooted local), and no captured `this` to rebind. When +/// the holder's shape names the slot's body (`body`), the slot is a function +/// object of it by that shape's fact, so the cell is not re-validated. +#[inline] +unsafe fn compiled_user_method( + value: u64, + body: Option<&'static crate::closure::JsFunctionInfo>, +) -> Option<( + *const crate::closure::ClosureHeader, + &'static crate::closure::JsFunctionInfo, +)> { + if value & !crate::value::POINTER_MASK != crate::value::POINTER_TAG { + return None; + } + let addr = (value & crate::value::POINTER_MASK) as usize; + let info = match body { + Some(info) => info, + None => { + if !crate::closure::is_closure_ptr(addr) { + return None; + } + (*(addr as *const crate::closure::ClosureHeader)) + .info + .as_ref()? + } + }; + if info.flags & crate::codegen_abi::FN_COMPILED_BODY == 0 + || crate::closure::info_rest(info).is_some() + { + return None; + } + let closure = addr as *const crate::closure::ClosureHeader; + let raw_count = (*closure).capture_count; + if raw_count & crate::closure::CAPTURES_THIS_FLAG != 0 + && raw_count & crate::closure::NO_THIS_REBIND_FLAG == 0 + && !crate::closure::closure_is_arrow(closure) + { + return None; + } + Some((closure, info)) +} + +/// Is `value` a user function rather than a realm built-in (whose native arm +/// the tower keeps)? A compiled body is user code by construction; anything +/// else asks the borrowed-builtin classifier. +#[inline] +fn is_user_function_value(value: u64, name: &[u8]) -> bool { + if value & !crate::value::POINTER_MASK == crate::value::POINTER_TAG { + let addr = (value & crate::value::POINTER_MASK) as usize; + if crate::closure::is_closure_ptr(addr) { + // SAFETY: a proven, live closure cell; a non-null info is static. + if let Some(info) = unsafe { + (*(addr as *const crate::closure::ClosureHeader)) + .info + .as_ref() + } { + if info.flags & crate::codegen_abi::FN_COMPILED_BODY != 0 { + return true; + } + } + } + } + std::str::from_utf8(name) + .is_ok_and(|name| crate::array::value_is_own_user_method(f64::from_bits(value), name)) +} + +/// The receiver predicate of the fast call (see [`class_receiver_fast_guard`]), +/// with the own-key absence decided by the receiver's key list. +#[inline] +unsafe fn class_receiver_guard(object: f64, key: &MethodKey<'_>) -> Option<(usize, u32)> { + super::class_receiver_fast_guard(object, key.bytes) +} + +/// Call `value` (a property value the chain answered) as `recv`'s method, +/// exactly as the tower calls an inherited value: a closure that keeps `this` +/// in a capture is re-bound to the receiver first. +/// +/// # Safety +/// `args_ptr` holds `args_len` values; `recv` is rooted by the caller. +#[inline(never)] +pub(crate) unsafe fn call_chain_value( + value: u64, + recv: &crate::gc::RuntimeHandle, + args_ptr: *const f64, + args_len: usize, +) -> f64 { + let bound = crate::closure::clone_closure_rebind_this(value, recv.get_nanbox_f64()); + crate::closure::native_call_value_this( + f64::from_bits(bound), + crate::closure::JsThis::from_f64(recv.get_nanbox_f64()), + args_ptr, + args_len, + ) +} + +/// The value a class instance's chain gives `name`, by shapes where they +/// answer and by an ordinary [[Get]] (with the instance as the receiver an +/// accessor sees) where they do not. `None` when the chain has no such +/// property, a data property that is a built-in rather than a user function, +/// or no chain could be found. Builds the class's prototype object +/// when it does not exist yet; allocates and may run a getter. +/// +/// # Safety +/// `recv` roots a live object receiver. +#[inline(never)] +pub(crate) unsafe fn class_instance_method_value( + recv: &crate::gc::RuntimeHandle, + key: &MethodKey<'_>, +) -> Option { + let obj = crate::value::js_nanbox_get_pointer(recv.get_nanbox_f64()) as *const ObjectHeader; + let start = class_instance_prototype_built(obj); + if start.is_null() { + return None; + } + let holder = match chain_method(start, key) { + // A built-in a prototype inherits from the realm (`toString` on + // `%Object.prototype%`, a native base's methods) keeps the tower's + // own native arms, exactly as before the chain answered. + ChainMethod::Data { value, .. } => { + // A no-op-backed intrinsic (`Response.prototype.text` on a native + // base) re-dispatches its call by name on the receiver (#11700): + // the tower's native arms answer it instead. + let name = std::str::from_utf8(key.bytes).ok()?; + if super::is_self_redispatching_proto_method(f64::from_bits(value), name) { + return None; + } + return is_user_function_value(value, key.bytes).then_some(value); + } + ChainMethod::Absent => return None, + ChainMethod::Get { holder } => holder, + }; + let scope = crate::gc::RuntimeHandleScope::new(); + let holder_h = scope.root_raw_mut_ptr(holder as *mut ObjectHeader); + let method_key = scope.root_string_ptr(crate::string::js_string_from_bytes( + key.bytes.as_ptr(), + key.bytes.len() as u32, + )); + let receiver_f64 = recv.get_nanbox_f64(); + let override_scope = crate::gc::RuntimeHandleScope::new(); + let prev_override = super::super::field_get_set::accessor_receiver_override_begin(receiver_f64) + .map(|value| override_scope.root_nanbox_f64(value)); + let value = holder_h.with_mut_ptr(|holder: *mut ObjectHeader| { + method_key.with_const_ptr::(|key| { + js_object_get_field_by_name(holder as *const _, key) + }) + }); + super::super::field_get_set::accessor_receiver_override_end( + prev_override.map(|handle| handle.get_nanbox_f64()), + ); + (!value.is_undefined() && !value.is_null()).then_some(value.bits()) +} + +/// A class member that is not a string-keyed property of its prototype: a +/// private method (`#m`, reached through its brand) or a symbol-keyed method +/// under its internal dispatch alias (`@@iterator`, `__perry_dispose__`, ...). +/// These live with the class's private and symbol members, not on the +/// prototype's key list, so the shape walk cannot name them; their own lanes +/// (brands, symbol members) retire them. Ordinary names return `None`. +/// +/// # Safety +/// `args_ptr` holds `args_len` values. +#[inline(never)] +pub(crate) unsafe fn call_non_property_member( + receiver: f64, + class_id: u32, + name: &str, + args_ptr: *const f64, + args_len: usize, +) -> Option { + if class_id == 0 || !is_non_property_member_name(name.as_bytes()) { + return None; + } + let (func_ptr, param_count, has_synthetic_arguments, has_rest) = + crate::object::class_registry::lookup_class_method_in_chain(class_id, name)?; + Some(crate::object::class_registry::call_vtable_method_value( + func_ptr, + receiver, + args_ptr, + args_len, + param_count, + has_synthetic_arguments, + has_rest, + None, + )) +} + +/// A private name or a symbol member's internal dispatch alias. +#[inline] +pub(crate) fn is_non_property_member_name(name: &[u8]) -> bool { + !name.is_empty() + && (name[0] == b'#' || name.starts_with(b"__perry_") || name.starts_with(b"@@")) +} + +/// The dispatch tower's arm for a class instance (`class_id` non-zero): the +/// property its prototype chain's shapes name, called with the instance as +/// `this` (the class's prototype object built first if no read has built it +/// yet); else a member that is not a prototype property. `None` when neither +/// answers. Out of line so the tower's own frame stays small. +/// +/// # Safety +/// `recv` roots a live object receiver; `args_ptr` holds `args_len` values, +/// which `arg_handles` roots. +#[inline(never)] +pub(crate) unsafe fn call_class_instance_member( + recv: &crate::gc::RuntimeHandle, + arg_handles: &[crate::gc::RuntimeHandle], + class_id: u32, + method_name: &str, + args_ptr: *const f64, + args_len: usize, +) -> Option { + if class_id != 0 && !name_is_not_a_prototype_method(method_name.as_bytes()) { + let key = MethodKey::bytes(method_name.as_bytes()); + let value = class_instance_method_value(recv, &key)?; + let args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(arg_handles); + return Some(call_chain_value(value, recv, args.as_ptr(), args.len())); + } + call_non_property_member( + recv.get_nanbox_f64(), + class_id, + method_name, + args_ptr, + args_len, + ) +} + +/// The tower's arm for a registered class id used as a receiver: the method +/// `C.prototype` holds for the name, called with an undefined `this`. +/// `None` when the prototype has no such user method. +/// +/// # Safety +/// `arg_handles` roots the call's arguments. +#[inline(never)] +pub(crate) unsafe fn call_class_ref_method( + class_id: u32, + method_name: &str, + arg_handles: &[crate::gc::RuntimeHandle], +) -> Option { + if name_is_not_a_prototype_method(method_name.as_bytes()) { + return None; + } + let proto = crate::object::class_registry::class_decl_prototype_value(class_id); + let proto = crate::JSValue::from_bits(proto.to_bits()); + if !proto.is_pointer() { + return None; + } + let key = MethodKey::bytes(method_name.as_bytes()); + let ChainMethod::Data { value, .. } = chain_method(proto.as_pointer::(), &key) + else { + return None; + }; + if !crate::array::value_is_own_user_method(f64::from_bits(value), method_name) { + return None; + } + let args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(arg_handles); + Some(crate::closure::native_call_value_this( + f64::from_bits(value), + crate::closure::JsThis::from_f64(f64::from_bits(crate::value::TAG_UNDEFINED)), + args.as_ptr(), + args.len(), + )) +} diff --git a/crates/perry-runtime/src/object/native_call_method/collection_methods.rs b/crates/perry-runtime/src/object/native_call_method/collection_methods.rs index 4f702d20a5..42a7791c66 100644 --- a/crates/perry-runtime/src/object/native_call_method/collection_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/collection_methods.rs @@ -465,64 +465,33 @@ pub(super) unsafe fn dispatch_raw_pointer( } } - // Vtable lookup — fast path via per-callsite IC + // A class instance's method: the property its prototype chain's + // shapes name (`class_holder`), as for a NaN-boxed receiver. let class_id = (*obj).class_id; if class_id != 0 { - if let Some((func_ptr, param_count, has_synthetic_arguments, has_rest)) = - vtable_ic_lookup(class_id, method_name_ptr as usize, method_name.as_bytes()) - { - let this_i64 = raw_bits as i64; - return Some(call_vtable_method( - func_ptr, - this_i64, - args_ptr, - args_len, - param_count, - has_synthetic_arguments, - has_rest, - )); - } - if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { - if let Some(ref reg) = *registry { - // Refs #420: parent-chain walk (mirror of the path - // above for raw pointer instances). - let mut cur_cid = class_id; - let mut depth = 0u32; - while depth < 32 { - if let Some(vtable) = reg.get(&cur_cid) { - if let Some(entry) = vtable.methods.get(method_name) { - vtable_ic_insert( - class_id, - method_name_ptr as usize, - method_name.as_bytes(), - entry.func_ptr, - entry.param_count, - entry.has_synthetic_arguments, - entry.has_rest, - ); - let this_i64 = raw_bits as i64; - return Some(call_vtable_method( - entry.func_ptr, - this_i64, - args_ptr, - args_len, - entry.param_count, - entry.has_synthetic_arguments, - entry.has_rest, - )); - } - } - match crate::object::class_registry::instance_chain_parent_class_id( - cur_cid, - ) { - Some(pid) if pid != 0 => { - cur_cid = pid; - depth += 1; - } - _ => break, - } - } + let scope = crate::gc::RuntimeHandleScope::new(); + let recv = scope.root_nanbox_f64(reboxed); + if !super::class_holder::name_is_not_a_prototype_method(method_name.as_bytes()) { + let key = super::class_holder::MethodKey::bytes(method_name.as_bytes()); + if let Some(value) = + super::class_holder::class_instance_method_value(&recv, &key) + { + let args = refreshed_args(); + return Some(super::class_holder::call_chain_value( + value, + &recv, + args.as_ptr(), + args.len(), + )); } + } else if let Some(result) = super::class_holder::call_non_property_member( + reboxed, + class_id, + method_name, + args_ptr, + args_len, + ) { + return Some(result); } } } diff --git a/crates/perry-runtime/src/object/native_call_method/direct_site.rs b/crates/perry-runtime/src/object/native_call_method/direct_site.rs index 2c3e528806..e91a71bd15 100644 --- a/crates/perry-runtime/src/object/native_call_method/direct_site.rs +++ b/crates/perry-runtime/src/object/native_call_method/direct_site.rs @@ -40,7 +40,7 @@ //! The prime never allocates, so the receiver read at entry is the receiver //! the dispatch then sees. -use super::class_vtable_receiver_guard; +use super::class_receiver_guard; /// Store `object`'s receiver word in `site` when its shape proves that an /// own-property lookup of `method` finds nothing, and (when @@ -57,14 +57,9 @@ unsafe fn learn_absent_method_word( if site.is_null() || method.is_empty() { return; } - // Names whose dispatch depends on per-object state the key list does not - // pin (symbol-keyed disposal hooks, iterator helpers). let Ok(name) = std::str::from_utf8(method) else { return; }; - if super::method_name_is_fast_dispatch_ineligible(name) { - return; - } // A word is only consulted after the prototype guard bytes pass, so a // site whose name is retired would learn for nothing on every miss. if crate::object::class_registry::class_prototype_fast_guard_invalidated_for_method( @@ -72,11 +67,16 @@ unsafe fn learn_absent_method_word( ) { return; } + // Names whose dispatch depends on per-object state the key list does not + // pin (symbol-keyed disposal hooks, iterator helpers). + if super::method_name_is_fast_dispatch_ineligible(name) { + return; + } // The receiver predicate the tower's class fast path uses — an ordinary // heap instance of a user class, not a dictionary, no own key equal to // `method`, no recorded prototype — with a metadata record accepted only // for overflow storage. - let Some((addr, class_id)) = class_vtable_receiver_guard::(object, method) else { + let Some((addr, class_id)) = class_receiver_guard::(object, method) else { return; }; if expected_class_id != 0 && class_id != expected_class_id { @@ -94,7 +94,14 @@ unsafe fn learn_absent_method_word( /// The miss edge of a compiled class-method site: learn the receiver's word /// for `expected_class_id`, then dispatch exactly as -/// [`super::js_native_call_method_by_id`] does. +/// [`super::js_native_call_method_by_id`] does, with the site's chain memo +/// slot (`method_site::chain_memo`), the word that follows the learned one +/// in the site's record. A miss edge the site takes on every call (its +/// prototype guard bytes are set) answers a repeat from the memo. +/// +/// `site` is the record's address, tagged with bit 0 when the compiled code +/// says no word could be consulted (its prototype guard bytes are set): then +/// nothing is learned. A null `site` dispatches only. #[no_mangle] pub unsafe extern "C-unwind" fn js_native_call_method_by_id_learn( object: f64, @@ -107,18 +114,41 @@ pub unsafe extern "C-unwind" fn js_native_call_method_by_id_learn( if method_id == 0 { return f64::from_bits(crate::value::TAG_UNDEFINED); } - // A null `site` is the compiled miss edge saying no word could be - // consulted (its prototype guard bytes are set): dispatch only. - if !site.is_null() && expected_class_id != 0 { - let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - if let Some(name_ref) = - crate::string::perry_string_ref_from_dispatch_id(method_id, &mut scratch) - { - let method = std::slice::from_raw_parts(name_ref.ptr, name_ref.len); - learn_absent_method_word(object, method, expected_class_id, site); - } + let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + let Some(name_ref) = crate::string::perry_string_ref_from_dispatch_id(method_id, &mut scratch) + else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + let learn = site as usize & 1 == 0; + let record = (site as usize & !1) as *mut u64; + let memo = if record.is_null() { + 0 + } else { + crate::object::method_site::chain_memo::memo_ref_slot( + record.add(1) as *mut crate::object::method_site::chain_memo::ChainMemoSlot, + false, + ) + }; + let method = std::slice::from_raw_parts(name_ref.ptr, name_ref.len); + // A repeat the site's memo answers: no arm took it, so there is nothing + // to learn from it either. + if let Some(result) = + super::class_holder::try_chain_memo_dispatch(object, memo, method, args_ptr, args_len) + { + return result; + } + if learn && !record.is_null() && expected_class_id != 0 { + learn_absent_method_word(object, method, expected_class_id, record); } - super::js_native_call_method_by_id(object, method_id, args_ptr, args_len) + super::memo_entries::memo_call( + object, + name_ref.ptr as *const i8, + name_ref.len, + args_ptr, + args_len, + memo, + true, + ) } /// The miss edge of a compiled own-method-override probe (#620): answer diff --git a/crates/perry-runtime/src/object/native_call_method/handle_methods.rs b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs index 8e1a97be8a..4a876d4fa4 100644 --- a/crates/perry-runtime/src/object/native_call_method/handle_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs @@ -992,235 +992,41 @@ pub(super) unsafe fn dispatch_handle( } } - // Vtable lookup for class instances — fast path via per-callsite IC + // A class instance's method: the property its prototype chain's + // shapes name (`class_holder`), the class's prototype object built + // first if no read has built it yet. A runtime assignment + // (`C.prototype.m = f`) or delete is a property write on that + // object, so the chain sees it like any other key. let class_id = (*obj).class_id; if class_id != 0 { - if let Some((func_ptr, param_count, has_synthetic_arguments, has_rest)) = - vtable_ic_lookup(class_id, method_name_ptr as usize, method_name.as_bytes()) - { - let this_i64 = jsval.as_pointer::() as i64; - return Some(call_vtable_method( - func_ptr, - this_i64, - args_ptr, - args_len, - param_count, - has_synthetic_arguments, - has_rest, - )); - } - // Refs #420: walk the parent chain via the class registry. Per - // JS spec, `subInstance.method()` for a method defined on a - // parent dispatches to the parent's implementation — drizzle's - // `serial("id").primaryKey()` where primaryKey is on - // ColumnBuilder (grandparent) but the receiver is a - // PgSerialBuilder (grandchild). The codegen-side dispatch tower - // in `lower_call.rs` only registers classes the importing module - // knows about; for not-by-name-imported subclasses (return - // values of imported functions) we depend on this runtime walk. - // - // DEADLOCK SAFETY: resolve the target under the registry READ - // lock, then DROP the lock before invoking the method body. - // A user method body can lazily init a module (function-local - // `require()` — Next.js `getServerImpl()` → `require('./next- - // server')`) whose top-level `class` declarations call - // `js_register_class_method` → a registry WRITE lock. std - // `RwLock` is not re-entrant, so holding the read guard across - // the call deadlocked the (single) main thread. - enum ResolvedMethod { - Vtable { - func_ptr: usize, - param_count: u32, - has_synthetic_arguments: bool, - has_rest: bool, - this_i64: i64, - }, - // #711 part 2 / #321: a method that is an own-property of a - // registered prototype object (`Function.prototype = X`, - // effect's `EffectPrototype.pipe`). - ProtoClosure { - field_bits: u64, - }, - } - let mut resolved_method: Option = None; - // Prototype assignments/deletes for this method are rare. - // Until its scoped guard is retired, the producer-registered - // vtable is authoritative and the per-class side tables cannot - // contain an override for this name. - let prototype_mutated = class_prototype_fast_guard_invalidated_for_method( - class_prototype_method_guard_slot(method_name), - ); - if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { - if let Some(ref reg) = *registry { - let mut cur_cid = class_id; - let mut depth = 0u32; - while depth < 32 { - let deleted = - prototype_mutated && class_proto_key_deleted(cur_cid, method_name); - // A runtime assignment is an own property of this - // exact prototype and replaces the declared vtable - // entry. Resolve it first; deletion hides both. - if prototype_mutated && !deleted { - if let Some(method_value) = - lookup_own_prototype_method(cur_cid, method_name) - { - resolved_method = Some(ResolvedMethod::ProtoClosure { - field_bits: method_value.to_bits(), - }); - break; - } - } - if !deleted { - if let Some(vtable) = reg.get(&cur_cid) { - if let Some(entry) = vtable.methods.get(method_name) { - vtable_ic_insert( - class_id, - method_name_ptr as usize, - method_name.as_bytes(), - entry.func_ptr, - entry.param_count, - entry.has_synthetic_arguments, - entry.has_rest, - ); - // #7769: this walk — not the tail vtable - // arm of `js_native_call_method` — is where - // an INHERITED method resolves, and - // inherited methods are the common case in - // any real hierarchy (`class Square extends - // Rect` calling `Rect`'s `area`). Recording - // the outcome here is what lets the - // top-of-tower fast path serve them; the - // helper re-checks the receiver-shape - // predicate before storing anything. - super::note_class_vtable_resolution( - f64::from_bits(jsval.bits()), - method_name, - entry.func_ptr, - entry.param_count, - entry.has_synthetic_arguments, - entry.has_rest, - ); - resolved_method = Some(ResolvedMethod::Vtable { - func_ptr: entry.func_ptr, - param_count: entry.param_count, - has_synthetic_arguments: entry.has_synthetic_arguments, - has_rest: entry.has_rest, - this_i64: jsval.as_pointer::() as i64, - }); - break; - } - } - } - let proto_obj = if deleted { - std::ptr::null_mut() - } else { - // #10890: never a parent class object's statics. - instance_class_prototype_object(cur_cid) - }; - if !proto_obj.is_null() { - let method_key = crate::string::js_string_from_bytes( - method_name.as_ptr(), - method_name.len() as u32, - ); - // An inherited method that resolves to an ACCESSOR - // on the prototype must observe the instance as - // `this` (spec `[[Get]](P, Receiver)`), not the - // prototype object the getter lives on. Stash the - // receiver so `invoke_accessor_getter` rebinds it. - // Without this, a schema library's lazily-installed - // `describe`/`clone` accessors — `Object.define- - // Property(proto, k, { get() { const b = fn.bind(this); - // Object.defineProperty(this, k, { value: b }); return b } })` - // on the shared class prototype — run with - // `this === prototype`, bake `this = prototype` into - // the returned bound method, and cache it on the - // prototype. Every downstream read of an - // instance-only field via `this.` then - // returns `undefined` (`Cannot read properties of - // undefined`) even though the instance has it. - // Mirrors `resolve_proto_chain_field_with_receiver` - // (the winston `get transports()` fix). - let receiver_f64 = f64::from_bits(jsval.bits()); - // #10490: the displaced accessor receiver rides - // through the getter call — root it. - let override_scope = crate::gc::RuntimeHandleScope::new(); - let prev_override = - super::super::field_get_set::accessor_receiver_override_begin( - receiver_f64, - ) - .map(|value| override_scope.root_nanbox_f64(value)); - let field_val = js_object_get_field_by_name( - proto_obj as *const _, - method_key as *const crate::StringHeader, - ); - super::super::field_get_set::accessor_receiver_override_end( - prev_override.map(|handle| handle.get_nanbox_f64()), - ); - if !field_val.is_undefined() && !field_val.is_null() { - resolved_method = Some(ResolvedMethod::ProtoClosure { - field_bits: field_val.bits(), - }); - break; - } - } - match crate::object::class_registry::instance_chain_parent_class_id( - cur_cid, - ) { - Some(pid) if pid != 0 => { - cur_cid = pid; - depth += 1; - } - _ => break, - } - } - } - } - // Registry guard released — safe to run the method body (which - // may register classes via lazy module init). - match resolved_method { - Some(ResolvedMethod::Vtable { - func_ptr, - param_count, - has_synthetic_arguments, - has_rest, - this_i64, - }) => { - return Some(call_vtable_method( - func_ptr, - this_i64, - args_ptr, - args_len, - param_count, - has_synthetic_arguments, - has_rest, + if !super::class_holder::name_is_not_a_prototype_method(method_name.as_bytes()) { + let key = super::class_holder::MethodKey { + bytes: method_name.as_bytes(), + }; + if let Some(value) = + super::class_holder::class_instance_method_value(object_handle, &key) + { + let args = refreshed_args(); + return Some(super::class_holder::call_chain_value( + value, + object_handle, + args.as_ptr(), + args.len(), )); } - Some(ResolvedMethod::ProtoClosure { field_bits }) => { - // #321 (effect Context/Layer/Scope): rebind the closure's - // `this` slot to the receiver — `clone_closure_rebind_this` - // is a no-op for closures that don't capture `this` and for - // non-closure values, so those paths are unaffected. - let bound = crate::closure::clone_closure_rebind_this( - field_bits, - f64::from_bits(jsval.bits()), - ); - let result = crate::closure::native_call_value_this( - f64::from_bits(bound), - crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), - args_ptr, - args_len, - ); - return Some(result); - } - None => {} + } else if let Some(result) = super::class_holder::call_non_property_member( + object_handle.get_nanbox_f64(), + class_id, + method_name, + args_ptr, + args_len, + ) { + return Some(result); } - // #809: independent prototype-object resolution. The walk - // above only runs when `CLASS_VTABLE_REGISTRY` is `Some` — - // a program with no user classes that only does - // `Object.create(objLiteral).method()` has an empty/None - // registry, so `inst.method()` never reached - // `class_prototype_object` and threw ` is not a - // function`. Resolve the method off the synthetic-class-id + // #809: independent prototype-object resolution. A + // synthetic class id (`Object.create(objLiteral)`, an ES5 + // constructor) has no declared prototype for the chain + // above to start from. Resolve the method off the synthetic-class-id // prototype chain directly (reuses the same helper as // `js_object_get_field_by_name`), then invoke it with // `this` bound to the receiver. diff --git a/crates/perry-runtime/src/object/native_call_method/memo_entries.rs b/crates/perry-runtime/src/object/native_call_method/memo_entries.rs new file mode 100644 index 0000000000..aa8afd7303 --- /dev/null +++ b/crates/perry-runtime/src/object/native_call_method/memo_entries.rs @@ -0,0 +1,71 @@ +//! The computed-key method call's entries for a site that owns a chain memo +//! slot (`method_site::chain_memo`): `obj[k](...)` where codegen knows the +//! key is a string (`str_key`) or holds any value (`value`). Each forwards to +//! the dispatch its memo-less sibling runs, with the site's memo, keyed by +//! the key's bytes. + +use crate::object::method_site::chain_memo::{memo_ref_slot, ChainMemoSlot, MemoRef}; + +/// [`super::js_native_call_method_str_key`] for a site with a chain memo slot. +/// +/// # Safety +/// As the sibling; `memo` is null or the site's live memo slot. +#[no_mangle] +pub unsafe extern "C-unwind" fn js_native_call_method_str_key_memo( + object: f64, + name_handle: i64, + args_ptr: *const f64, + args_len: usize, + memo: *mut ChainMemoSlot, +) -> f64 { + let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + let Some(name_ref) = + crate::string::perry_string_ref_from_dispatch_id(name_handle, &mut scratch) + else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + memo_call( + object, + name_ref.ptr as *const i8, + name_ref.len, + args_ptr, + args_len, + memo_ref_slot(memo, true), + false, + ) +} + +/// [`super::js_native_call_method_value`] for a site with a chain memo slot. +/// +/// # Safety +/// As the sibling; `memo` is null or the site's live memo slot. +#[no_mangle] +pub unsafe extern "C-unwind" fn js_native_call_method_value_memo( + object: f64, + key: f64, + args_ptr: *const f64, + args_len: usize, + memo: *mut ChainMemoSlot, +) -> f64 { + super::native_call_method_value_memo(object, key, args_ptr, args_len, memo_ref_slot(memo, true)) +} + +/// A by-name call at a site with a chain memo: the dispatch, with the memo +/// (`checked`: the caller has just asked the memo, so a miss there is not +/// asked again). +/// +/// # Safety +/// As [`super::js_native_call_method`]; `memo` is 0 or names a live memo slot. +#[inline] +pub(super) unsafe fn memo_call( + object: f64, + name_ptr: *const i8, + name_len: usize, + args_ptr: *const f64, + args_len: usize, + memo: MemoRef, + checked: bool, +) -> f64 { + let _ = checked; + super::native_call_method_tower(object, name_ptr, name_len, args_ptr, args_len, memo) +} diff --git a/crates/perry-runtime/src/object/native_call_method/vtable_guard_scan_tests.rs b/crates/perry-runtime/src/object/native_call_method/vtable_guard_scan_tests.rs index ce539fac04..e4606a6458 100644 --- a/crates/perry-runtime/src/object/native_call_method/vtable_guard_scan_tests.rs +++ b/crates/perry-runtime/src/object/native_call_method/vtable_guard_scan_tests.rs @@ -1,4 +1,4 @@ -//! #10724: [`class_vtable_fast_guard`]'s own-key shadowing scan reads the +//! #10724: [`class_receiver_fast_guard`]'s own-key shadowing scan reads the //! receiver's keys through the raw dense slots, never through the JS-facing //! element accessor. //! @@ -30,7 +30,7 @@ fn class_instance(class_id: u32, keys: &[&str]) -> f64 { } fn guard(receiver: f64, method: &str) -> Option<(usize, u32)> { - unsafe { class_vtable_fast_guard(receiver, method.as_bytes()) } + unsafe { class_receiver_fast_guard(receiver, method.as_bytes()) } } #[test] diff --git a/docs/src/api/reference.md b/docs/src/api/reference.md index 200f901664..e69de29bb2 100644 --- a/docs/src/api/reference.md +++ b/docs/src/api/reference.md @@ -1,3767 +0,0 @@ -# Supported API Reference - -This page is auto-generated from Perry's compile-time API manifest (`perry-api-manifest::API_MANIFEST`). It is the source of truth for what `perry compile` accepts; references to symbols not listed here produce `R005 UnimplementedApi` (issue #463). Stubs (#464) are flagged ⚠ — they link cleanly but no-op at runtime on the chosen target. - -Total: 2781 entries across 111 modules. - -## Modules - -- [`@lydell/node-pty`](#lydellnode-pty) -- [`@parcel/watcher`](#parcelwatcher) -- [`@parcel/watcher-darwin-arm64`](#parcelwatcher-darwin-arm64) -- [`@parcel/watcher-darwin-x64`](#parcelwatcher-darwin-x64) -- [`@parcel/watcher-linux-arm64-glibc`](#parcelwatcher-linux-arm64-glibc) -- [`@parcel/watcher-linux-arm64-musl`](#parcelwatcher-linux-arm64-musl) -- [`@parcel/watcher-linux-x64-glibc`](#parcelwatcher-linux-x64-glibc) -- [`@parcel/watcher-linux-x64-musl`](#parcelwatcher-linux-x64-musl) -- [`@parcel/watcher-win32-arm64`](#parcelwatcher-win32-arm64) -- [`@parcel/watcher-win32-x64`](#parcelwatcher-win32-x64) -- [`@perryts/pdf`](#perrytspdf) -- [`__disposable__`](#__disposable__) -- [`argon2`](#argon2) -- [`assert`](#assert) -- [`assert/strict`](#assertstrict) -- [`async_hooks`](#async_hooks) -- [`bcrypt`](#bcrypt) -- [`better-sqlite3`](#better-sqlite3) -- [`buffer`](#buffer) -- [`bun`](#bun) -- [`bun-pty`](#bun-pty) -- [`bun:ffi`](#bunffi) -- [`bun:jsc`](#bunjsc) -- [`bun:sqlite`](#bunsqlite) -- [`cheerio`](#cheerio) -- [`child_process`](#child_process) -- [`cluster`](#cluster) -- [`console`](#console) -- [`constants`](#constants) -- [`crypto`](#crypto) -- [`dgram`](#dgram) -- [`diagnostics_channel`](#diagnostics_channel) -- [`dns`](#dns) -- [`dns/promises`](#dnspromises) -- [`domain`](#domain) -- [`ethers`](#ethers) -- [`events`](#events) -- [`fetch`](#fetch) -- [`ffi`](#ffi) -- [`fs`](#fs) -- [`fs/promises`](#fspromises) -- [`http`](#http) -- [`http2`](#http2) -- [`https`](#https) -- [`inspector`](#inspector) -- [`inspector/promises`](#inspectorpromises) -- [`lodash`](#lodash) -- [`module`](#module) -- [`net`](#net) -- [`node-fetch`](#node-fetch) -- [`node-pty`](#node-pty) -- [`nodemailer`](#nodemailer) -- [`os`](#os) -- [`path`](#path) -- [`path/posix`](#pathposix) -- [`path/win32`](#pathwin32) -- [`perf_hooks`](#perf_hooks) -- [`perry`](#perry) -- [`perry/ads`](#perryads) -- [`perry/audio`](#perryaudio) -- [`perry/background`](#perrybackground) -- [`perry/compose`](#perrycompose) -- [`perry/container`](#perrycontainer) -- [`perry/container-compose`](#perrycontainer-compose) -- [`perry/gc`](#perrygc) -- [`perry/i18n`](#perryi18n) -- [`perry/ios`](#perryios) -- [`perry/media`](#perrymedia) -- [`perry/native`](#perrynative) -- [`perry/plugin`](#perryplugin) -- [`perry/system`](#perrysystem) -- [`perry/thread`](#perrythread) -- [`perry/tui`](#perrytui) -- [`perry/ui`](#perryui) -- [`perry/updater`](#perryupdater) -- [`perry/widget`](#perrywidget) -- [`perry/workloads`](#perryworkloads) -- [`perry/yoga`](#perryyoga) -- [`process`](#process) -- [`punycode`](#punycode) -- [`querystring`](#querystring) -- [`readline`](#readline) -- [`readline/promises`](#readlinepromises) -- [`repl`](#repl) -- [`sea`](#sea) -- [`sharp`](#sharp) -- [`sqlite`](#sqlite) -- [`stream`](#stream) -- [`stream/consumers`](#streamconsumers) -- [`stream/promises`](#streampromises) -- [`stream/web`](#streamweb) -- [`streams`](#streams) -- [`string_decoder`](#string_decoder) -- [`sys`](#sys) -- [`test`](#test) -- [`test/reporters`](#testreporters) -- [`timers`](#timers) -- [`timers/promises`](#timerspromises) -- [`tls`](#tls) -- [`tty`](#tty) -- [`typescript`](#typescript) -- [`undici`](#undici) -- [`url`](#url) -- [`util`](#util) -- [`util/types`](#utiltypes) -- [`v8`](#v8) -- [`vm`](#vm) -- [`wasi`](#wasi) -- [`worker_threads`](#worker_threads) -- [`ws`](#ws) -- [`zlib`](#zlib) - ---- - -## `@lydell/node-pty` - -### Methods - -- `spawn` — module - -### Properties - -- `default` - -## `@parcel/watcher` - -### Methods - -- `__nativeEventCount` — module -- `getEventsSince` — module -- `subscribe` — module -- `unsubscribe` — module -- `writeSnapshot` — module - -## `@parcel/watcher-darwin-arm64` - -### Methods - -- `subscribe` — module - -## `@parcel/watcher-darwin-x64` - -### Methods - -- `subscribe` — module - -## `@parcel/watcher-linux-arm64-glibc` - -### Methods - -- `subscribe` — module - -## `@parcel/watcher-linux-arm64-musl` - -### Methods - -- `subscribe` — module - -## `@parcel/watcher-linux-x64-glibc` - -### Methods - -- `subscribe` — module - -## `@parcel/watcher-linux-x64-musl` - -### Methods - -- `subscribe` — module - -## `@parcel/watcher-win32-arm64` - -### Methods - -- `subscribe` — module - -## `@parcel/watcher-win32-x64` - -### Methods - -- `subscribe` — module - -## `@perryts/pdf` - -### Methods - -- `createPdf` — module -- `pdfAddLine` — module -- `pdfAddText` — module -- `pdfNewPage` — module -- `pdfSave` — module - -## `__disposable__` - -### Methods - -- `adopt` — instance -- `defer` — instance -- `dispose` — instance -- `disposeAsync` — instance -- `disposed` — instance -- `move` — instance -- `use` — instance - -## `argon2` - -### Methods - -- `hash` — module -- `verify` — module - -## `assert` - -### Classes - -- `Assert` -- `AssertionError` - -### Methods - -- `deepEqual` — module -- `deepStrictEqual` — module -- `default` — module -- `doesNotMatch` — module -- `doesNotReject` — module -- `doesNotThrow` — module -- `equal` — module -- `fail` — module -- `ifError` — module -- `match` — module -- `notDeepEqual` — module -- `notDeepStrictEqual` — module -- `notEqual` — module -- `notStrictEqual` — module -- `ok` — module -- `partialDeepStrictEqual` — module -- `rejects` — module -- `strict` — module -- `strictEqual` — module -- `throws` — module - -### Properties - -- `strict` - -## `assert/strict` - -### Classes - -- `Assert` -- `AssertionError` - -### Methods - -- `deepEqual` — module -- `deepStrictEqual` — module -- `default` — module -- `doesNotMatch` — module -- `doesNotReject` — module -- `doesNotThrow` — module -- `equal` — module -- `fail` — module -- `ifError` — module -- `match` — module -- `notDeepEqual` — module -- `notDeepStrictEqual` — module -- `notEqual` — module -- `notStrictEqual` — module -- `ok` — module -- `partialDeepStrictEqual` — module -- `rejects` — module -- `strict` — module -- `strictEqual` — module -- `throws` — module - -### Properties - -- `strict` - -## `async_hooks` - -### Classes - -- `AsyncLocalStorage` -- `AsyncResource` - -### Methods - -- `asyncId` — instance *(class: `AsyncResource`)* -- `bind` — module *(class: `AsyncLocalStorage`)* -- `bind` — module *(class: `AsyncResource`)* -- `bind` — instance *(class: `AsyncResource`)* -- `createHook` — module -- `disable` — instance -- `emitDestroy` — instance *(class: `AsyncResource`)* -- `enable` — instance *(class: `AsyncHook`)* -- `enterWith` — instance -- `executionAsyncId` — module -- `executionAsyncResource` — module -- `exit` — instance -- `getStore` — instance -- `run` — instance -- `runInAsyncScope` — instance *(class: `AsyncResource`)* -- `snapshot` — module *(class: `AsyncLocalStorage`)* -- `triggerAsyncId` — module -- `triggerAsyncId` — instance *(class: `AsyncResource`)* - -### Properties - -- `asyncWrapProviders` -- `default` - -## `bcrypt` - -### Methods - -- `compare` — module -- `hash` — module - -## `better-sqlite3` - -### Methods - -- `all` — instance -- `close` — instance -- `columns` — instance -- `default` — module -- `exec` — instance -- `get` — instance -- `iterate` — instance -- `pluck` — instance -- `pragma` — instance -- `prepare` — instance -- `raw` — instance -- `run` — instance -- `transaction` — instance - -## `buffer` - -### Classes - -- `Blob` -- `Buffer` -- `File` - -### Methods - -- `atob` — module -- `btoa` — module -- `isAscii` — module -- `isUtf8` — module -- `resolveObjectURL` — module -- `transcode` — module - -### Properties - -- `INSPECT_MAX_BYTES` -- `constants` -- `kMaxLength` -- `kStringMaxLength` - -## `bun` - -### Classes - -- `Transpiler` - -### Methods - -- `Glob` — module -- `SQL` — module -- `Terminal` — module -- `Transpiler` — module -- `build` — module -- `connect` — module -- `deepEquals` — module -- `file` — module -- `fileURLToPath` — module -- `gc` — module -- `generateHeapSnapshot` — module -- `hash` — module -- `listen` — module -- `pathToFileURL` — module -- `plugin` — module ⚠ **stub** — setup runs synchronously; runtime loader hooks and clearAll are inert (#10100) -- `scan` — instance *(class: `Transpiler`)* -- `scanImports` — instance *(class: `Transpiler`)* -- `serve` — module -- `spawn` — module -- `stringWidth` — module -- `stripANSI` — module -- `transform` — instance *(class: `Transpiler`)* -- `transformSync` — instance *(class: `Transpiler`)* -- `unsupported` — module -- `which` — module -- `wrapAnsi` — module -- `write` — module -- `zstdDecompress` — module -- `zstdDecompressSync` — module - -### Properties - -- `JSONL` -- `TOML` -- `YAML` -- `ant` -- `isStandaloneExecutable` -- `semver` -- `stderr` -- `stdin` -- `stdout` -- `version` - -## `bun-pty` - -### Methods - -- `spawn` — module - -### Properties - -- `default` - -## `bun:ffi` - -### Methods - -- `CFunction` — module -- `CString` — module -- `JSCallback` — module -- `dlopen` — module -- `linkSymbols` — module -- `ptr` — module -- `toArrayBuffer` — module -- `toBuffer` — module -- `viewSource` — module - -### Properties - -- `FFIType` -- `read` -- `suffix` - -## `bun:jsc` - -### Methods - -- `heapStats` — module - -## `bun:sqlite` - -### Classes - -- `Database` -- `Statement` - -### Methods - -- `Database` — module -- `all` — instance *(class: `Statement`)* -- `close` — instance *(class: `Database`)* -- `finalize` — instance *(class: `Statement`)* -- `get` — instance *(class: `Statement`)* -- `loadExtension` — instance *(class: `Database`)* -- `prepare` — instance *(class: `Database`)* -- `query` — instance *(class: `Database`)* -- `run` — instance *(class: `Database`)* -- `run` — instance *(class: `Statement`)* -- `safeIntegers` — instance *(class: `Statement`)* -- `serialize` — instance *(class: `Database`)* -- `transaction` — instance *(class: `Database`)* -- `values` — instance *(class: `Statement`)* - -### Properties - -- `filename` -- `inTransaction` - -## `cheerio` - -### Methods - -- `attr` — instance -- `children` — instance -- `eq` — instance -- `find` — instance -- `first` — instance -- `hasClass` — instance -- `html` — instance -- `last` — instance -- `length` — instance -- `load` — module -- `parent` — instance -- `select` — instance -- `text` — instance - -## `child_process` - -### Classes - -- `ChildProcess` - -### Methods - -- `_forkChild` — module -- `exec` — module -- `execFile` — module -- `execFileSync` — module -- `execSync` — module -- `fork` — module -- `spawn` — module -- `spawnSync` — module - -### Properties - -- `default` - -## `cluster` - -### Classes - -- `Worker` - -### Methods - -- `disconnect` — module -- `fork` — module -- `setupMaster` — module -- `setupPrimary` — module - -### Properties - -- `SCHED_NONE` -- `SCHED_RR` -- `default` -- `isMaster` -- `isPrimary` -- `isWorker` -- `schedulingPolicy` -- `settings` -- `workers` - -## `console` - -### Classes - -- `Console` - -### Methods - -- `assert` — module -- `clear` — module -- `context` — module -- `count` — module -- `countReset` — module -- `createTask` — module -- `debug` — module -- `dir` — module -- `dirxml` — module -- `error` — module -- `group` — module -- `groupCollapsed` — module -- `groupEnd` — module -- `info` — module -- `log` — module -- `profile` — module -- `profileEnd` — module -- `table` — module -- `time` — module -- `timeEnd` — module -- `timeLog` — module -- `timeStamp` — module -- `trace` — module -- `warn` — module - -## `constants` - -### Properties - -- `COPYFILE_EXCL` -- `COPYFILE_FICLONE` -- `COPYFILE_FICLONE_FORCE` -- `DH_CHECK_P_NOT_PRIME` -- `DH_CHECK_P_NOT_SAFE_PRIME` -- `DH_NOT_SUITABLE_GENERATOR` -- `DH_UNABLE_TO_CHECK_GENERATOR` -- `E2BIG` -- `EACCES` -- `EADDRINUSE` -- `EADDRNOTAVAIL` -- `EAFNOSUPPORT` -- `EAGAIN` -- `EALREADY` -- `EBADF` -- `EBADMSG` -- `EBUSY` -- `ECANCELED` -- `ECHILD` -- `ECONNABORTED` -- `ECONNREFUSED` -- `ECONNRESET` -- `EDEADLK` -- `EDESTADDRREQ` -- `EDOM` -- `EDQUOT` -- `EEXIST` -- `EFAULT` -- `EFBIG` -- `EHOSTUNREACH` -- `EIDRM` -- `EILSEQ` -- `EINPROGRESS` -- `EINTR` -- `EINVAL` -- `EIO` -- `EISCONN` -- `EISDIR` -- `ELOOP` -- `EMFILE` -- `EMLINK` -- `EMSGSIZE` -- `EMULTIHOP` -- `ENAMETOOLONG` -- `ENETDOWN` -- `ENETRESET` -- `ENETUNREACH` -- `ENFILE` -- `ENGINE_METHOD_ALL` -- `ENGINE_METHOD_CIPHERS` -- `ENGINE_METHOD_DH` -- `ENGINE_METHOD_DIGESTS` -- `ENGINE_METHOD_DSA` -- `ENGINE_METHOD_EC` -- `ENGINE_METHOD_NONE` -- `ENGINE_METHOD_PKEY_ASN1_METHS` -- `ENGINE_METHOD_PKEY_METHS` -- `ENGINE_METHOD_RAND` -- `ENGINE_METHOD_RSA` -- `ENOBUFS` -- `ENODATA` -- `ENODEV` -- `ENOENT` -- `ENOEXEC` -- `ENOLCK` -- `ENOLINK` -- `ENOMEM` -- `ENOMSG` -- `ENOPROTOOPT` -- `ENOSPC` -- `ENOSR` -- `ENOSTR` -- `ENOSYS` -- `ENOTCONN` -- `ENOTDIR` -- `ENOTEMPTY` -- `ENOTSOCK` -- `ENOTSUP` -- `ENOTTY` -- `ENXIO` -- `EOPNOTSUPP` -- `EOVERFLOW` -- `EPERM` -- `EPIPE` -- `EPROTO` -- `EPROTONOSUPPORT` -- `EPROTOTYPE` -- `ERANGE` -- `EROFS` -- `ESPIPE` -- `ESRCH` -- `ESTALE` -- `ETIME` -- `ETIMEDOUT` -- `ETXTBSY` -- `EWOULDBLOCK` -- `EXDEV` -- `F_OK` -- `OPENSSL_VERSION_NUMBER` -- `O_APPEND` -- `O_CREAT` -- `O_DIRECT` -- `O_DIRECTORY` -- `O_DSYNC` -- `O_EXCL` -- `O_NOATIME` -- `O_NOCTTY` -- `O_NOFOLLOW` -- `O_NONBLOCK` -- `O_RDONLY` -- `O_RDWR` -- `O_SYMLINK` -- `O_SYNC` -- `O_TRUNC` -- `O_WRONLY` -- `POINT_CONVERSION_COMPRESSED` -- `POINT_CONVERSION_HYBRID` -- `POINT_CONVERSION_UNCOMPRESSED` -- `PRIORITY_ABOVE_NORMAL` -- `PRIORITY_BELOW_NORMAL` -- `PRIORITY_HIGH` -- `PRIORITY_HIGHEST` -- `PRIORITY_LOW` -- `PRIORITY_NORMAL` -- `RSA_NO_PADDING` -- `RSA_PKCS1_OAEP_PADDING` -- `RSA_PKCS1_PADDING` -- `RSA_PKCS1_PSS_PADDING` -- `RSA_PSS_SALTLEN_AUTO` -- `RSA_PSS_SALTLEN_DIGEST` -- `RSA_PSS_SALTLEN_MAX_SIGN` -- `RSA_X931_PADDING` -- `RTLD_DEEPBIND` -- `RTLD_GLOBAL` -- `RTLD_LAZY` -- `RTLD_LOCAL` -- `RTLD_NOW` -- `R_OK` -- `SIGABRT` -- `SIGALRM` -- `SIGBUS` -- `SIGCHLD` -- `SIGCONT` -- `SIGFPE` -- `SIGHUP` -- `SIGILL` -- `SIGINFO` -- `SIGINT` -- `SIGIO` -- `SIGIOT` -- `SIGKILL` -- `SIGPIPE` -- `SIGPOLL` -- `SIGPROF` -- `SIGPWR` -- `SIGQUIT` -- `SIGSEGV` -- `SIGSTKFLT` -- `SIGSTOP` -- `SIGSYS` -- `SIGTERM` -- `SIGTRAP` -- `SIGTSTP` -- `SIGTTIN` -- `SIGTTOU` -- `SIGURG` -- `SIGUSR1` -- `SIGUSR2` -- `SIGVTALRM` -- `SIGWINCH` -- `SIGXCPU` -- `SIGXFSZ` -- `SSL_OP_ALL` -- `SSL_OP_ALLOW_NO_DHE_KEX` -- `SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION` -- `SSL_OP_CIPHER_SERVER_PREFERENCE` -- `SSL_OP_CISCO_ANYCONNECT` -- `SSL_OP_COOKIE_EXCHANGE` -- `SSL_OP_CRYPTOPRO_TLSEXT_BUG` -- `SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS` -- `SSL_OP_LEGACY_SERVER_CONNECT` -- `SSL_OP_NO_COMPRESSION` -- `SSL_OP_NO_ENCRYPT_THEN_MAC` -- `SSL_OP_NO_QUERY_MTU` -- `SSL_OP_NO_RENEGOTIATION` -- `SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION` -- `SSL_OP_NO_SSLv2` -- `SSL_OP_NO_SSLv3` -- `SSL_OP_NO_TICKET` -- `SSL_OP_NO_TLSv1` -- `SSL_OP_NO_TLSv1_1` -- `SSL_OP_NO_TLSv1_2` -- `SSL_OP_NO_TLSv1_3` -- `SSL_OP_PRIORITIZE_CHACHA` -- `SSL_OP_TLS_ROLLBACK_BUG` -- `S_IFBLK` -- `S_IFCHR` -- `S_IFDIR` -- `S_IFIFO` -- `S_IFLNK` -- `S_IFMT` -- `S_IFREG` -- `S_IFSOCK` -- `S_IRGRP` -- `S_IROTH` -- `S_IRUSR` -- `S_IRWXG` -- `S_IRWXO` -- `S_IRWXU` -- `S_IWGRP` -- `S_IWOTH` -- `S_IWUSR` -- `S_IXGRP` -- `S_IXOTH` -- `S_IXUSR` -- `TLS1_1_VERSION` -- `TLS1_2_VERSION` -- `TLS1_3_VERSION` -- `TLS1_VERSION` -- `UV_DIRENT_BLOCK` -- `UV_DIRENT_CHAR` -- `UV_DIRENT_DIR` -- `UV_DIRENT_FIFO` -- `UV_DIRENT_FILE` -- `UV_DIRENT_LINK` -- `UV_DIRENT_SOCKET` -- `UV_DIRENT_UNKNOWN` -- `UV_FS_COPYFILE_EXCL` -- `UV_FS_COPYFILE_FICLONE` -- `UV_FS_COPYFILE_FICLONE_FORCE` -- `UV_FS_O_FILEMAP` -- `UV_FS_SYMLINK_DIR` -- `UV_FS_SYMLINK_JUNCTION` -- `W_OK` -- `X_OK` -- `default` -- `defaultCoreCipherList` - -## `crypto` - -### Classes - -- `Cipheriv` -- `Decipheriv` -- `DiffieHellman` -- `DiffieHellmanGroup` -- `ECDH` -- `KeyObject` -- `X509Certificate` - -### Methods - -- `Hash` — module -- `Hmac` — module -- `Sign` — module -- `Verify` — module -- `argon2` — module -- `argon2Sync` — module -- `checkPrime` — module -- `checkPrimeSync` — module -- `createCipheriv` — module -- `createDecipheriv` — module -- `createDiffieHellman` — module -- `createDiffieHellmanGroup` — module -- `createECDH` — module -- `createHash` — module -- `createHmac` — module -- `createPrivateKey` — module -- `createPublicKey` — module -- `createSecretKey` — module -- `createSign` — module -- `createSign` — module -- `createVerify` — module -- `createVerify` — module -- `decapsulate` — module -- `diffieHellman` — module -- `encapsulate` — module -- `generateKey` — module -- `generateKeyPair` — module -- `generateKeyPairSync` — module -- `generateKeyPairSync` — module -- `generateKeySync` — module -- `generatePrime` — module -- `generatePrimeSync` — module -- `getCipherInfo` — module -- `getCiphers` — module -- `getCurves` — module -- `getDiffieHellman` — module -- `getFips` — module -- `getHashes` — module -- `getRandomValues` — module -- `hash` — module -- `hkdf` — module -- `hkdfSync` — module -- `pbkdf2` — module -- `pbkdf2Sync` — module -- `privateDecrypt` — module -- `privateEncrypt` — module -- `publicDecrypt` — module -- `publicEncrypt` — module -- `randomBytes` — module -- `randomFill` — module -- `randomFillSync` — module -- `randomInt` — module -- `randomInt` — module -- `randomUUID` — module -- `scrypt` — module -- `scryptSync` — module -- `secureHeapUsed` — module -- `setFips` — module -- `sign` — module -- `timingSafeEqual` — module -- `verify` — module - -### Properties - -- `Certificate` -- `constants` -- `subtle` -- `webcrypto` - -## `dgram` - -### Classes - -- `Socket` - -### Methods - -- `Socket` — module -- `addListener` — instance *(class: `Socket`)* -- `addMembership` — instance *(class: `Socket`)* -- `addSourceSpecificMembership` — instance *(class: `Socket`)* -- `address` — instance *(class: `Socket`)* -- `bind` — instance *(class: `Socket`)* -- `close` — instance *(class: `Socket`)* -- `connect` — instance *(class: `Socket`)* -- `createSocket` — module -- `disconnect` — instance *(class: `Socket`)* -- `dropMembership` — instance *(class: `Socket`)* -- `dropSourceSpecificMembership` — instance *(class: `Socket`)* -- `emit` — instance *(class: `Socket`)* -- `eventNames` — instance *(class: `Socket`)* -- `getRecvBufferSize` — instance *(class: `Socket`)* -- `getSendBufferSize` — instance *(class: `Socket`)* -- `getSendQueueCount` — instance *(class: `Socket`)* -- `getSendQueueSize` — instance *(class: `Socket`)* -- `listenerCount` — instance *(class: `Socket`)* -- `off` — instance *(class: `Socket`)* -- `on` — instance *(class: `Socket`)* -- `once` — instance *(class: `Socket`)* -- `ref` — instance *(class: `Socket`)* -- `remoteAddress` — instance *(class: `Socket`)* -- `removeListener` — instance *(class: `Socket`)* -- `send` — instance *(class: `Socket`)* -- `sendto` — instance *(class: `Socket`)* -- `setBroadcast` — instance *(class: `Socket`)* -- `setMulticastInterface` — instance *(class: `Socket`)* -- `setMulticastLoopback` — instance *(class: `Socket`)* -- `setMulticastTTL` — instance *(class: `Socket`)* -- `setRecvBufferSize` — instance *(class: `Socket`)* -- `setSendBufferSize` — instance *(class: `Socket`)* -- `setTTL` — instance *(class: `Socket`)* -- `unref` — instance *(class: `Socket`)* - -### Properties - -- `default` - -## `diagnostics_channel` - -### Classes - -- `BoundedChannel` -- `Channel` - -### Methods - -- `boundedChannel` — module -- `channel` — module -- `hasSubscribers` — module -- `subscribe` — module -- `tracingChannel` — module -- `unsubscribe` — module - -### Properties - -- `default` - -## `dns` - -### Classes - -- `Resolver` - -### Methods - -- `Resolver` — module -- `cancel` — instance *(class: `Resolver`)* -- `getDefaultResultOrder` — module -- `getServers` — module -- `getServers` — instance *(class: `Resolver`)* -- `lookup` — module -- `lookupService` — module -- `resolve` — module -- `resolve` — instance *(class: `Resolver`)* -- `resolve4` — module -- `resolve4` — instance *(class: `Resolver`)* -- `resolve6` — module -- `resolve6` — instance *(class: `Resolver`)* -- `resolveAny` — module -- `resolveAny` — instance *(class: `Resolver`)* -- `resolveCaa` — module -- `resolveCaa` — instance *(class: `Resolver`)* -- `resolveCname` — module -- `resolveCname` — instance *(class: `Resolver`)* -- `resolveMx` — module -- `resolveMx` — instance *(class: `Resolver`)* -- `resolveNaptr` — module -- `resolveNaptr` — instance *(class: `Resolver`)* -- `resolveNs` — module -- `resolveNs` — instance *(class: `Resolver`)* -- `resolvePtr` — module -- `resolvePtr` — instance *(class: `Resolver`)* -- `resolveSoa` — module -- `resolveSoa` — instance *(class: `Resolver`)* -- `resolveSrv` — module -- `resolveSrv` — instance *(class: `Resolver`)* -- `resolveTlsa` — module -- `resolveTlsa` — instance *(class: `Resolver`)* -- `resolveTxt` — module -- `resolveTxt` — instance *(class: `Resolver`)* -- `reverse` — module -- `reverse` — instance *(class: `Resolver`)* -- `setDefaultResultOrder` — module -- `setLocalAddress` — instance *(class: `Resolver`)* -- `setServers` — module -- `setServers` — instance *(class: `Resolver`)* - -### Properties - -- `ADDRCONFIG` -- `ADDRCONFIG` -- `ADDRGETNETWORKPARAMS` -- `ADDRGETNETWORKPARAMS` -- `ALL` -- `ALL` -- `BADFAMILY` -- `BADFAMILY` -- `BADFLAGS` -- `BADFLAGS` -- `BADHINTS` -- `BADHINTS` -- `BADNAME` -- `BADNAME` -- `BADQUERY` -- `BADQUERY` -- `BADRESP` -- `BADRESP` -- `BADSTR` -- `BADSTR` -- `CANCELLED` -- `CANCELLED` -- `CONNREFUSED` -- `CONNREFUSED` -- `DESTRUCTION` -- `DESTRUCTION` -- `EOF` -- `EOF` -- `FILE` -- `FILE` -- `FORMERR` -- `FORMERR` -- `LOADIPHLPAPI` -- `LOADIPHLPAPI` -- `NODATA` -- `NODATA` -- `NOMEM` -- `NOMEM` -- `NONAME` -- `NONAME` -- `NOTFOUND` -- `NOTFOUND` -- `NOTIMP` -- `NOTIMP` -- `NOTINITIALIZED` -- `NOTINITIALIZED` -- `REFUSED` -- `REFUSED` -- `SERVFAIL` -- `SERVFAIL` -- `TIMEOUT` -- `TIMEOUT` -- `V4MAPPED` -- `V4MAPPED` -- `default` -- `promises` - -## `dns/promises` - -### Classes - -- `Resolver` - -### Methods - -- `Resolver` — module -- `cancel` — instance *(class: `Resolver`)* -- `getDefaultResultOrder` — module -- `getServers` — module -- `getServers` — instance *(class: `Resolver`)* -- `lookup` — module -- `lookupService` — module -- `resolve` — module -- `resolve` — instance *(class: `Resolver`)* -- `resolve4` — module -- `resolve4` — instance *(class: `Resolver`)* -- `resolve6` — module -- `resolve6` — instance *(class: `Resolver`)* -- `resolveAny` — module -- `resolveAny` — instance *(class: `Resolver`)* -- `resolveCaa` — module -- `resolveCaa` — instance *(class: `Resolver`)* -- `resolveCname` — module -- `resolveCname` — instance *(class: `Resolver`)* -- `resolveMx` — module -- `resolveMx` — instance *(class: `Resolver`)* -- `resolveNaptr` — module -- `resolveNaptr` — instance *(class: `Resolver`)* -- `resolveNs` — module -- `resolveNs` — instance *(class: `Resolver`)* -- `resolvePtr` — module -- `resolvePtr` — instance *(class: `Resolver`)* -- `resolveSoa` — module -- `resolveSoa` — instance *(class: `Resolver`)* -- `resolveSrv` — module -- `resolveSrv` — instance *(class: `Resolver`)* -- `resolveTlsa` — module -- `resolveTlsa` — instance *(class: `Resolver`)* -- `resolveTxt` — module -- `resolveTxt` — instance *(class: `Resolver`)* -- `reverse` — module -- `reverse` — instance *(class: `Resolver`)* -- `setDefaultResultOrder` — module -- `setLocalAddress` — instance *(class: `Resolver`)* -- `setServers` — module -- `setServers` — instance *(class: `Resolver`)* - -### Properties - -- `ADDRGETNETWORKPARAMS` -- `BADFAMILY` -- `BADFLAGS` -- `BADHINTS` -- `BADNAME` -- `BADQUERY` -- `BADRESP` -- `BADSTR` -- `CANCELLED` -- `CONNREFUSED` -- `DESTRUCTION` -- `EOF` -- `FILE` -- `FORMERR` -- `LOADIPHLPAPI` -- `NODATA` -- `NOMEM` -- `NONAME` -- `NOTFOUND` -- `NOTIMP` -- `NOTINITIALIZED` -- `REFUSED` -- `SERVFAIL` -- `TIMEOUT` -- `default` - -## `domain` - -### Classes - -- `Domain` - -### Methods - -- `Domain` — module -- `add` — instance -- `addListener` — instance -- `bind` — instance -- `create` — module -- `createDomain` — module -- `emit` — instance -- `enter` — instance -- `exit` — instance -- `intercept` — instance -- `on` — instance -- `remove` — instance -- `run` — instance - -### Properties - -- `_stack` -- `active` -- `members` - -## `ethers` - -### Methods - -- `createRandom` — module *(class: `Wallet`)* -- `formatEther` — module -- `formatUnits` — module -- `getAddress` — module -- `parseEther` — module -- `parseUnits` — module - -## `events` - -### Classes - -- `EventEmitter` -- `EventEmitterAsyncResource` - -### Methods - -- `EventEmitter` — module -- `EventEmitterAsyncResource` — module -- `addAbortListener` — module -- `addListener` — instance -- `asyncId` — instance *(class: `EventEmitterAsyncResource`)* -- `asyncResource` — instance *(class: `EventEmitterAsyncResource`)* -- `domain` — instance -- `emit` — instance -- `emitDestroy` — instance *(class: `EventEmitterAsyncResource`)* -- `eventNames` — instance -- `getEventListeners` — module -- `getMaxListeners` — instance -- `getMaxListeners` — module -- `init` — module -- `listenerCount` — instance -- `listenerCount` — module -- `listeners` — instance -- `off` — instance -- `on` — instance -- `on` — module -- `once` — instance -- `once` — module -- `prependListener` — instance -- `prependOnceListener` — instance -- `rawListeners` — instance -- `removeAllListeners` — instance -- `removeListener` — instance -- `setMaxListeners` — instance -- `setMaxListeners` — module -- `triggerAsyncId` — instance *(class: `EventEmitterAsyncResource`)* - -### Properties - -- `captureRejectionSymbol` -- `captureRejections` -- `default` -- `defaultMaxListeners` -- `errorMonitor` -- `usingDomains` - -## `fetch` - -### Classes - -- `Blob` -- `FormData` -- `Headers` -- `Request` -- `Response` - -### Methods - -- `default` — module - -## `ffi` - -### Methods - -- `dlopen` — module -- `getRawPointer` — module -- `toArrayBuffer` — module -- `toBuffer` — module -- `toString` — module - -### Properties - -- `suffix` - -## `fs` - -### Classes - -- `Dir` -- `Dirent` -- `FileReadStream` -- `FileWriteStream` -- `ReadStream` -- `Stats` -- `Utf8Stream` -- `WriteStream` - -### Methods - -- `_toUnixTimestamp` — module -- `_toUnixTimestamp` — module -- `access` — module -- `accessSync` — module -- `appendFile` — module -- `appendFileSync` — module -- `chmod` — module -- `chmodSync` — module -- `chown` — module -- `chownSync` — module -- `close` — module -- `closeSync` — module -- `copyFile` — module -- `copyFileSync` — module -- `cp` — module -- `cpSync` — module -- `createReadStream` — module -- `createWriteStream` — module -- `exists` — module -- `existsSync` — module -- `fchmod` — module -- `fchmodSync` — module -- `fchown` — module -- `fchownSync` — module -- `fdatasync` — module -- `fdatasyncSync` — module -- `fstat` — module -- `fstatSync` — module -- `fsync` — module -- `fsyncSync` — module -- `ftruncate` — module -- `ftruncateSync` — module -- `futimes` — module -- `futimesSync` — module -- `glob` — module -- `globSync` — module -- `lchmod` — module -- `lchmodSync` — module -- `lchown` — module -- `lchownSync` — module -- `link` — module -- `linkSync` — module -- `lstat` — module -- `lstatSync` — module -- `lutimes` — module -- `lutimesSync` — module -- `mkdir` — module -- `mkdirSync` — module -- `mkdtemp` — module -- `mkdtempDisposableSync` — module -- `mkdtempSync` — module -- `open` — module -- `openAsBlob` — module -- `openSync` — module -- `opendir` — module -- `opendirSync` — module -- `read` — module -- `readFile` — module -- `readFileSync` — module -- `readSync` — module -- `readdir` — module -- `readdirSync` — module -- `readlink` — module -- `readlinkSync` — module -- `readv` — module -- `readvSync` — module -- `realpath` — module -- `realpathSync` — module -- `rename` — module -- `renameSync` — module -- `rm` — module -- `rmSync` — module -- `rmdir` — module -- `rmdirSync` — module -- `stat` — module -- `statSync` — module -- `statfs` — module -- `statfsSync` — module -- `symlink` — module -- `symlinkSync` — module -- `truncate` — module -- `truncateSync` — module -- `unlink` — module -- `unlinkSync` — module -- `unwatchFile` — module -- `utimes` — module -- `utimesSync` — module -- `watch` — module -- `watchFile` — module -- `write` — module -- `writeFile` — module -- `writeFileSync` — module -- `writeSync` — module -- `writev` — module -- `writevSync` — module - -### Properties - -- `constants` -- `promises` - -## `fs/promises` - -### Methods - -- `access` — module -- `appendFile` — module -- `chmod` — module -- `chown` — module -- `copyFile` — module -- `cp` — module -- `glob` — module -- `lchmod` — module -- `lchown` — module -- `link` — module -- `lstat` — module -- `lutimes` — module -- `mkdir` — module -- `mkdtemp` — module -- `mkdtempDisposable` — module -- `open` — module -- `opendir` — module -- `pull` — instance *(class: `FileHandle`)* -- `pullSync` — instance *(class: `FileHandle`)* -- `readFile` — module -- `readdir` — module -- `readlink` — module -- `realpath` — module -- `rename` — module -- `rm` — module -- `rmdir` — module -- `stat` — module -- `statfs` — module -- `symlink` — module -- `truncate` — module -- `unlink` — module -- `utimes` — module -- `watch` — module -- `writeFile` — module -- `writer` — instance *(class: `FileHandle`)* - -### Properties - -- `constants` -- `default` - -## `http` - -### Classes - -- `Agent` -- `ClientRequest` -- `IncomingMessage` -- `IncomingMessage` -- `OutgoingMessage` -- `OutgoingMessage` -- `Server` -- `Server` -- `ServerResponse` -- `ServerResponse` -- `WebSocket` - -### Methods - -- `Agent` — module -- `Server` — module -- `__get_aborted` — instance *(class: `ClientRequest`)* -- `__get_aborted` — instance *(class: `IncomingMessage`)* -- `__get_complete` — instance *(class: `IncomingMessage`)* -- `__get_connection` — instance *(class: `IncomingMessage`)* -- `__get_connection` — instance *(class: `ClientRequest`)* -- `__get_createConnection` — instance *(class: `Agent`)* -- `__get_createSocket` — instance *(class: `Agent`)* -- `__get_defaultPort` — instance *(class: `Agent`)* -- `__get_destroyed` — instance *(class: `Agent`)* -- `__get_destroyed` — instance *(class: `ClientRequest`)* -- `__get_destroyed` — instance *(class: `IncomingMessage`)* -- `__get_finished` — instance *(class: `ClientRequest`)* -- `__get_freeSockets` — instance *(class: `Agent`)* -- `__get_headers` — instance *(class: `IncomingMessage`)* -- `__get_headersSent` — instance *(class: `ServerResponse`)* -- `__get_headersTimeout` — instance *(class: `HttpServer`)* -- `__get_host` — instance *(class: `ClientRequest`)* -- `__get_httpVersion` — instance *(class: `IncomingMessage`)* -- `__get_httpVersionMajor` — instance *(class: `IncomingMessage`)* -- `__get_httpVersionMinor` — instance *(class: `IncomingMessage`)* -- `__get_keepAlive` — instance *(class: `Agent`)* -- `__get_keepAliveMsecs` — instance *(class: `Agent`)* -- `__get_keepAliveTimeout` — instance *(class: `HttpServer`)* -- `__get_keepAliveTimeoutBuffer` — instance *(class: `HttpServer`)* -- `__get_listening` — instance *(class: `HttpServer`)* -- `__get_maxFreeSockets` — instance *(class: `Agent`)* -- `__get_maxHeadersCount` — instance *(class: `HttpServer`)* -- `__get_maxHeadersCount` — instance *(class: `ClientRequest`)* -- `__get_maxRequestsPerSocket` — instance *(class: `HttpServer`)* -- `__get_maxSockets` — instance *(class: `Agent`)* -- `__get_maxTotalSockets` — instance *(class: `Agent`)* -- `__get_method` — instance *(class: `ClientRequest`)* -- `__get_method` — instance *(class: `IncomingMessage`)* -- `__get_path` — instance *(class: `ClientRequest`)* -- `__get_protocol` — instance *(class: `Agent`)* -- `__get_protocol` — instance *(class: `ClientRequest`)* -- `__get_rawHeaders` — instance *(class: `IncomingMessage`)* -- `__get_req` — instance *(class: `IncomingMessage`)* -- `__get_requestTimeout` — instance *(class: `HttpServer`)* -- `__get_requests` — instance *(class: `Agent`)* -- `__get_reusedSocket` — instance *(class: `ClientRequest`)* -- `__get_socket` — instance *(class: `IncomingMessage`)* -- `__get_socket` — instance *(class: `ClientRequest`)* -- `__get_sockets` — instance *(class: `Agent`)* -- `__get_statusCode` — instance *(class: `IncomingMessage`)* -- `__get_statusCode` — instance *(class: `ServerResponse`)* -- `__get_statusMessage` — instance *(class: `IncomingMessage`)* -- `__get_timeout` — instance *(class: `HttpServer`)* -- `__get_trailers` — instance *(class: `IncomingMessage`)* -- `__get_url` — instance *(class: `IncomingMessage`)* -- `__get_writableEnded` — instance *(class: `ClientRequest`)* -- `__get_writableEnded` — instance *(class: `ServerResponse`)* -- `__get_writableFinished` — instance *(class: `ClientRequest`)* -- `__get_writableFinished` — instance *(class: `ServerResponse`)* -- `__set_createConnection` — instance *(class: `Agent`)* -- `__set_createSocket` — instance *(class: `Agent`)* -- `__set_headersTimeout` — instance *(class: `HttpServer`)* -- `__set_keepAlive` — instance *(class: `Agent`)* -- `__set_keepAliveMsecs` — instance *(class: `Agent`)* -- `__set_keepAliveTimeout` — instance *(class: `HttpServer`)* -- `__set_keepAliveTimeoutBuffer` — instance *(class: `HttpServer`)* -- `__set_maxFreeSockets` — instance *(class: `Agent`)* -- `__set_maxHeadersCount` — instance *(class: `HttpServer`)* -- `__set_maxRequestsPerSocket` — instance *(class: `HttpServer`)* -- `__set_maxSockets` — instance *(class: `Agent`)* -- `__set_maxTotalSockets` — instance *(class: `Agent`)* -- `__set_protocol` — instance *(class: `Agent`)* -- `__set_requestTimeout` — instance *(class: `HttpServer`)* -- `__set_sendDate` — instance *(class: `ServerResponse`)* -- `__set_statusCode` — instance *(class: `ServerResponse`)* -- `__set_statusMessage` — instance *(class: `ServerResponse`)* -- `__set_strictContentLength` — instance *(class: `ServerResponse`)* -- `__set_timeout` — instance *(class: `HttpServer`)* -- `_connectionListener` — module -- `abort` — instance *(class: `ClientRequest`)* -- `addListener` — instance *(class: `HttpServer`)* -- `addListener` — instance *(class: `IncomingMessage`)* -- `addListener` — instance *(class: `ServerResponse`)* -- `addTrailers` — instance *(class: `ServerResponse`)* -- `address` — instance *(class: `HttpServer`)* -- `appendHeader` — instance *(class: `ServerResponse`)* -- `close` — instance *(class: `Agent`)* -- `close` — instance *(class: `HttpServer`)* -- `closeAllConnections` — instance *(class: `HttpServer`)* -- `closeIdleConnections` — instance *(class: `HttpServer`)* -- `complete` — instance *(class: `IncomingMessage`)* -- `connection` — instance *(class: `IncomingMessage`)* -- `cork` — instance *(class: `ClientRequest`)* -- `cork` — instance *(class: `ServerResponse`)* -- `createServer` — module -- `createServer` — module -- `defaultPort` — instance *(class: `Agent`)* -- `destroy` — instance *(class: `Agent`)* -- `destroy` — instance *(class: `IncomingMessage`)* -- `destroy` — instance *(class: `ClientRequest`)* -- `destroyed` — instance *(class: `Agent`)* -- `end` — instance *(class: `ServerResponse`)* -- `flushHeaders` — instance *(class: `ClientRequest`)* -- `flushHeaders` — instance *(class: `ServerResponse`)* -- `freeSockets` — instance *(class: `Agent`)* -- `get` — module -- `getHeader` — instance *(class: `ClientRequest`)* -- `getHeader` — instance *(class: `ServerResponse`)* -- `getHeaderNames` — instance *(class: `ClientRequest`)* -- `getHeaderNames` — instance *(class: `ServerResponse`)* -- `getHeaders` — instance *(class: `ClientRequest`)* -- `getHeaders` — instance *(class: `ServerResponse`)* -- `getName` — instance *(class: `Agent`)* -- `getRawHeaderNames` — instance *(class: `ClientRequest`)* -- `getStatus` — instance *(class: `ServerResponse`)* -- `hasHeader` — instance *(class: `ClientRequest`)* -- `hasHeader` — instance *(class: `ServerResponse`)* -- `headers` — instance *(class: `IncomingMessage`)* -- `headersTimeout` — instance *(class: `HttpServer`)* -- `httpVersion` — instance *(class: `IncomingMessage`)* -- `httpVersionMajor` — instance *(class: `IncomingMessage`)* -- `httpVersionMinor` — instance *(class: `IncomingMessage`)* -- `keepAlive` — instance *(class: `Agent`)* -- `keepAliveMsecs` — instance *(class: `Agent`)* -- `keepAliveTimeout` — instance *(class: `HttpServer`)* -- `keepAliveTimeoutBuffer` — instance *(class: `HttpServer`)* -- `keepSocketAlive` — instance *(class: `Agent`)* ⚠ **stub** — reqwest owns the keep-alive pool; per-socket hooks are no-ops, warns once (#4917) -- `listen` — instance *(class: `HttpServer`)* -- `listenerCount` — instance *(class: `ClientRequest`)* -- `listening` — instance *(class: `HttpServer`)* -- `maxFreeSockets` — instance *(class: `Agent`)* -- `maxHeadersCount` — instance *(class: `HttpServer`)* -- `maxRequestsPerSocket` — instance *(class: `HttpServer`)* -- `maxSockets` — instance *(class: `Agent`)* -- `maxTotalSockets` — instance *(class: `Agent`)* -- `method` — instance *(class: `IncomingMessage`)* -- `on` — instance *(class: `HttpServer`)* -- `on` — instance *(class: `IncomingMessage`)* -- `on` — instance *(class: `ServerResponse`)* -- `once` — instance *(class: `IncomingMessage`)* -- `once` — instance *(class: `ClientRequest`)* -- `pause` — instance *(class: `IncomingMessage`)* -- `protocol` — instance *(class: `Agent`)* -- `rawHeaders` — instance *(class: `IncomingMessage`)* -- `read` — instance *(class: `IncomingMessage`)* -- `ref` — instance *(class: `HttpServer`)* -- `removeHeader` — instance *(class: `ClientRequest`)* -- `removeHeader` — instance *(class: `ServerResponse`)* -- `req` — instance *(class: `IncomingMessage`)* -- `request` — module -- `requestTimeout` — instance *(class: `HttpServer`)* -- `requests` — instance *(class: `Agent`)* -- `resume` — instance *(class: `IncomingMessage`)* -- `reuseSocket` — instance *(class: `Agent`)* ⚠ **stub** — reqwest owns the keep-alive pool; per-socket hooks are no-ops, warns once (#4917) -- `setEncoding` — instance *(class: `IncomingMessage`)* -- `setGlobalProxyFromEnv` — module -- `setHeader` — instance *(class: `ClientRequest`)* -- `setHeader` — instance *(class: `ServerResponse`)* -- `setHeaders` — instance *(class: `ServerResponse`)* -- `setMaxIdleHTTPParsers` — module -- `setNoDelay` — instance *(class: `ClientRequest`)* -- `setSocketKeepAlive` — instance *(class: `ClientRequest`)* -- `setStatus` — instance *(class: `ServerResponse`)* -- `setTimeout` — instance *(class: `HttpServer`)* -- `setTimeout` — instance *(class: `IncomingMessage`)* -- `setTimeout` — instance *(class: `ClientRequest`)* -- `setTimeout` — instance *(class: `ServerResponse`)* -- `socket` — instance *(class: `IncomingMessage`)* -- `sockets` — instance *(class: `Agent`)* -- `statusCode` — instance *(class: `IncomingMessage`)* -- `statusMessage` — instance *(class: `IncomingMessage`)* -- `timeout` — instance *(class: `HttpServer`)* -- `trailers` — instance *(class: `IncomingMessage`)* -- `uncork` — instance *(class: `ClientRequest`)* -- `uncork` — instance *(class: `ServerResponse`)* -- `unref` — instance *(class: `HttpServer`)* -- `url` — instance *(class: `IncomingMessage`)* -- `validateHeaderName` — module -- `validateHeaderValue` — module -- `write` — instance *(class: `ServerResponse`)* -- `writeContinue` — instance *(class: `ServerResponse`)* -- `writeEarlyHints` — instance *(class: `ServerResponse`)* -- `writeHead` — instance *(class: `ServerResponse`)* -- `writeProcessing` — instance *(class: `ServerResponse`)* - -### Properties - -- `METHODS` -- `STATUS_CODES` -- `globalAgent` -- `kConnectionsCheckingInterval` -- `maxHeaderSize` - -## `http2` - -### Classes - -- `Http2ServerRequest` -- `Http2ServerResponse` - -### Methods - -- `connect` — module -- `createSecureServer` — module -- `createServer` — module -- `getDefaultSettings` — module -- `getPackedSettings` — module -- `getUnpackedSettings` — module -- `performServerHandshake` — module - -### Properties - -- `constants` -- `default` -- `sensitiveHeaders` - -## `https` - -### Classes - -- `Agent` -- `Server` -- `Server` - -### Methods - -- `Agent` — module -- `Server` — module -- `__get_headersTimeout` — instance *(class: `HttpsServer`)* -- `__get_keepAliveTimeout` — instance *(class: `HttpsServer`)* -- `__get_keepAliveTimeoutBuffer` — instance *(class: `HttpsServer`)* -- `__get_listening` — instance *(class: `HttpsServer`)* -- `__get_maxHeadersCount` — instance *(class: `HttpsServer`)* -- `__get_maxRequestsPerSocket` — instance *(class: `HttpsServer`)* -- `__get_requestTimeout` — instance *(class: `HttpsServer`)* -- `__get_timeout` — instance *(class: `HttpsServer`)* -- `__set_headersTimeout` — instance *(class: `HttpsServer`)* -- `__set_keepAliveTimeout` — instance *(class: `HttpsServer`)* -- `__set_keepAliveTimeoutBuffer` — instance *(class: `HttpsServer`)* -- `__set_maxHeadersCount` — instance *(class: `HttpsServer`)* -- `__set_maxRequestsPerSocket` — instance *(class: `HttpsServer`)* -- `__set_requestTimeout` — instance *(class: `HttpsServer`)* -- `__set_timeout` — instance *(class: `HttpsServer`)* -- `addListener` — instance *(class: `HttpsServer`)* -- `address` — instance *(class: `HttpsServer`)* -- `close` — instance *(class: `HttpsServer`)* -- `closeAllConnections` — instance *(class: `HttpsServer`)* -- `closeIdleConnections` — instance *(class: `HttpsServer`)* -- `createServer` — module -- `createServer` — module -- `get` — module -- `headersTimeout` — instance *(class: `HttpsServer`)* -- `keepAliveTimeout` — instance *(class: `HttpsServer`)* -- `keepAliveTimeoutBuffer` — instance *(class: `HttpsServer`)* -- `listen` — instance *(class: `HttpsServer`)* -- `listening` — instance *(class: `HttpsServer`)* -- `maxHeadersCount` — instance *(class: `HttpsServer`)* -- `maxRequestsPerSocket` — instance *(class: `HttpsServer`)* -- `on` — instance *(class: `HttpsServer`)* -- `ref` — instance *(class: `HttpsServer`)* -- `request` — module -- `requestTimeout` — instance *(class: `HttpsServer`)* -- `setTimeout` — instance *(class: `HttpsServer`)* -- `timeout` — instance *(class: `HttpsServer`)* -- `unref` — instance *(class: `HttpsServer`)* - -### Properties - -- `globalAgent` - -## `inspector` - -### Classes - -- `Session` - -### Methods - -- `Session` — module -- `close` — module -- `connect` — instance *(class: `Session`)* -- `connectToMainThread` — instance *(class: `Session`)* -- `debug` — module *(class: `console`)* -- `disconnect` — instance *(class: `Session`)* -- `error` — module *(class: `console`)* -- `info` — module *(class: `console`)* -- `log` — module *(class: `console`)* -- `on` — instance *(class: `Session`)* -- `once` — instance *(class: `Session`)* -- `open` — module ⚠ **stub** — accepts port/host but binds no real WebSocket inspector endpoint; sessions are in-process fakes (#4916) -- `post` — instance *(class: `Session`)* ⚠ **stub** — only Runtime.enable and a canned Runtime.evaluate subset respond; every other protocol method throws Inspector error -32601 (#4916) -- `url` — module ⚠ **stub** — always undefined: Perry never exposes a real inspector endpoint (#4916) -- `waitForDebugger` — module ⚠ **stub** — returns immediately after open(); there is no debugger to wait for (#4916) -- `warn` — module *(class: `console`)* - -### Properties - -- `Network` -- `console` -- `default` - -## `inspector/promises` - -### Classes - -- `Session` - -### Methods - -- `Session` — module -- `connect` — instance *(class: `Session`)* -- `connectToMainThread` — instance *(class: `Session`)* -- `disconnect` — instance *(class: `Session`)* -- `on` — instance *(class: `Session`)* -- `once` — instance *(class: `Session`)* -- `post` — instance *(class: `Session`)* - -### Properties - -- `default` - -## `lodash` - -### Methods - -- `camelCase` — module -- `chunk` — module -- `clamp` — module -- `clamp` — module -- `compact` — module -- `drop` — module -- `first` — module -- `flatten` — module -- `head` — module -- `inRange` — module -- `kebabCase` — module -- `last` — module -- `max` — module -- `maxBy` — module -- `mean` — module -- `meanBy` — module -- `min` — module -- `minBy` — module -- `random` — module -- `range` — module -- `reverse` — module -- `size` — module -- `snakeCase` — module -- `sum` — module -- `sumBy` — module -- `tail` — module -- `take` — module -- `times` — module -- `uniq` — module - -## `module` - -### Classes - -- `Module` -- `SourceMap` - -### Methods - -- `Module` — module -- `SourceMap` — module -- `_findPath` — module -- `_initPaths` — module -- `_load` — module -- `_nodeModulePaths` — module -- `_preloadModules` — module -- `_resolveFilename` — module -- `_resolveLookupPaths` — module -- `createRequire` — module -- `enableCompileCache` — module -- `findPackageJSON` — module -- `findSourceMap` — module -- `flushCompileCache` — module -- `getCompileCacheDir` — module -- `getSourceMapsSupport` — module -- `isBuiltin` — module -- `register` — module -- `registerHooks` — module -- `runMain` — module -- `setSourceMapsSupport` — module -- `stripTypeScriptTypes` — module -- `syncBuiltinESMExports` — module - -### Properties - -- `Module` -- `_cache` -- `_extensions` -- `_pathCache` -- `builtinModules` -- `constants` -- `default` -- `globalPaths` - -## `net` - -### Classes - -- `BlockList` -- `Server` -- `Socket` -- `SocketAddress` -- `Stream` - -### Methods - -- `BlockList` — module -- `Server` — module -- `Socket` — module -- `SocketAddress` — module -- `Stream` — module -- `__set_dropMaxConnection` — instance *(class: `Server`)* -- `__set_maxConnections` — instance *(class: `Server`)* -- `_createServerHandle` — module -- `_normalizeArgs` — module -- `_readableState` — instance -- `_writableState` — instance -- `addAddress` — instance *(class: `BlockList`)* -- `addListener` — instance *(class: `Socket`)* -- `addListener` — instance *(class: `Server`)* -- `addRange` — instance *(class: `BlockList`)* -- `addSubnet` — instance *(class: `BlockList`)* -- `address` — instance *(class: `Socket`)* -- `address` — instance *(class: `SocketAddress`)* -- `address` — instance *(class: `Server`)* -- `autoSelectFamilyAttemptedAddresses` — instance *(class: `Socket`)* -- `bufferSize` — instance *(class: `Socket`)* -- `bytesRead` — instance *(class: `Socket`)* -- `bytesWritten` — instance *(class: `Socket`)* -- `check` — instance *(class: `BlockList`)* -- `close` — instance *(class: `Server`)* -- `connect` — module -- `connect` — instance *(class: `Socket`)* -- `connecting` — instance *(class: `Socket`)* -- `cork` — instance *(class: `Socket`)* -- `createConnection` — module -- `createServer` — module -- `destroy` — instance *(class: `Socket`)* -- `destroyed` — instance *(class: `Socket`)* -- `dropMaxConnection` — instance *(class: `Server`)* -- `end` — instance *(class: `Socket`)* -- `eventNames` — instance *(class: `Socket`)* -- `eventNames` — instance *(class: `Server`)* -- `exportKeyingMaterial` — instance *(class: `Socket`)* -- `family` — instance *(class: `SocketAddress`)* -- `flowlabel` — instance *(class: `SocketAddress`)* -- `fromJSON` — instance *(class: `BlockList`)* -- `getCertificate` — instance *(class: `Socket`)* -- `getCipher` — instance *(class: `Socket`)* -- `getConnections` — instance *(class: `Server`)* -- `getDefaultAutoSelectFamily` — module -- `getDefaultAutoSelectFamilyAttemptTimeout` — module -- `getEphemeralKeyInfo` — instance *(class: `Socket`)* -- `getFinished` — instance *(class: `Socket`)* -- `getPeerCertificate` — instance *(class: `Socket`)* -- `getPeerFinished` — instance *(class: `Socket`)* -- `getPeerX509Certificate` — instance *(class: `Socket`)* -- `getProtocol` — instance *(class: `Socket`)* -- `getSession` — instance *(class: `Socket`)* -- `getSharedSigalgs` — instance *(class: `Socket`)* -- `getTypeOfService` — instance *(class: `Socket`)* -- `getX509Certificate` — instance *(class: `Socket`)* -- `isBlockList` — module *(class: `BlockList`)* -- `isIP` — module -- `isIPv4` — module -- `isIPv6` — module -- `isSessionReused` — instance *(class: `Socket`)* -- `listen` — instance *(class: `Server`)* -- `listenerCount` — instance *(class: `Socket`)* -- `listenerCount` — instance *(class: `Server`)* -- `listeners` — instance *(class: `Socket`)* -- `listeners` — instance *(class: `Server`)* -- `listening` — instance *(class: `Server`)* -- `localAddress` — instance *(class: `Socket`)* -- `localFamily` — instance *(class: `Socket`)* -- `localPort` — instance *(class: `Socket`)* -- `maxConnections` — instance *(class: `Server`)* -- `off` — instance *(class: `Socket`)* -- `off` — instance *(class: `Server`)* -- `on` — instance *(class: `Socket`)* -- `once` — instance *(class: `Socket`)* -- `once` — instance *(class: `Server`)* -- `parse` — module *(class: `SocketAddress`)* -- `pause` — instance *(class: `Socket`)* -- `pending` — instance *(class: `Socket`)* -- `pipe` — instance *(class: `Socket`)* -- `port` — instance *(class: `SocketAddress`)* -- `prependListener` — instance *(class: `Socket`)* -- `prependOnceListener` — instance *(class: `Socket`)* -- `rawListeners` — instance *(class: `Socket`)* -- `rawListeners` — instance *(class: `Server`)* -- `read` — instance *(class: `Socket`)* -- `readable` — instance -- `readableEnded` — instance -- `readyState` — instance *(class: `Socket`)* -- `ref` — instance *(class: `Socket`)* -- `remoteAddress` — instance *(class: `Socket`)* -- `remoteFamily` — instance *(class: `Socket`)* -- `remotePort` — instance *(class: `Socket`)* -- `removeAllListeners` — instance *(class: `Socket`)* -- `removeAllListeners` — instance *(class: `Server`)* -- `removeListener` — instance *(class: `Socket`)* -- `removeListener` — instance *(class: `Server`)* -- `resetAndDestroy` — instance *(class: `Socket`)* -- `resume` — instance *(class: `Socket`)* -- `rules` — instance *(class: `BlockList`)* -- `setDefaultAutoSelectFamily` — module -- `setDefaultAutoSelectFamilyAttemptTimeout` — module -- `setDefaultEncoding` — instance *(class: `Socket`)* -- `setEncoding` — instance *(class: `Socket`)* -- `setKeepAlive` — instance *(class: `Socket`)* -- `setKeyCert` — instance *(class: `Socket`)* -- `setMaxSendFragment` — instance *(class: `Socket`)* -- `setNoDelay` — instance *(class: `Socket`)* -- `setTimeout` — instance *(class: `Socket`)* -- `setTypeOfService` — instance *(class: `Socket`)* -- `timeout` — instance *(class: `Socket`)* -- `toJSON` — instance *(class: `BlockList`)* -- `uncork` — instance *(class: `Socket`)* -- `unpipe` — instance *(class: `Socket`)* -- `unref` — instance *(class: `Socket`)* -- `upgradeToTLS` — instance *(class: `Socket`)* -- `writable` — instance -- `writableCorked` — instance *(class: `Socket`)* -- `writableEnded` — instance -- `write` — instance *(class: `Socket`)* - -## `node-fetch` - -### Classes - -- `Blob` -- `FormData` -- `Headers` -- `Request` -- `Response` - -### Methods - -- `default` — module - -## `node-pty` - -### Methods - -- `spawn` — module - -### Properties - -- `default` - -## `nodemailer` - -### Methods - -- `createTransport` — module -- `sendMail` — instance -- `verify` — instance - -## `os` - -### Methods - -- `arch` — module -- `availableParallelism` — module -- `cpus` — module -- `endianness` — module -- `freemem` — module -- `getPriority` — module -- `homedir` — module -- `hostname` — module -- `loadavg` — module -- `machine` — module -- `networkInterfaces` — module -- `platform` — module -- `release` — module -- `setPriority` — module -- `tmpdir` — module -- `totalmem` — module -- `type` — module -- `uptime` — module -- `userInfo` — module -- `version` — module - -### Properties - -- `EOL` -- `constants` -- `default` -- `devNull` - -## `path` - -### Methods - -- `_makeLong` — module -- `basename` — module -- `dirname` — module -- `extname` — module -- `format` — module -- `isAbsolute` — module -- `join` — module -- `matchesGlob` — module -- `normalize` — module -- `parse` — module -- `relative` — module -- `resolve` — module -- `toNamespacedPath` — module - -### Properties - -- `default` -- `delimiter` -- `posix` -- `sep` -- `win32` - -## `path/posix` - -### Methods - -- `_makeLong` — module -- `basename` — module -- `dirname` — module -- `extname` — module -- `format` — module -- `isAbsolute` — module -- `join` — module -- `matchesGlob` — module -- `normalize` — module -- `parse` — module -- `relative` — module -- `resolve` — module -- `toNamespacedPath` — module - -### Properties - -- `default` -- `delimiter` -- `posix` -- `sep` -- `win32` - -## `path/win32` - -### Methods - -- `_makeLong` — module -- `basename` — module -- `dirname` — module -- `extname` — module -- `format` — module -- `isAbsolute` — module -- `join` — module -- `matchesGlob` — module -- `normalize` — module -- `parse` — module -- `relative` — module -- `resolve` — module -- `toNamespacedPath` — module - -### Properties - -- `default` -- `delimiter` -- `posix` -- `sep` -- `win32` - -## `perf_hooks` - -### Classes - -- `Performance` -- `PerformanceEntry` -- `PerformanceMark` -- `PerformanceMeasure` -- `PerformanceObserver` -- `PerformanceObserverEntryList` -- `PerformanceResourceTiming` - -### Methods - -- `createHistogram` — module -- `disconnect` — instance *(class: `PerformanceObserver`)* -- `eventLoopUtilization` — module -- `monitorEventLoopDelay` — module -- `observe` — instance *(class: `PerformanceObserver`)* -- `takeRecords` — instance *(class: `PerformanceObserver`)* -- `timerify` — module - -### Properties - -- `constants` -- `default` -- `performance` - -## `perry` - -### Methods - -- `embeddedFiles` — module -- `readEmbedded` — module - -### Properties - -- `isStandaloneExecutable` - -## `perry/ads` - -### Methods - -- `js_ads_banner_create` — module -- `js_ads_banner_destroy` — module -- `js_ads_interstitial_load` — module -- `js_ads_interstitial_show` — module -- `js_ads_request_consent` — module -- `js_ads_rewarded_load` — module -- `js_ads_rewarded_show` — module - -## `perry/audio` - -### Methods - -- `createBus` — module -- `crossfade` — module -- `destroyBus` — module -- `fadeIn` — module -- `fadeOut` — module -- `getDuration` — module -- `getPosition` — module -- `isPlaying` — module -- `loadSound` — module -- `muteBus` — module -- `onEnded` — module -- `onLoaded` — module -- `pause` — module -- `play` — module -- `resume` — module -- `resumeAll` — module -- `setMasterVolume` — module -- `setPan` — module -- `setRate` — module -- `setVolume` — module -- `soloBus` — module -- `stop` — module -- `suspend` — module -- `unload` — module - -## `perry/background` - -### Methods - -- `cancel` — module -- `registerTask` — module -- `schedule` — module - -## `perry/compose` - -### Methods - -- `config` — module -- `down` — module -- `exec` — module -- `logs` — module -- `ps` — module -- `restart` — module -- `start` — module -- `stop` — module -- `up` — module - -## `perry/container` - -### Methods - -- `composeUp` — module -- `create` — module -- `detectBackend` — module -- `downAll` — module -- `downByProject` — module -- `exec` — module -- `getAvailableBackends` — module -- `getBackend` — module -- `getBackendPriority` — module -- `inspect` — module -- `list` — module -- `listImages` — module -- `logs` — module -- `pullImage` — module -- `remove` — module -- `removeIfExists` — module -- `removeImage` — module -- `run` — module -- `selectBackendFor` — module -- `setBackend` — module -- `setBackends` — module -- `start` — module -- `stop` — module - -## `perry/container-compose` - -### Methods - -- `config` — module -- `down` — module -- `exec` — module -- `logs` — module -- `ps` — module -- `restart` — module -- `start` — module -- `stop` — module -- `up` — module - -## `perry/gc` - -### Methods - -- `collect` — module -- `idleHint` — module -- `minor` — module - -## `perry/i18n` - -### Methods - -- `Currency` — module -- `FormatNumber` — module -- `FormatTime` — module -- `LongDate` — module -- `Percent` — module -- `Raw` — module -- `ShortDate` — module -- `t` — module - -## `perry/ios` - -### Methods - -- `createLanguageModelSession` — module -- `destroyLanguageModelSession` — module -- `foundationModelAvailability` — module -- `getLayoutEnvironment` — module -- `offLayoutChange` — module -- `onLayoutChange` — module -- `respond` — module - -## `perry/media` - -### Methods - -- `createPlayer` — module -- `destroy` — module -- `getCurrentTime` — module -- `getDuration` — module -- `getState` — module -- `isPlaying` — module -- `onStateChange` — module -- `onTimeUpdate` — module -- `pause` — module -- `play` — module -- `seek` — module -- `setNowPlaying` — module -- `setRate` — module -- `setVolume` — module -- `stop` — module - -## `perry/native` - -### Methods - -- `alignof` — module *(intrinsic)* -- `f32` — module -- `f64` — module -- `i16` — module -- `i32` — module -- `i64` — module -- `i8` — module -- `isize` — module -- `offsetof` — module *(intrinsic)* -- `sizeof` — module *(intrinsic)* -- `u16` — module -- `u32` — module -- `u64` — module -- `u8` — module -- `usize` — module - -### Properties - -- `NativeArena` - -## `perry/plugin` - -### Classes - -- `PluginApi` - -### Methods - -- `discoverPlugins` — module -- `emitEvent` — module -- `emitHook` — module -- `initPlugins` — module -- `invokeTool` — module -- `listHooks` — module -- `listPlugins` — module -- `listTools` — module -- `loadPlugin` — module -- `pluginCount` — module -- `setPluginConfig` — module -- `unloadPlugin` — module - -## `perry/system` - -### Methods - -- `appGetLaunchUrl` — module -- `appGroupDelete` — module -- `appGroupGet` — module -- `appGroupSet` — module -- `appOnOpenUrl` — module -- `audioGetLevel` — module -- `audioGetPeak` — module -- `audioGetWaveform` — module -- `audioRegisterCallback` — module -- `audioSetOutputFilename` — module -- `audioStart` — module -- `audioStartRecording` — module -- `audioStop` — module -- `audioStopRecording` — module -- `audioUnregisterCallback` — module -- `geolocationGetCurrent` — module -- `geolocationRequestPermission` — module -- `geolocationStopWatch` — module -- `geolocationWatch` — module -- `getAppBuildNumber` — module -- `getAppIcon` — module -- `getAppVersion` — module -- `getBundleId` — module -- `getDeviceIdiom` — module -- `getDeviceModel` — module -- `getLocale` — module -- `getOSVersion` — module -- `getSafeAreaInsets` — module -- `hapticPlay` — module -- `imagePickerPick` — module -- `isDarkMode` — module -- `keychainDelete` — module -- `keychainGet` — module -- `keychainSave` — module -- `networkGetStatus` — module -- `networkOnChange` — module -- `networkStopOnChange` — module -- `notificationCancel` — module -- `notificationOnBackgroundReceive` — module -- `notificationOnReceive` — module -- `notificationOnTap` — module -- `notificationRegisterRemote` — module -- `notificationSend` — module -- `openURL` — module -- `preferencesGet` — module -- `preferencesSet` — module -- `shareText` — module -- `shareUrl` — module -- `takeScreenshot` — module - -## `perry/thread` - -### Methods - -- `parallelFilter` — module -- `parallelMap` — module -- `spawn` — module - -## `perry/tui` - -### Methods - -- `AnimatedSpinner` — module -- `Box` — module -- `Input` — module -- `InputAt` — module -- `List` — module -- `ProgressBar` — module -- `Select` — module -- `Spacer` — module -- `Spinner` — module -- `Table` — module -- `Tabs` — module -- `Text` — module -- `TextArea` — module -- `TextStyled` — module -- `boxSetAlignItems` — module -- `boxSetFlexBasis` — module -- `boxSetFlexBasisPct` — module -- `boxSetFlexDirection` — module -- `boxSetFlexGrow` — module -- `boxSetFlexShrink` — module -- `boxSetGap` — module -- `boxSetHeight` — module -- `boxSetHeightPct` — module -- `boxSetJustifyContent` — module -- `boxSetPadding` — module -- `boxSetPaddingEach` — module -- `boxSetWidth` — module -- `boxSetWidthPct` — module -- `columns` — instance *(class: `TuiStdout`)* -- `enter` — module -- `exit` — module -- `exit` — instance *(class: `TuiApp`)* -- `focus` — module -- `focus` — instance *(class: `FocusManager`)* -- `focusNext` — module -- `focusNext` — instance *(class: `FocusManager`)* -- `focusPrevious` — module -- `focusPrevious` — instance *(class: `FocusManager`)* -- `get` — instance *(class: `State`)* -- `get` — instance *(class: `RefBox`)* -- `render` — module -- `rows` — instance *(class: `TuiStdout`)* -- `run` — module -- `set` — instance *(class: `State`)* -- `set` — instance *(class: `RefBox`)* -- `state` — module -- `useApp` — module -- `useEffect` — module -- `useFocus` — module -- `useFocusManager` — module -- `useInput` — module -- `useMemo` — module -- `useRef` — module -- `useState` — module -- `useStateSet` — module -- `useStateTuple` — module -- `useStdout` — module -- `waitUntilExit` — module -- `waitUntilExit` — instance *(class: `TuiApp`)* -- `write` — instance *(class: `TuiStdout`)* - -## `perry/ui` - -### Methods - -- `App` — module -- `AttributedText` — module -- `BloomView` — module -- `BottomNavigation` — module -- `Button` — module -- `CameraView` — module -- `Canvas` — module -- `Divider` — module -- `ForEach` — module -- `HStack` — module -- `HStackWithInsets` — module -- `Image` — module -- `ImageFile` — module -- `ImageGallery` — module -- `ImageSymbol` — module -- `LazyVStack` — module -- `NavStack` — module -- `Picker` — module -- `ProgressView` — module -- `ScrollView` — module -- `Section` — module -- `SecureField` — module -- `Slider` — module -- `Spacer` — module -- `SplitView` — module -- `State` — module -- `TabBar` — module -- `Table` — module -- `Text` — module -- `TextArea` — module -- `TextField` — module -- `Toggle` — module -- `VStack` — module -- `VStackWithInsets` — module -- `WebView` — module -- `WheelPicker` — module -- `Window` — module -- `ZStack` — module -- `addKeyboardShortcut` — module -- `alert` — module -- `alertWithButtons` — module -- `appSetActivationPolicy` — module -- `appSetMaxSize` — module -- `appSetMinSize` — module -- `appSetTimer` — module -- `attributedTextAppend` — module -- `attributedTextClear` — module -- `bloomViewGetHwnd` — module -- `bloomViewGetNativeHandle` — module -- `blur` — module -- `bottomNavAddItem` — module -- `bottomNavSetBadge` — module -- `bottomNavSetSelected` — module -- `bottomNavSetTintColor` — module -- `bottomNavSetUnselectedTintColor` — module -- `cameraFreeze` — module -- `cameraRegisterFrameCallback` — module -- `cameraSampleColor` — module -- `cameraSetOnTap` — module -- `cameraStart` — module -- `cameraStop` — module -- `cameraUnfreeze` — module -- `cameraUnregisterFrameCallback` — module -- `clipboardRead` — module -- `clipboardWrite` — module -- `currentModifiers` — module -- `embedNSView` — module -- `focus` — module -- `frameSplitAddChild` — module -- `frameSplitCreate` — module -- `imageGalleryAddImage` — module -- `imageGallerySetIndex` — module -- `isKeyDown` — module -- `lazyvstackEndRefreshing` — module -- `lazyvstackSetRefreshControl` — module -- `lazyvstackSetScrollEndCallback` — module -- `loadImage` — module -- `menuAddItem` — module -- `menuAddItemWithShortcut` — module -- `menuAddSeparator` — module -- `menuAddStandardAction` — module -- `menuAddSubmenu` — module -- `menuBarAddMenu` — module -- `menuBarAttach` — module -- `menuBarCreate` — module -- `menuClear` — module -- `menuCreate` — module -- `onActivate` — module -- `onAppKeyDown` — module -- `onAppKeyUp` — module -- `onKeyDown` — module -- `onKeyUp` — module -- `onTerminate` — module -- `openFileDialog` — module -- `openFolderDialog` — module -- `pollOpenFile` — module -- `registerGlobalHotkey` — module -- `saveFileDialog` — module -- `scrollViewSetScrollEndCallback` — module -- `scrollviewSetScrollEndCallback` — module -- `setText` — module -- `sheetCreate` — module -- `sheetDismiss` — module -- `sheetPresent` — module -- `showToast` — module -- `toolbarAddItem` — module -- `toolbarAttach` — module -- `toolbarCreate` — module -- `trayAttachMenu` — module -- `trayCreate` — module -- `trayDestroy` — module -- `trayOnClick` — module -- `traySetIcon` — module -- `traySetTooltip` — module -- `webviewCanGoBack` — module -- `webviewClearCookies` — module -- `webviewEvaluateJs` — module -- `webviewGoBack` — module -- `webviewGoForward` — module -- `webviewLoadUrl` — module -- `webviewReload` — module -- `wheelPickerAddItem` — module -- `wheelPickerGetSelected` — module -- `wheelPickerSetSelected` — module - -## `perry/updater` - -### Methods - -- `clearSentinel` — module -- `compareVersions` — module -- `computeFileSha256` — module -- `embeddedCheckHeaders` — module -- `embeddedCheckUrl` — module -- `embeddedRefreshDue` — module -- `getBackupPath` — module -- `getEmbeddedConfig` — module -- `getExePath` — module -- `getSentinelPath` — module -- `installUpdate` — module -- `performRollback` — module -- `readSentinel` — module -- `recordEmbeddedResponse` — module -- `relaunch` — module -- `verifyHash` — module -- `verifySignature` — module -- `verifySignatureV2` — module -- `writeSentinel` — module - -## `perry/widget` - -### Methods - -- `Widget` — module - -## `perry/workloads` - -### Methods - -- `graph` — module -- `inspectGraph` — module -- `node` — module -- `runGraph` — module - -### Properties - -- `policy` -- `runtime` - -## `perry/yoga` - -### Methods - -- `calculateLayout` — module -- `childCount` — module -- `getComputed` — module -- `getComputedEdge` — module -- `insertChild` — module -- `nodeFree` — module -- `nodeNew` — module -- `removeChild` — module -- `setEdge` — module -- `setEnum` — module -- `setGap` — module -- `setMeasureFunc` — module -- `setNumber` — module -- `unsetMeasureFunc` — module - -## `process` - -### Methods - -- `_debugEnd` — module -- `_debugProcess` — module -- `_fatalException` — module -- `_getActiveHandles` — module -- `_getActiveRequests` — module -- `_kill` — module -- `_linkedBinding` — module -- `_rawDebug` — module -- `_startProfilerIdleNotifier` — module -- `_stopProfilerIdleNotifier` — module -- `_tickCallback` — module -- `abort` — module -- `addUncaughtExceptionCaptureCallback` — module -- `availableMemory` — module -- `binding` — module -- `chdir` — module -- `constrainedMemory` — module -- `cpuUsage` — module -- `cwd` — module -- `dlopen` — module -- `emitWarning` — module -- `execve` — module -- `exit` — module -- `getActiveResourcesInfo` — module -- `getBuiltinModule` — module -- `getegid` — module -- `geteuid` — module -- `getgid` — module -- `getgroups` — module -- `getuid` — module -- `hasUncaughtExceptionCaptureCallback` — module -- `hrtime` — module -- `initgroups` — module -- `kill` — module -- `loadEnvFile` — module -- `memoryUsage` — module -- `nextTick` — module -- `openStdin` — module -- `reallyExit` — module -- `ref` — module -- `resourceUsage` — module -- `setSourceMapsEnabled` — module -- `setSourceMapsEnabled` — module -- `setUncaughtExceptionCaptureCallback` — module -- `setegid` — module -- `seteuid` — module -- `setgid` — module -- `setgroups` — module -- `setuid` — module -- `sourceMapsEnabled` — module -- `sourceMapsEnabled` — module -- `threadCpuUsage` — module -- `umask` — module -- `unref` — module -- `uptime` — module - -### Properties - -- `_eval` -- `_events` -- `_eventsCount` -- `_exiting` -- `_maxListeners` -- `_preload_modules` -- `allowedNodeEnvironmentFlags` -- `arch` -- `argv` -- `argv0` -- `config` -- `debugPort` -- `domain` -- `env` -- `execArgv` -- `execPath` -- `features` -- `finalization` -- `moduleLoadList` -- `permission` -- `pid` -- `platform` -- `ppid` -- `release` -- `report` -- `stderr` -- `stdin` -- `stdout` -- `title` -- `version` -- `versions` - -## `punycode` - -### Methods - -- `decode` — module -- `encode` — module -- `toASCII` — module -- `toUnicode` — module - -### Properties - -- `default` -- `ucs2` -- `version` - -## `querystring` - -### Methods - -- `decode` — module -- `encode` — module -- `escape` — module -- `parse` — module -- `stringify` — module -- `unescape` — module -- `unescapeBuffer` — module - -### Properties - -- `default` - -## `readline` - -### Methods - -- `clearLine` — module -- `clearScreenDown` — module -- `close` — instance -- `createInterface` — module -- `cursorTo` — module -- `emitKeypressEvents` — module -- `getCursorPos` — instance -- `getPrompt` — instance -- `iterator` — instance -- `line` — instance -- `moveCursor` — module -- `on` — instance -- `pause` — instance -- `prompt` — instance -- `question` — instance -- `resume` — instance -- `setPrompt` — instance -- `terminal` — instance -- `write` — instance - -## `readline/promises` - -### Classes - -- `Interface` -- `Readline` - -### Methods - -- `close` — instance -- `createInterface` — module -- `question` — instance - -## `repl` - -### Classes - -- `REPLServer` -- `Recoverable` - -### Methods - -- `REPLServer` — module ⚠ **stub** — REPLServer shape only: never reads the input stream, and .write() evaluates just numeric literals, context lookups, and a single '+'; no real JS eval loop (#4916) -- `Recoverable` — module -- `addListener` — instance *(class: `REPLServer`)* -- `clearBufferedCommand` — instance *(class: `REPLServer`)* -- `defineCommand` — instance *(class: `REPLServer`)* -- `displayPrompt` — instance *(class: `REPLServer`)* -- `emit` — instance *(class: `REPLServer`)* -- `on` — instance *(class: `REPLServer`)* -- `once` — instance *(class: `REPLServer`)* -- `setupHistory` — instance *(class: `REPLServer`)* -- `start` — module ⚠ **stub** — REPLServer shape only: never reads the input stream, and .write() evaluates just numeric literals, context lookups, and a single '+'; no real JS eval loop (#4916) -- `write` — instance *(class: `REPLServer`)* - -### Properties - -- `REPL_MODE_SLOPPY` -- `REPL_MODE_STRICT` -- `builtinModules` -- `default` - -## `sea` - -### Methods - -- `getAsset` — module -- `getAssetAsBlob` — module -- `getAssetKeys` — module -- `getRawAsset` — module -- `isSea` — module - -### Properties - -- `default` - -## `sharp` - -### Methods - -- `autoOrient` — instance -- `avif` — instance -- `blur` — instance -- `composite` — instance -- `default` — module -- `extend` — instance -- `extract` — instance -- `flip` — instance -- `flop` — instance -- `grayscale` — instance -- `height` — instance -- `jpeg` — instance -- `metadata` — instance -- `png` — instance -- `resize` — instance -- `rotate` — instance -- `sharp` — module -- `sharpen` — instance -- `toBuffer` — instance -- `toFile` — instance -- `trim` — instance -- `webp` — instance -- `width` — instance - -## `sqlite` - -### Classes - -- `DatabaseSync` -- `SQLTagStore` -- `Session` -- `StatementSync` - -### Methods - -- `@@__perry_wk_dispose` — instance -- `DatabaseSync` — module -- `Session` — module -- `StatementSync` — module -- `__perry_dispose__` — instance -- `aggregate` — instance *(class: `DatabaseSync`)* -- `all` — instance *(class: `SQLTagStore`)* -- `all` — instance -- `applyChangeset` — instance -- `backup` — module -- `capacity` — instance *(class: `SQLTagStore`)* -- `changeset` — instance -- `clear` — instance *(class: `SQLTagStore`)* -- `close` — instance -- `columns` — instance -- `createSession` — instance -- `createTagStore` — instance *(class: `DatabaseSync`)* -- `db` — instance *(class: `SQLTagStore`)* -- `deserialize` — instance -- `enableDefensive` — instance *(class: `DatabaseSync`)* -- `enableLoadExtension` — instance -- `exec` — instance -- `expandedSQL` — instance -- `function` — instance *(class: `DatabaseSync`)* -- `get` — instance *(class: `SQLTagStore`)* -- `get` — instance -- `isOpen` — instance -- `isTransaction` — instance -- `iterate` — instance *(class: `SQLTagStore`)* -- `iterate` — instance -- `limits` — instance -- `loadExtension` — instance -- `location` — instance -- `open` — instance -- `patchset` — instance -- `prepare` — instance -- `run` — instance *(class: `SQLTagStore`)* -- `run` — instance -- `serialize` — instance -- `setAllowBareNamedParameters` — instance -- `setAllowUnknownNamedParameters` — instance -- `setAuthorizer` — instance *(class: `DatabaseSync`)* -- `setReadBigInts` — instance -- `setReturnArrays` — instance -- `size` — instance *(class: `SQLTagStore`)* -- `sourceSQL` — instance - -### Properties - -- `constants` - -## `stream` - -### Classes - -- `Duplex` -- `PassThrough` -- `Readable` -- `Stream` -- `Transform` -- `Writable` - -### Methods - -- `_isArrayBufferView` — module -- `_isUint8Array` — module -- `_uint8ArrayToBuffer` — module -- `addAbortSignal` — module -- `addListener` — instance -- `allowHalfOpen` — instance -- `closed` — instance -- `compose` — module -- `cork` — instance -- `default` — module -- `destroy` — instance -- `destroyed` — instance -- `duplexPair` — module -- `emit` — instance -- `end` — instance -- `errored` — instance -- `eventNames` — instance -- `finished` — module -- `getDefaultHighWaterMark` — module -- `getMaxListeners` — instance -- `isDestroyed` — module -- `isDisturbed` — module -- `isErrored` — module -- `isPaused` — instance -- `isReadable` — module -- `isWritable` — module -- `listenerCount` — instance -- `listeners` — instance -- `off` — instance -- `on` — instance -- `once` — instance -- `pause` — instance -- `pipe` — instance -- `pipeline` — module -- `prependListener` — instance -- `prependOnceListener` — instance -- `push` — instance -- `rawListeners` — instance -- `read` — instance -- `readable` — instance -- `readableAborted` — instance -- `readableDidRead` — instance -- `readableEncoding` — instance -- `readableEnded` — instance -- `readableFlowing` — instance -- `readableHighWaterMark` — instance -- `readableLength` — instance -- `readableObjectMode` — instance -- `removeAllListeners` — instance -- `removeListener` — instance -- `resume` — instance -- `setDefaultHighWaterMark` — module -- `setEncoding` — instance -- `setMaxListeners` — instance -- `uncork` — instance -- `unpipe` — instance -- `unshift` — instance -- `writable` — instance -- `writableCorked` — instance -- `writableEnded` — instance -- `writableFinished` — instance -- `writableHighWaterMark` — instance -- `writableLength` — instance -- `writableNeedDrain` — instance -- `writableObjectMode` — instance -- `write` — instance - -### Properties - -- `promises` -- `promises` - -## `stream/consumers` - -### Methods - -- `arrayBuffer` — module -- `blob` — module -- `buffer` — module -- `bytes` — module -- `json` — module -- `text` — module - -### Properties - -- `default` - -## `stream/promises` - -### Methods - -- `finished` — module -- `finished` — module -- `pipeline` — module -- `pipeline` — module - -## `stream/web` - -### Classes - -- `ByteLengthQueuingStrategy` -- `CompressionStream` -- `CountQueuingStrategy` -- `DecompressionStream` -- `ReadableByteStreamController` -- `ReadableStream` -- `ReadableStreamBYOBReader` -- `ReadableStreamBYOBRequest` -- `ReadableStreamDefaultController` -- `ReadableStreamDefaultReader` -- `TextDecoderStream` -- `TextEncoderStream` -- `TransformStream` -- `TransformStreamDefaultController` -- `WritableStream` -- `WritableStreamDefaultController` -- `WritableStreamDefaultWriter` - -### Properties - -- `default` - -## `streams` - -### Classes - -- `ByteLengthQueuingStrategy` -- `CountQueuingStrategy` -- `DecompressionStream` -- `ReadableStream` -- `TextDecoder` -- `TextEncoder` -- `TransformStream` -- `WritableStream` - -## `string_decoder` - -### Classes - -- `StringDecoder` - -### Methods - -- `end` — instance *(class: `StringDecoder`)* -- `write` — instance *(class: `StringDecoder`)* - -## `sys` - -### Classes - -- `MIMEParams` -- `MIMEType` -- `TextDecoder` -- `TextEncoder` - -### Methods - -- `MIMEParams` — module -- `MIMEType` — module -- `_errnoException` — module -- `_exceptionWithHostPort` — module -- `_extend` — module -- `aborted` — module -- `callbackify` — module -- `convertProcessSignalToExitCode` — module -- `debug` — module -- `debuglog` — module -- `deprecate` — module -- `diff` — module -- `format` — module -- `formatWithOptions` — module -- `getCallSites` — module -- `getSystemErrorMap` — module -- `getSystemErrorMessage` — module -- `getSystemErrorName` — module -- `inherits` — module -- `inspect` — module -- `isArray` — module -- `isDeepStrictEqual` — module -- `parseArgs` — module -- `parseEnv` — module -- `promisify` — module -- `setTraceSigInt` — module -- `stripVTControlCharacters` — module -- `styleText` — module -- `toUSVString` — module -- `transferableAbortController` — module -- `transferableAbortSignal` — module - -### Properties - -- `default` -- `types` - -## `test` - -### Methods - -- `after` — module -- `afterEach` — module -- `before` — module -- `beforeEach` — module -- `default` — module -- `describe` — module -- `enable` — module *(class: `timers`)* -- `expectFailure` — module -- `fn` — module *(class: `mock`)* -- `getter` — module *(class: `mock`)* -- `it` — module -- `method` — module *(class: `mock`)* -- `only` — module -- `property` — module *(class: `mock`)* -- `reset` — module *(class: `mock`)* -- `restoreAll` — module *(class: `mock`)* -- `run` — module -- `runAll` — module *(class: `timers`)* -- `setDefaultSnapshotSerializers` — module *(class: `snapshot`)* -- `setResolveSnapshotPath` — module *(class: `snapshot`)* -- `setTime` — module *(class: `timers`)* -- `setter` — module *(class: `mock`)* -- `skip` — module -- `suite` — module -- `test` — module -- `tick` — module *(class: `timers`)* -- `todo` — module - -### Properties - -- `assert` -- `mock` -- `snapshot` - -## `test/reporters` - -### Methods - -- `dot` — module -- `junit` — module -- `lcov` — module -- `spec` — module -- `tap` — module - -### Properties - -- `default` - -## `timers` - -### Methods - -- `clearImmediate` — module -- `clearInterval` — module -- `clearTimeout` — module -- `setImmediate` — module -- `setInterval` — module -- `setTimeout` — module - -### Properties - -- `promises` - -## `timers/promises` - -### Methods - -- `setImmediate` — module -- `setInterval` — module -- `setTimeout` — module - -### Properties - -- `scheduler` - -## `tls` - -### Classes - -- `SecureContext` - -### Methods - -- `SecureContext` — module -- `Server` — module -- `TLSSocket` — module -- `addListener` — instance *(class: `Server`)* -- `address` — instance *(class: `Server`)* -- `checkServerIdentity` — module -- `close` — instance *(class: `Server`)* -- `connect` — module -- `convertALPNProtocols` — module -- `createSecureContext` — module -- `createServer` — module -- `eventNames` — instance *(class: `Server`)* -- `getCACertificates` — module -- `getCertificateCompressionAlgorithms` — module -- `getCiphers` — module -- `getTicketKeys` — instance *(class: `Server`)* -- `listen` — instance *(class: `Server`)* -- `listenerCount` — instance *(class: `Server`)* -- `off` — instance *(class: `Server`)* -- `on` — instance *(class: `Server`)* -- `once` — instance *(class: `Server`)* -- `removeAllListeners` — instance *(class: `Server`)* -- `removeListener` — instance *(class: `Server`)* -- `setDefaultCACertificates` — module -- `setSecureContext` — instance *(class: `Server`)* -- `setTicketKeys` — instance *(class: `Server`)* - -### Properties - -- `CLIENT_RENEG_LIMIT` -- `CLIENT_RENEG_WINDOW` -- `DEFAULT_CIPHERS` -- `DEFAULT_ECDH_CURVE` -- `DEFAULT_MAX_VERSION` -- `DEFAULT_MIN_VERSION` -- `rootCertificates` - -## `tty` - -### Classes - -- `ReadStream` -- `WriteStream` - -### Methods - -- `ReadStream` — module -- `WriteStream` — module -- `_refreshSize` — instance *(class: `WriteStream`)* -- `addListener` — instance *(class: `WriteStream`)* -- `clearLine` — instance *(class: `WriteStream`)* -- `clearScreenDown` — instance *(class: `WriteStream`)* -- `cursorTo` — instance *(class: `WriteStream`)* -- `getColorDepth` — instance *(class: `WriteStream`)* -- `getWindowSize` — instance *(class: `WriteStream`)* -- `hasColors` — instance *(class: `WriteStream`)* -- `isatty` — module -- `moveCursor` — instance *(class: `WriteStream`)* -- `off` — instance *(class: `WriteStream`)* -- `on` — instance *(class: `WriteStream`)* -- `once` — instance *(class: `WriteStream`)* -- `removeAllListeners` — instance *(class: `WriteStream`)* -- `removeListener` — instance *(class: `WriteStream`)* -- `setRawMode` — instance *(class: `ReadStream`)* - -## `typescript` - -### Methods - -- `flattenDiagnosticMessageText` — module -- `transpileModule` — module - -### Properties - -- `DiagnosticCategory` -- `ModuleKind` -- `ScriptTarget` - -## `undici` - -### Classes - -- `Agent` -- `ProxyAgent` - -### Methods - -- `Agent` — module -- `ProxyAgent` — module -- `close` — instance *(class: `ProxyAgent`)* -- `close` — instance *(class: `Agent`)* -- `destroy` — instance *(class: `ProxyAgent`)* -- `destroy` — instance *(class: `Agent`)* -- `fetch` — module -- `getGlobalDispatcher` — module -- `request` — module -- `setGlobalDispatcher` — module - -## `url` - -### Classes - -- `URL` -- `URLPattern` -- `URLSearchParams` -- `Url` - -### Methods - -- `Url` — module -- `domainToASCII` — module -- `domainToUnicode` — module -- `exec` — instance *(class: `URLPattern`)* -- `fileURLToPath` — module -- `fileURLToPathBuffer` — module -- `format` — module -- `parse` — module -- `pathToFileURL` — module -- `resolve` — module -- `resolveObject` — module -- `test` — instance *(class: `URLPattern`)* -- `urlToHttpOptions` — module - -### Properties - -- `default` - -## `util` - -### Classes - -- `MIMEParams` -- `MIMEType` -- `TextDecoder` -- `TextEncoder` - -### Methods - -- `MIMEParams` — module -- `MIMEType` — module -- `_errnoException` — module -- `_exceptionWithHostPort` — module -- `_extend` — module -- `aborted` — module -- `callbackify` — module -- `convertProcessSignalToExitCode` — module -- `debug` — module -- `debuglog` — module -- `deprecate` — module -- `diff` — module -- `format` — module -- `formatWithOptions` — module -- `getCallSites` — module -- `getSystemErrorMap` — module -- `getSystemErrorMessage` — module -- `getSystemErrorName` — module -- `inherits` — module -- `inspect` — module -- `isArray` — module -- `isDeepStrictEqual` — module -- `parseArgs` — module -- `parseEnv` — module -- `promisify` — module -- `setTraceSigInt` — module -- `stripVTControlCharacters` — module -- `styleText` — module -- `toUSVString` — module -- `transferableAbortController` — module -- `transferableAbortSignal` — module - -### Properties - -- `default` -- `types` - -## `util/types` - -### Methods - -- `isAnyArrayBuffer` — module -- `isArgumentsObject` — module -- `isArrayBuffer` — module -- `isArrayBufferView` — module -- `isAsyncFunction` — module -- `isBigInt64Array` — module -- `isBigIntObject` — module -- `isBigUint64Array` — module -- `isBooleanObject` — module -- `isBoxedPrimitive` — module -- `isCryptoKey` — module -- `isDataView` — module -- `isDate` — module -- `isExternal` — module -- `isFloat16Array` — module -- `isFloat32Array` — module -- `isFloat64Array` — module -- `isGeneratorFunction` — module -- `isGeneratorObject` — module -- `isInt16Array` — module -- `isInt32Array` — module -- `isInt8Array` — module -- `isKeyObject` — module -- `isMap` — module -- `isMapIterator` — module -- `isModuleNamespaceObject` — module -- `isNativeError` — module -- `isNumberObject` — module -- `isPromise` — module -- `isProxy` — module -- `isRegExp` — module -- `isSet` — module -- `isSetIterator` — module -- `isSharedArrayBuffer` — module -- `isStringObject` — module -- `isSymbolObject` — module -- `isTypedArray` — module -- `isUint16Array` — module -- `isUint32Array` — module -- `isUint8Array` — module -- `isUint8ClampedArray` — module -- `isWeakMap` — module -- `isWeakSet` — module - -## `v8` - -### Classes - -- `DefaultDeserializer` -- `DefaultSerializer` -- `Deserializer` -- `GCProfiler` -- `Serializer` - -### Methods - -- `addDeserializeCallback` — instance *(class: `startupSnapshot`)* -- `addSerializeCallback` — instance *(class: `startupSnapshot`)* -- `cachedDataVersionTag` — module -- `createHook` — instance *(class: `promiseHooks`)* -- `deserialize` — module -- `getCppHeapStatistics` — module -- `getHeapCodeStatistics` — module ⚠ **stub** — all fields 0; Perry compiles AOT, there is no JIT code heap (#4916) -- `getHeapSnapshot` — module -- `getHeapSpaceStatistics` — module ⚠ **stub** — Node space names with all live usage attributed to old_space from Perry arenas; other spaces report 0 (#4916) -- `getHeapStatistics` — module ⚠ **stub** — Node shape, Perry numbers: total_heap_size/used_heap_size/malloced_memory/total_allocated_bytes from Perry arenas, total_physical_size=RSS, heap_size_limit fixed ~2GB (not enforced); *_executable, external_memory, global-handles and zap fields are 0 (#4916) -- `isBuildingSnapshot` — instance *(class: `startupSnapshot`)* -- `isStringOneByteRepresentation` — module -- `onAfter` — instance *(class: `promiseHooks`)* -- `onBefore` — instance *(class: `promiseHooks`)* -- `onInit` — instance *(class: `promiseHooks`)* -- `onSettled` — instance *(class: `promiseHooks`)* -- `queryObjects` — module -- `readDouble` — instance *(class: `Deserializer`)* -- `readHeader` — instance *(class: `Deserializer`)* -- `readRawBytes` — instance *(class: `Deserializer`)* -- `readUint32` — instance *(class: `Deserializer`)* -- `readUint64` — instance *(class: `Deserializer`)* -- `readValue` — instance *(class: `Deserializer`)* -- `releaseBuffer` — instance *(class: `Serializer`)* -- `serialize` — module -- `setDeserializeMainFunction` — instance *(class: `startupSnapshot`)* -- `setFlagsFromString` — module -- `setHeapSnapshotNearHeapLimit` — module -- `start` — instance *(class: `GCProfiler`)* -- `startCpuProfile` — module -- `stop` — instance *(class: `GCProfiler`)* ⚠ **stub** — report has the Node shape but the statistics array is always empty (#4916) -- `stopCoverage` — module -- `takeCoverage` — module -- `writeDouble` — instance *(class: `Serializer`)* -- `writeHeader` — instance *(class: `Serializer`)* -- `writeHeapSnapshot` — module -- `writeRawBytes` — instance *(class: `Serializer`)* -- `writeUint32` — instance *(class: `Serializer`)* -- `writeUint64` — instance *(class: `Serializer`)* -- `writeValue` — instance *(class: `Serializer`)* - -### Properties - -- `promiseHooks` -- `startupSnapshot` - -## `vm` - -### Classes - -- `Script` - -### Methods - -- `compileFunction` — module -- `createCachedData` — instance -- `createContext` — module -- `createScript` — module -- `dependencySpecifiers` — instance -- `error` — instance -- `evaluate` — instance -- `hasAsyncGraph` — instance -- `hasTopLevelAwait` — instance -- `identifier` — instance -- `instantiate` — instance -- `isContext` — module -- `link` — instance -- `linkRequests` — instance -- `measureMemory` — module -- `moduleRequests` — instance -- `namespace` — instance -- `runInContext` — module -- `runInNewContext` — module -- `runInThisContext` — module -- `setExport` — instance -- `status` — instance - -### Properties - -- `constants` -- `default` - -## `wasi` - -### Classes - -- `WASI` - -### Methods - -- `WASI` — module -- `finalizeBindings` — instance *(class: `WASI`)* -- `getImportObject` — instance *(class: `WASI`)* -- `initialize` — instance *(class: `WASI`)* -- `start` — instance *(class: `WASI`)* - -### Properties - -- `wasiImport` - -## `worker_threads` - -### Classes - -- `BroadcastChannel` -- `MessageChannel` -- `MessagePort` -- `Worker` - -### Methods - -- `BroadcastChannel` — module -- `MessageChannel` — module -- `addEventListener` — instance -- `cpuUsage` — instance *(class: `Worker`)* -- `getEnvironmentData` — module -- `getHeapSnapshot` — instance *(class: `Worker`)* -- `getHeapStatistics` — instance *(class: `Worker`)* -- `isMarkedAsUntransferable` — module -- `markAsUncloneable` — module -- `markAsUntransferable` — module -- `moveMessagePortToContext` — module -- `off` — instance *(class: `Worker`)* -- `on` — instance *(class: `Worker`)* -- `once` — instance *(class: `Worker`)* -- `postMessageToThread` — module -- `receiveMessageOnPort` — module -- `ref` — instance *(class: `Worker`)* -- `reload` — instance *(class: `Worker`)* -- `removeEventListener` — instance -- `setEnvironmentData` — module -- `startCpuProfile` — instance *(class: `Worker`)* -- `startHeapProfile` — instance *(class: `Worker`)* -- `terminate` — instance *(class: `Worker`)* -- `unref` — instance *(class: `Worker`)* - -### Properties - -- `SHARE_ENV` -- `isInternalThread` -- `isMainThread` -- `locks` -- `parentPort` -- `resourceLimits` -- `threadId` -- `threadName` -- `workerData` - -## `ws` - -### Classes - -- `Client` -- `WebSocket` -- `WebSocketServer` - -### Methods - -- `Server` — module -- `WebSocket` — module -- `addListener` — instance *(class: `Client`)* -- `address` — instance -- `clients` — instance -- `close` — instance -- `close` — instance *(class: `Client`)* -- `closeClient` — module -- `emit` — instance -- `handleUpgrade` — instance -- `on` — instance -- `on` — instance *(class: `Client`)* -- `ping` — instance -- `ping` — instance *(class: `Client`)* -- `pong` — instance -- `pong` — instance *(class: `Client`)* -- `readyState` — instance -- `send` — instance -- `send` — instance *(class: `Client`)* -- `sendToClient` — module -- `terminate` — instance -- `terminate` — instance *(class: `Client`)* - -### Properties - -- `CLOSED` -- `CLOSING` -- `CONNECTING` -- `OPEN` - -## `zlib` - -### Classes - -- `BrotliCompress` -- `BrotliCompress` -- `BrotliDecompress` -- `BrotliDecompress` -- `Deflate` -- `Deflate` -- `DeflateRaw` -- `DeflateRaw` -- `Gunzip` -- `Gunzip` -- `Gzip` -- `Gzip` -- `Inflate` -- `Inflate` -- `InflateRaw` -- `InflateRaw` -- `Unzip` -- `Unzip` -- `ZstdCompress` -- `ZstdDecompress` - -### Methods - -- `brotliCompress` — module -- `brotliCompressSync` — module -- `brotliDecompress` — module -- `brotliDecompressSync` — module -- `crc32` — module -- `createBrotliCompress` — module ⚠ **stub** — params/quality options accepted but ignored, warns once (#4917) -- `createBrotliDecompress` — module ⚠ **stub** — params/quality options accepted but ignored, warns once (#4917) -- `createDeflate` — module ⚠ **stub** — level honored; strategy/memLevel validated but not applied (#4917) -- `createDeflateRaw` — module ⚠ **stub** — level honored; strategy/memLevel validated but not applied (#4917) -- `createGunzip` — module -- `createGzip` — module ⚠ **stub** — level honored; strategy/memLevel validated but not applied (#4917) -- `createInflate` — module -- `createInflateRaw` — module -- `createUnzip` — module -- `createZstdCompress` — module ⚠ **stub** — params/quality options accepted but ignored, warns once (#4917) -- `createZstdDecompress` — module ⚠ **stub** — params/quality options accepted but ignored, warns once (#4917) -- `deflate` — module -- `deflateRaw` — module -- `deflateRawSync` — module -- `deflateSync` — module -- `gunzip` — module -- `gunzipSync` — module -- `gzip` — module -- `gzipSync` — module -- `inflate` — module -- `inflateRaw` — module -- `inflateRawSync` — module -- `inflateSync` — module -- `unzip` — module -- `unzipSync` — module -- `zstdCompress` — module -- `zstdCompressSync` — module -- `zstdDecompress` — module -- `zstdDecompressSync` — module - -### Properties - -- `codes` -- `constants` diff --git a/scripts/gc_runtime_root_holders.json b/scripts/gc_runtime_root_holders.json index fdb0a48865..96ef42ead4 100644 --- a/scripts/gc_runtime_root_holders.json +++ b/scripts/gc_runtime_root_holders.json @@ -343,7 +343,7 @@ "file": "crates/perry-runtime/src/gc/census.rs", "name": "PASS1_MARKED", "verdict": "non_moving_snapshot", - "why": "Real GC header addresses, deliberately untraced so the diagnostic does not keep its observed objects alive. Populated only at the end of mark propagation of a synchronous full cycle; consumed at sweep entry in the same run_to_completion invocation. The intervening full-cycle phases do not relocate or run JS callbacks. The Vec is used for membership comparisons and dropped with the census before sweep. Budgeted and minor cycles skip both boundaries. Pin re-audited 2026-09-05 after #9760 touched `gc/mod.rs`: that change is `mod heap_stats;` plus a `pub(crate) use` re-export and alters no mark/sweep control flow. `heap_stats()` is reached only from `js_bun_jsc_heap_stats` (the JS-facing `bun:jsc.heapStats()`), i.e. from mutator code, never inside a cycle, and its own module contract forbids allocation or collection during its walk. The mark-complete \u2192 sweep-entry window is unchanged. Re-audited 2026-09-05 (train125) after #9769 and #9771 touched pinned files. #9769 adds one `reg_scanner!` registration to `gc/mod.rs`; #9771 adds a feature-gated `alloc_census_init()` there and a feature-gated Rust-heap dump inside `take_census`. `alloc-census` is not in the default feature set, and decisively: `census_take_if_armed_at_full_sweep_start` does `PASS1_MARKED.with(|p| p.borrow_mut().take())` BEFORE calling `take_census`, so the snapshot has already left the thread-local by the time #9771's code runs \u2014 it cannot affect the window. Neither change alters mark/sweep control flow. Re-audited 2026-09-06 after #9831 touched `gc/policy.rs`. Its hunks are (a) the tiny-parse pressure guard's pricing (`tiny_parse_pressure_headroom_bytes`, `tiny_parse_pressure_due*`, a `Cell` byte-count base) consulted from JSON.parse's mutator-side boundaries (`gc_bump_malloc_trigger`, `gc_collect_pending_suppressed_parse`, `gc_schedule_parse_boundary_collection_if_pressure`), none of which is reachable from inside a cycle, and (b) one extra `Cell` store in `note_collection_finished_arena_occupancy`, which runs from `publish_reclaim_outcome` in the Publish subphase \u2014 after `step_sweep` has already consumed the snapshot. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Re-audited 2026-09-05 (train126) after #9755 restructured `gc/cycle.rs`. Its hunks are all root-scan machinery (`RootScanSubphase`, `RootScanCycleState`, the mutable-scanner iteration state), which runs BEFORE mark propagation completes; `gc/mod.rs` gains only a `mod young_log;` declaration. The bracketing is unchanged \u2014 `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep` \u2014 and a synchronous full mark-sweep still moves nothing between them. Re-pinned 2026-09-05 for the #9740 hot-TLS conversion of this file: the sole change is `thread_local!` \u2192 `crate::perry_thread_local!`, a macro-name swap with identical declaration syntax and `.with()` call sites. No control flow, no phase boundary, and no storage semantics change. Re-audited 2026-09-06 (train128) after #9794's GC diagnostics touched `gc/mod.rs` and `gc/policy.rs`: both gain diagnostic module declarations and counters only \u2014 no mark/sweep control flow, and the census bracketing in `step_mark_propagation` / `step_sweep` is unchanged. Re-audited for #9794's GC diagnostics: `gc/mod.rs` gains `mod diag_sites;` / `mod survival_diag;`, a re-export, a `diag_sites::full_started(...)` call at TRIGGER time (before mark propagation begins), and exit-time reporting. Nothing executes between mark-complete and sweep-entry, so the window is unchanged. Re-audited 2026-09-06 for the retained array-growth verifier fix: the cycle.rs change passes the existing non-copying evacuation verifier an explicit all-forwarded policy. That call remains in minor finalization, outside the synchronous full-cycle census window; its root and heap reads do not allocate GC objects, move objects, or invoke JS callbacks. The mark-complete and sweep-entry boundaries are unchanged. Re-audited 2026-09-05 after #9830 touched `gc/policy.rs`. That change is (a) six `thread_local! {` blocks rewritten as `crate::perry_thread_local! {` and (b) one `#[cfg(test)]` accessor listing the trigger path's hot-slot indices. The macro keeps the same storage, the same `.with()` at every read and write, and the same destructor registration (the teardown guard exists exactly when `needs_drop` holds, which is what `std::thread_local!` already decided); no value, predicate or branch in the file changes, so no mark or sweep control flow does. The one new behaviour is on a declaration's FIRST read: `HotKey::resolve_and_cache` takes a mutex and allocates a key through the GLOBAL allocator. Even if a first read landed inside this window it would be sound \u2014 the window's contract is that nothing relocates and no JS callback runs, and a mimalloc allocation does neither. `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`; the bracketing is untouched. Re-audited 2026-09-06 (train132) after #9860 and #9845 touched `gc/mod.rs`. Both hunks are re-export lists and nothing else: #9860 adds `idle_reclaim_elapsed_starts` / `IDLE_RECLAIM_REARM_MS`, and #9845 adds `owner_is_dead_copied_minor_from_space_of_type`. No mark or sweep control flow changes. #9845's substantive work sits in `gc/oldgen.rs` and `gc/copying.rs`, neither pinned: the copying-minor arm (`finalize_dead_copied_minor_from_space_regexps`) runs on a MINOR, which skips both census boundaries; the full-cycle arm (`collect_dead_registered_regexps_post_trace`, from `with_dead_collection_finalize`) walks the RegExp registry building a Vec of addresses \u2014 no GC allocation, no JS callback, so it cannot relocate the snapshot's subjects \u2014 and it is reached from the sweep body, i.e. AFTER `census_take_if_armed_at_full_sweep_start` has already `take()`n the snapshot out of the thread-local. The mark-complete -> sweep-entry window is unchanged. Re-audited 2026-09-07 for #9965 after 1ec9e0e8a touched `gc/cycle.rs` and `gc/mod.rs`: `gc/mod.rs:216-217` only declares and imports the failure-attribution module, while `gc/cycle.rs:1414-1417` reads the trigger and diagnostic counters immediately before evacuation verification inside `atomic_finalize_minor_prelude`. Full cycles bypass `MinorPrelude` at `gc/cycle.rs:1192-1196`; evacuation remains guarded by the minor-only context at `gc/cycle.rs:1330-1372`. The snapshot store remains at `gc/cycle.rs:963-964` after synchronous full marking, and its take remains at `gc/cycle.rs:1454-1457` before sweep. No new write, relocation, collection, or JS callback was added to that full-cycle interval, so the PASS1_MARKED window is unaffected. Re-audited 2026-09-07 for the regex census rows: all new work is in `take_census` after `census_take_if_armed_at_full_sweep_start` has taken PASS1_MARKED out of TLS; neither boundary nor the intervening cycle control flow changed. Re-audited 2026-09-08 (train144) after #9976 and #9977 touched pinned files. `gc/mod.rs` gains exactly three lines: `mod copying_phase;` and `mod regex_census;` (declarations) and one `reg_scanner!(regex::site_test::scan_roots_mut)` registration. A scanner registration adds a root SOURCE for the mutable-root walks; it does not move either census boundary and runs nowhere between them. `gc/census.rs` widens `side_tables()` to `pub(super)`, extends it with regex rows and adds a test module \u2014 all census REPORTING, which runs from the diagnostic dump, not inside a cycle. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Re-audited 2026-09-08 for #9849 JSON construction deferral. `gc/mod.rs` adds the `json_defer` module/re-export and a trusted-header layout helper used only by already-validated JSON emitters; neither changes or runs in collector phase control flow. `gc/policy.rs` adds JSON completion scheduling, construction-grace checks, and safepoint deferral predicates. These are called from mutator-side JSON allocation/output boundaries and ordinary safepoint entry; they do not alter `step_mark_propagation`, `step_sweep`, or invoke callbacks or relocation between the census boundaries. The mark-complete to sweep-entry window is unchanged. The follow-up adds a cfg(test)-only one-shot boolean for deterministic explicit-pressure fixtures; it is absent from production builds and cannot affect the census window. The first predicate read consumes it, so post-parse accounting exercises normal pricing. Re-audited 2026-09-09 for bounded tiny-JSON completion polling. The policy.rs changes split the mutator-side pending-parse check into an inlined empty fast path plus an outlined debt-service path, and amortize the mutator-side arena-pressure read across 64 bounded parse completions. Neither function is reachable from step_mark_propagation or step_sweep; neither census boundary nor the synchronous full-cycle interval between them changes. Re-audited 2026-09-09 for lazy JSON record batches: policy.rs only widens gc_budgeted_cycle_active visibility from pub(super) to pub(crate). Its body remains a read-only Cell query. The new caller is lazy_get materialization in the mutator; run_to_completion, step_mark_propagation, census snapshot consumption at step_sweep, and the synchronous non-moving window are unchanged. Re-audited 2026-09-09 for completed JSON-output debt: the added gc_service_json_output_sweep function calls the existing trigger check from a rooted mutator boundary and reports whether its malloc-count request remains due. It is not called from any census or collector phase; the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-09 for the JSON byte-debt carry: the same mutator-only service helper now distinguishes requests satisfied before its call from those satisfied by its trigger check. The added enum contains no payload, both count reads are scalar, and no census boundary or collector phase changed. Re-audited 2026-09-11 for #10055: gc/mod.rs only registers the weak UTF-16 index scanner during gc_init. It neither marks strings nor allocates GC objects or runs JS; offset vectors use the Rust allocator. The mark-complete to sweep-entry census window and cycle control flow are unchanged. Re-audited 2026-09-11 for #10054: gc/mod.rs adds only the trim-cache mutable-root scanner registration in gc_init. Its scanner visits two existing string slots without allocating or invoking JS. Root scanning still precedes mark completion, and neither census boundary nor the synchronous mark-complete to sweep-entry window changes. Re-audited 2026-09-11 for #10060: the census array classifier now reads the logical element start and bounds its scan by the remaining capacity. The helper only reads the existing GC/header words and performs pointer arithmetic; it cannot allocate, collect, or call JS. This classifier runs in take_census after PASS1_MARKED has been taken out of TLS. Neither census boundary nor the mark-complete to sweep-entry control flow changed. Re-audited for #8512: gc/mod.rs only enables the existing PTY mutable-root scanner on Windows; it changes no mark/sweep phase or census boundary. The scanner visits NaN-boxed slots without running JS callbacks. Re-audited 2026-09-12 for the single regular-expression engine: `gc/mod.rs` changes `mod prefetch;` to `pub(crate) mod prefetch;` so the RegExp owner-table walks can prefetch headers, a visibility change with no new call in collector control flow; `gc/census.rs` changes only its `#[cfg(test)]` `regex_census_tests` module, dropping assertions for the previous engine's cache rows. Neither boundary (`census_pass1_if_armed` in `step_mark_propagation`, `census_take_if_armed_at_full_sweep_start` in `step_sweep`) nor the synchronous mark-complete to sweep-entry interval changes. Re-audited 2026-09-13 after the #10169 fix touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` gains only `pub(crate) use` re-exports (`policy::note_young_leaf_born_old`, `policy::young_generation_holds_a_nursery`, `promote_in_place::{young_generation_measured_dying, young_generation_measured_retained}`, and cfg(test) survival seeders). `gc/policy.rs` gains a `Cell` thread-local (`GC_YOUNG_LEAF_BORN_OLD`, no pointer), its setter, a pure predicate over `copying_from_space_in_use_bytes` vs the base nursery cap, and a consumed-once branch at the top of `gc_budgeted_due_trigger` that may answer `YoungScavengeCap` ahead of `OldReclaim`. That branch decides WHICH collection a safepoint starts (a minor instead of a full); it runs before any cycle begins and never inside one, so the mark-complete \u2192 sweep-entry window of a synchronous full \u2014 where PASS1_MARKED is populated and consumed within one `run_to_completion` \u2014 is unchanged, and neither hunk adds an allocation, a JS callback, or a relocation to it. Re-audited 2026-09-13 for the heap generation (#10164 cross-call search positions): `gc/mod.rs` only declares `pub(crate) mod heap_generation;`. `gc/cycle.rs` wraps the `Sweep` and `Reclaim` arms of `GcCycleState::step` in a `HeapChange` scope and opens one inside `atomic_finalize_minor_prelude`'s evacuation branch (with a nested one around old-page defrag). Opening and closing a scope only increments two thread-local integer cells (`HEAP_GENERATION`, `OPEN_HEAP_CHANGES`); a first thread-local read may allocate a key through the global allocator, which neither relocates nor runs JS. The `Sweep` scope opens immediately before `step_sweep`, i.e. before `census_take_if_armed_at_full_sweep_start` takes PASS1_MARKED out of TLS, and adds no relocation, collection or JS callback to the synchronous mark-complete to sweep-entry window; the minor-prelude scope is unreachable from a full cycle, which bypasses `MinorPrelude`. Neither boundary nor the intervening control flow changed. Re-audited 2026-09-13 for #10182 block-granular reclamation, which touched `gc/cycle.rs`. Two hunks: (a) in the `RememberedSetRebuild` subphase of AtomicFinalize \u2014 INSIDE the window \u2014 the require-marked old-to-young rebuild is now constructed with `OldToYoungRememberedRebuildState::new_skipping`, whose cursor never enters blocks the census recorded as holding no reached, pinned or pre-marked object (`BlockCensus::unmarked_blocks`); computing that list reads `arena_block_snapshots()` and allocates one `Vec` through the global allocator. It visits a subset of the same objects the rebuild already walked (every skipped object would have been rejected as unmarked), and it neither allocates a GC object, relocates anything, nor runs a JS callback. (b) In `step_sweep`, `IncrementalSweepState::with_block_skip` runs after `census_take_if_armed_at_full_sweep_start` has already taken PASS1_MARKED out of TLS. Neither boundary moved and the synchronous mark-complete to sweep-entry interval gains no relocation, collection or callback. Re-audited 2026-09-11 for the startup memory profile: gc/mod.rs only retains the pre-main allocator-policy constructor in js_gc_init. The constructor applies process allocation options, without invoking GC or JS. No census boundary, collector phase, or mark-complete to sweep-entry control flow changed. Re-audited 2026-09-13 for #10179: census.rs only adds a native regex cache metadata row and its unit assertion; snapshot consumption and the full-cycle window are unchanged. Re-audited 2026-09-14 for the GC due-check fast path, which touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` only changes the safepoint re-exports: `gc_runtime_safepoint` becomes cfg(test) and `gc_runtime_safepoint_poll` is added. `gc/policy.rs`: the budgeted step returns a debt-free `GcStepReport` (debt is attached by the FFI and test entry points after the step returns) and moves cycle start/step into an out-of-line `gc_budgeted_start_or_step`; `gc_check_trigger` reuses a repeatable due-trigger answer through `DueTriggerMemo`, placed after its `GC_FLAG_IN_ALLOC` and suppression early returns; the young scavenge cap reuses the old-gen pressure value the due trigger already read and checks the census-seeded flag first. All of it runs from mutator safepoints, allocation-point trigger checks and the host step API, before a cycle starts or between budgeted steps. None of it is reachable between `census_pass1_if_armed` in `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` in `step_sweep` of a synchronous full: an allocation inside that window reaches `gc_check_trigger` with `GC_FLAG_IN_ALLOC` set and returns before the changed code. No allocation, relocation, collection or JS callback is added to the window. Re-audited 2026-09-14 for the tiny-parse nursery-cap boundary, which touched `gc/policy.rs`. It adds `tiny_parse_generational_collection_due`, a pure predicate (the existing `tiny_parse_pressure_due` OR the existing `young_scavenge_cap_due` read), and calls it instead of `tiny_parse_pressure_due` from `gc_bump_malloc_trigger_inner` and `gc_collect_pending_suppressed_parse_slow` (generational branch only) and from `gc_schedule_parse_boundary_collection_if_pressure`. All three are JSON.parse mutator-side boundaries, none reachable from `step_mark_propagation` or `step_sweep`; the predicate reads counters and allocates nothing. Neither census boundary nor the synchronous mark-complete to sweep-entry interval changed. Re-audited 2026-09-13 for #10182's full-collection throughput follow-up, which touched `gc/cycle.rs` in one hunk, INSIDE the window: the `RememberedSetRebuild` subphase of a synchronous full now first asks `verify::full_remembered_rebuild_provably_empty` and, when it holds, installs `OldToYoungRememberedRebuildState::provably_empty()` (an empty sticky set, no walk) instead of the require-marked rebuild. The predicate reads `arena_block_snapshots()` (one `Vec` through the global allocator), the census's per-block reached/pre-marked facts and the malloc registry's length; the constructor bumps a `Cell` counter and prints one line under `PERRY_GC_DIAG`. None of it allocates a GC object, relocates anything, collects, or runs a JS callback, and both census boundaries stay where they were. Re-audited 2026-09-14 for #10182's pacing-full work, which touched `gc/cycle.rs`, `gc/mod.rs` and `gc/policy.rs`. `gc/cycle.rs`: `GcCycleState::new_full` no longer calls `materialize_all_promoted_page_runs`; that call ran in the constructor, before the census and far before `census_pass1_if_armed`, and removing it adds nothing to the window. `gc/mod.rs`: one `mod promoted_cohort;` declaration. `gc/policy.rs`: (a) `credit_promoted_bytes_to_old_baseline` also credits a `Cell` cohort counter (it runs after a copying minor completes); (b) `finish_full_old_reclaim_baseline` also records the verified old live bytes and resets that counter (Publish, after `step_sweep` consumed the snapshot); (c) `gc_safepoint_moving_minor` arms and disarms the promotion-census record around its nursery minor and calls `run_promoted_cohort_full_if_due`, which starts a synchronous full through the same `gc_collect_full_mark_sweep_with_trigger` entry and reads byte counters before and after it. All of it runs before a cycle starts or after it completes; none of it runs between mark completion and sweep entry, allocates a GC object, relocates anything, or calls into JS. The census the promoted-cohort full may adopt from the promotion walk is built in `BuildValidPointerSet`, before either boundary. Both boundaries are unchanged. Re-audited 2026-09-14 for the #10182 dead-stack scrub in `gc/cycle.rs`: `step_build_valid_pointer_set` now calls `scrub_dead_stack_below`, which zeroes a local array in its own frame (dead stack below the caller), right after the census finishes \u2014 in `BuildValidPointerSet`, before the root scan and far before `census_pass1_if_armed`. It writes no heap memory, allocates nothing, relocates nothing and calls no JS; both boundaries are unchanged. Re-audited 2026-09-14 for #10241 (cohort survival), which touched `gc/cycle.rs` and `gc/policy.rs`. `gc/cycle.rs`: one call, `promoted_cohort::survival::check_minor_view_at_full_sweep_start()`, in `step_sweep` immediately AFTER `census_take_if_armed_at_full_sweep_start` has taken PASS1_MARKED out of TLS, i.e. outside the window. It is a no-op unless a promoted-cohort full armed its survival probe; when armed it walks the old page index over the preceding minor's dirty pages (`old_arena_walk_objects_on_pages`, Rust-allocator Vecs), reads GC headers' mark flags and the slots of unmarked ones, and records one enum. It writes no heap memory, allocates no GC object, relocates nothing and calls no JS. `gc/policy.rs`: `run_promoted_cohort_full_if_due` arms the probe before `gc_collect_full_mark_sweep_with_trigger` and takes it after the full returns (feeding `note_full_measured_promotion_survival` and one diagnostic line); both run before a cycle starts or after it completes. Both boundaries are unchanged. Re-audited 2026-09-14 for #10241's in-place-only cohort: `gc/policy.rs` drops the `promoted_cohort::note_promoted` call from `credit_promoted_bytes_to_old_baseline` (the copying minor now calls `promoted_cohort::note_minor_promotion` itself, after the credit). Both run at the end of a copying minor, outside any full cycle; the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-14 for the parse-boundary side-allocation band (medium-parse pacing), which touched `gc/policy.rs`. Three hunks: (a) a `Cell` thread-local (`GC_LAST_COLLECTION_EXTERNAL_SIDE_BYTES`, a byte COUNT, no pointer) plus three pure predicates over it and `external_side_live_bytes()`; (b) that predicate added as a third disjunct of `tiny_parse_generational_collection_due`, which is read only from the three JSON.parse mutator-side boundaries (`gc_bump_malloc_trigger_inner`, `gc_collect_pending_suppressed_parse_slow`, `gc_schedule_parse_boundary_collection_if_pressure`), none of them reachable from `step_mark_propagation` or `step_sweep`; and (c) one extra `Cell` store in `note_collection_finished_arena_occupancy` plus two extra reads in the `PERRY_GC_DIAG` tiny-parse line. `note_collection_finished_arena_occupancy` runs from `publish_reclaim_outcome` in the Publish subphase, i.e. AFTER `step_sweep` has already `take()`n the snapshot out of the thread-local, exactly as #9831's store on the same line does. Nothing added allocates a GC object, relocates anything, or runs a JS callback, and neither census boundary moved. Re-audited 2026-09-14 for the drained-bytes counterweight to that band, which touched `gc/policy.rs` again. Four hunks: a second `Cell` thread-local (`GC_EXTERNAL_SIDE_DRAINED_SINCE_FULL`, a byte COUNT); one increment of it inside `gc_note_external_side_free`; a pure read (`external_side_old_reclaim_pressure_bytes`) substituted for `external_side_live_bytes()` at the four old-reclaim pressure sites; and one `Cell` store at the top of `finish_full_old_reclaim_baseline`. None of it can run between the census boundaries. `gc_note_external_side_free` is also reached by mutator-side tape materialization, regex scratch teardown, native-addon adjustments and buffer replacement. Its added operation is only a saturating increment of a scalar Cell, with no GC allocation, relocation, collection or JS callback, so this wider caller set does not invalidate the census window. `finish_full_old_reclaim_baseline` runs from `publish_reclaim_outcome` in the Publish subphase, the same place #9831's store already sits. The pressure reads happen at trigger decisions, before a cycle starts. No allocation, relocation, collection or JS callback is added to the mark-complete -> sweep-entry window, and neither boundary moved. Re-audited 2026-09-15 for turnloop P0, which touched `gc/mod.rs` with one added call: `crate::event_pump::shutdown_wait_driver()` inside `js_gc_release_current_thread_collection_side_allocations`, the process-exit funnel. That function runs once no more JavaScript can run on the thread, never from inside a collection cycle; the added call drops the thread's turnloop wait loop (closing its kqueue/epoll descriptor) and may print a diagnostic line. It allocates no GC object, relocates nothing, starts no collection and runs no JS callback. The census boundaries and the mark-complete -> sweep-entry window are untouched.. Re-audited 2026-09-16 for the copying minor's per-parent weak-holder fact: `gc/mod.rs` gains exactly one line, `mod copying_parent_facts;`, a module declaration. The module it declares holds `weak_holder_fact` (a read of the parent's `obj_type`/`class_id` via `weakref::is_weak_holder_header`) and the copying minor's `visit_slot_with_parent`, moved verbatim out of `gc/copying.rs` for the 2000-line lint. Both run only inside a COPYING MINOR, which skips both census boundaries (`census_pass1_if_armed` in `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` in `step_sweep` are synchronous-full only). Nothing was added to any full-cycle phase, and the declaration itself executes no code. Neither boundary moved and the synchronous mark-complete to sweep-entry window gains no allocation, relocation, collection or JS callback. Re-audited 2026-09-18 for the #10532 follow-up argument-list rooting fix, which touched `gc/mod.rs`. The only change there is `mod collection_points;` plus a `pub(crate) use collection_points::collection_point;` re-export (and, under `#[cfg(test)]`, `arm_collection_point`). `collection_point` is an inline no-op outside `cfg(test)`; under test it only runs a copying minor when called from ordinary MUTATOR code (`proxy.rs`'s `Reflect.apply` rebind path and `registry.rs`'s rest-array bundler), never from inside `step_mark_propagation` or `step_sweep`. Neither `census_pass1_if_armed` nor `census_take_if_armed_at_full_sweep_start` is reachable from it, so the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-18 (same PR, round 2) for the added `arm_collection_point_after` re-export in `gc/mod.rs`: another pure re-export line, same as the `collection_point`/`arm_collection_point` one already covered above. `arm_collection_point_after` only changes test-only arming state in `collection_points.rs` (which named site fires and on which hit); it still runs no mark/sweep control flow. Re-audited 2026-09-19 for #10735 (require.main threading): gc/mod.rs gains exactly one line, `reg_scanner!(crate::module_require::scan_cjs_main_module_root_mut);`, registering the new CJS_MAIN_MODULE thread-local's mutable-root scanner beside the existing `scan_module_path_roots_mut` registration. A scanner registration adds a root SOURCE for the mutable-root walks; it runs during root scanning, before mark propagation completes, and does not execute between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start`. Neither census boundary moved and the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-20 for #10834 (inherited-property read cache). `gc/mod.rs` gains exactly one line: `reg_scanner!(crate::object::inherited_read_cache::scan_inherited_read_cache_roots_mut);` in `gc_init()`. A scanner registration adds a root SOURCE for the mutable-root walks. The walk runs inside `RootScanCycleState::step_current_subphase`, i.e. entirely within the RootScan phase: `step_root_scan` only sets `self.phase = GcCyclePhase::MarkPropagation` once that loop reports done (`gc/cycle.rs:958-961`), and `census_pass1_if_armed()` fires at the END of `step_mark_propagation` (`gc/cycle.rs:982`). The scanner therefore runs strictly BEFORE the window opens and can never execute between the boundaries. Its body is a bounded walk of a fixed 512-entry thread-local array calling `visit_tagged_usize_slot` / `visit_usize_slot`; it allocates nothing, relocates nothing and runs no JS callback. Same shape as #9769, #9976/#9977, #10054, #10055 and #10735, all previously cleared. The PR also adds an `INHERITED_READ_CACHE` entry to `DEAD_KEY_PRUNES` in `gc/dead_owner.rs` (not a pinned source). That registry is consumed by `IncrementalSweepState::with_dead_collection_finalize` at `gc/cycle.rs:1548`, which is AFTER `census_take_if_armed_at_full_sweep_start` at `gc/cycle.rs:1505` has already `take()`n the snapshot out of the thread-local -- the same argument that cleared #9845's `collect_dead_registered_regexps_post_trace`. The prune reads addresses and zeroes entries; no GC allocation, relocation or callback. Both additions sit outside the window, on opposite sides of it. Neither boundary moved and the synchronous mark-complete to sweep-entry interval is unchanged. Re-audited 2026-09-22 for #10399 (per-thread module init), which touched `gc/mod.rs`. Two hunks, both init-time: a new free function `raise_default_thread_stack_floor()` and one call to it at the top of `js_gc_init`, before `enter_current_thread_image`'s successor statements. The function reads `RUST_MIN_STACK` from the environment and, only when it is unset, sets it to 32 MiB so a thread spawned against a multi-megabyte static TLS block still has usable stack (glibc carves static TLS out of the thread's stack mapping). It touches no heap object, allocates no GC object, relocates nothing and runs no JS callback. `js_gc_init` is the first runtime call of a compiled `main`, so it runs once before any cycle exists, and it is not reachable from `step_mark_propagation` or `step_sweep`. Same shape as the 2026-09-11 startup-memory-profile re-audit, which cleared the pre-main allocator-policy constructor in the same function. Neither census boundary moved and the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-23 (size/runtime-decouple, #11135) after the binary-size branch touched `gc/census.rs`, `gc/mod.rs` and `gc/policy.rs`. census.rs: `census_pass1_if_armed` / `census_take_if_armed_at_full_sweep_start` keep their bodies verbatim, moved into `_impl` functions compiled only with the new `gc-instruments` feature (without it both are empty and `census_path()` is `None`, so nothing is ever armed); the take still empties PASS1_MARKED before `take_census`. gc/mod.rs: `gc_init` gains a startup env check that aborts when an instrument knob is set without the feature, before any cycle exists. gc/policy.rs: env-knob OnceLock caches now initialize through `crate::once_init::get_or_init` (same closures, same values). No mark/sweep control flow between the two census boundaries changed; the window is unchanged. Re-audited for Fetch handle reclamation: cycle.rs only redirects the incomplete-cycle Drop cancellation hook to also cancel the Fetch trace. The full-trace finish hook removes native records and cached slots without allocating GC objects or invoking JS; it cannot relocate the census addresses before sweep entry. Re-audited 2026-09-22 for #10928 (one proportional old-reclaim rule), which touched `gc/policy.rs`. Six hunks. (a) Two new thread-locals, `GC_OLD_RECLAIM_PRE_IN_USE_BYTES` (`Cell`) and `GC_OLD_RECLAIM_BACKOFF_SHIFT` (`Cell`): both are byte/shift COUNTS, neither holds a pointer. (b) `gc_old_reclaim_growth_band_bytes` gains a `Cell` read and a left shift -- pure arithmetic over byte counts. (c) `old_reclaim_pressure_due` loses the #7937 absolute first-crossing arm, splits its pure form out as `old_reclaim_pressure_due_inner`, and calls `note_old_reclaim_cycle_started()` when the answer is true. That predicate is read at TRIGGER decisions only -- the allocation-point `gc_check_trigger` and `gc_budgeted_due_trigger` at safepoints -- i.e. before a cycle starts, never between the boundaries; an allocation inside the window reaches `gc_check_trigger` with `GC_FLAG_IN_ALLOC` set and returns before this code, the same argument the 2026-09-14 due-check fast-path re-audit made for the same function. Even if it did run there it would be sound: `note_old_reclaim_cycle_started` stores one scalar `Cell` from `pacing_arena_in_use_bytes()` (a read of `arena_live_allocated_bytes`), which allocates no GC object, relocates nothing and runs no JS callback -- the window's contract. (d) `update_old_reclaim_backoff` is called only from `finish_full_old_reclaim_baseline`, which runs from `publish_reclaim_outcome` in the Publish subphase, AFTER `step_sweep` has already `take()`n the snapshot out of the thread-local -- exactly where #9831's store and the medium-parse pacing store already sit. (e) `gc_old_reclaim_debt_bytes` drops the absolute arm it mirrored; it remains pure arithmetic read at debt/trigger decisions. (f) `#[cfg(test)]` seams, absent from production builds. `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback between them; the change alters only WHEN a collection is scheduled, never what runs inside one. Neither boundary moved and the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-24 for #10960 (growth-aware old-reclaim backoff), which touched `gc/policy.rs` again. One new thread-local, `GC_OLD_RECLAIM_LAST_POST_IN_USE_BYTES` (`Cell`), a byte COUNT that holds no pointer. It is written only by `update_old_reclaim_backoff`, which runs from `finish_full_old_reclaim_baseline` in the Publish subphase, after `step_sweep` has already taken the snapshot out of the thread-local; the change there is pure integer arithmetic deciding whether to widen the band. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback, and neither window boundary moved. Re-audited 2026-09-24 after the class-capture environment added one `reg_scanner!` registration (`scan_class_env_roots_mut`, visiting each guarded class environment's owner class object) to `gc/mod.rs`: a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-25 after the inherited-access lane touched `gc/mod.rs`: the change is one `reg_scanner!` registration for `object::chain_store::scan_chain_store_roots_mut`, a root scanner that visits store-site chain verdicts (one interned key pointer each) during root scanning. It runs at the start of a cycle, never between mark completion and sweep entry, relocates nothing and runs no JS callback. The synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-25 for #10698 (allocation-point trigger watermark), which touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` gains `pub(crate) mod trigger_watermark;` and a `pub(crate) use` re-export -- no control flow. In `gc/policy.rs`: (a) `gc_check_trigger` becomes an out-of-line wrapper over an inlined fast path that reads the watermark cell, the malloc registry's length through an unguarded shared borrow and the inline allocator's offset, then either returns or runs the previous body unchanged (`gc_check_trigger_evaluate`). An allocation inside the window reaches it with `GC_FLAG_IN_ALLOC` set: the fast path returns without acting, which is the outcome of the `GC_FLAG_IN_ALLOC` early return it would otherwise reach, and the slow path still takes that early return. (b) The due-trigger evaluation also returns a watermark -- integer arithmetic over values it already read -- published only on the no-action return past every entry guard. (c) The ladder's thresholds and flags are retyped `Cell` -> `TriggerInput`, whose writes add one store retiring the watermark; heap-generation advances and `Arena::set_current` retire it too. (d) `#[cfg(test)]` seams. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback; `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`, so the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-25 for #10498 (class-accessor cache), which adds one `reg_scanner!` registration (`scan_class_accessor_cache_roots_mut`, marking and rewriting the cache's key strings) to `gc/mod.rs`: again a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-26 for #10572: `gc/mod.rs` gains one `#[cfg(not(feature = \"hot-diag\"))]` call to `hot_diag::refuse_knobs_without_hot_diag()` in `gc_init`, a startup-only knob check that runs before any cycle and alters no mark/sweep control flow; the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-27 for #11416: `gc/mod.rs` loses the `reg_scanner!` registration for `class_accessor_cache::scan_class_accessor_cache_roots_mut` (the cache is deleted); removing a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 for the method-calls lane, which adds one `reg_scanner!` registration (`scan_method_site_roots_mut`, marking and rewriting the method closures inherited method-site entries hold) to `gc/mod.rs`: a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-28 (charter step 5, P0): the field-representation census adds `mod census_field_repr;` to `gc/mod.rs` and, in `census.rs`, one Rust-owned accumulator fed from `visit_object` inside `take_census`, which runs after `census_take_if_armed_at_full_sweep_start` has already taken PASS1_MARKED; it reads slots and the per-object layout maps (try_borrow), allocates nothing on the JS heap and alters no mark/sweep control flow. The window is unchanged. Re-audited 2026-09-27 for #11416: `gc/mod.rs` loses the `reg_scanner!` registration for `class_accessor_cache::scan_class_accessor_cache_roots_mut` (the cache is deleted); removing a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 for the method-calls lane, which adds one `reg_scanner!` registration (`scan_method_site_roots_mut`, marking and rewriting the method closures inherited method-site entries hold) to `gc/mod.rs`: a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 (class constructors as function objects, #11414): `gc/mod.rs` gains one `reg_scanner!` registration (`object::class_value::scan_class_value_roots_mut`, the per-agent class function-object table, visited and rewritten) \u2014 a root-scanner registration that alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-29 after #11659 added `verify::verify_array_hole_tails_at_collection()` to `gc/mod.rs` and `gc/policy.rs`. Every call sits at a collection or budgeted-cycle ENTRY, beside `roots::ensure_stack_maps_built()`, i.e. before mark begins and so before `census_pass1_if_armed` opens the window. The call is a read-only walk of array headers (debug builds, or release with PERRY_GC_VERIFY_ARRAY_HOLES): it neither allocates GC memory, relocates, runs JS, nor reads or writes PASS1_MARKED, and it panics rather than continuing on a violation. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Pin re-audited 2026-09-29 after PR #11646 added one call inside that window, in `cycle.rs` beside `census_take_if_armed_at_full_sweep_start`: `object::shapes::store_kind::audit_heap_at_full_sweep_start()`. It compiles to nothing unless the `shape-fact-audit` feature is on; when on it walks the arena read-only (`gc::for_each_live_object_at_sweep_start`, in `gc/verify.rs`) and reads each marked object header and its shape record. It allocates no GC object, moves nothing, runs no JS callback and holds no address past the walk, so PASS1_MARKED stays valid across it; a disagreement panics rather than continuing. Re-audited 2026-09-29 after the pinned-roots fix touched `gc/cycle.rs` and `gc/mod.rs`: the block-persistence live-block predicate now reads GC_FLAG_MARKED alone (pinned objects are marked as roots, so the set of live blocks is unchanged), and `gc/mod.rs` adds one `reg_scanner!` registration. Neither relocates, runs JS, or moves the mark-complete to sweep-entry window. Re-audited again 2026-09-29 (same fix, copying-minor follow-up): `BlockPersistCycleState`'s force-mark in `gc/cycle.rs` no longer skips an unmarked pinned header, so it marks and pushes it like any other object of a live block. That is marking inside the budgeted cycle's persistence step: nothing relocates, no JS runs, and the budgeted path still skips both census boundaries. The window is unchanged. Re-audited 2026-09-29 for this-as-a-parameter stage 3: `gc/mod.rs` registers `scan_dispatch_binding_roots_mut` in place of `scan_implicit_this_roots_mut` (the implicit-`this` cell is deleted; the same scanner body keeps `new.target`, the static-`this` override and the static private-owner stack) and rewords its comment; a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-28 for the JsFunctionInfo lane: `gc/census.rs`'s `side_tables()` loses its `closure_registry_census()` row (the closure-body registry is deleted); a census report row, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-29 for the read-site holder entry: `gc/mod.rs` adds one `reg_scanner!` registration, `read_holder::scan_read_holder_roots_mut`, which visits the holder and hop words of registered read-site caches during ROOT SCAN, before mark propagation completes. It rewrites root slots only through the visitor (as every registered scanner does), runs no JS, and nothing it does executes between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start`. The window is unchanged. Re-audited 2026-09-30 for Step 5 P4: census.rs only removes the typed-layout count, and gc/mod.rs removes shape-install registration/re-export and an init-time typed-intact verifier gate. The snapshot remains stored after mark propagation and taken at sweep entry; none of these edits relocates objects or invokes callbacks in that interval. Re-audited 2026-09-29 (#11549 trace cost): `gc/mod.rs` gains one `mod copying_object_scan;` declaration, the copying minor's plain-object slot walk. It is reached only from the copying drain, never from a full cycle, and changes no mark or sweep control flow. The window is unchanged. Re-audited 2026-09-30 after rebasing P4 over #11549/#11676: gc/mod.rs retains the copying_object_scan declaration and P4 removes typed-layout registration; both are module wiring outside the synchronous full mark-complete to sweep-entry window. The census boundaries and the window owner in cycle.rs are unchanged. Re-audited 2026-09-30 for the scope-context change: gc/mod.rs only removes the box root-scanner registration and exit-time box statistics. The former ran during root scan, before mark completion, and the latter runs at process exit. Neither changes the synchronous mark-complete to sweep-entry window. Re-audited 2026-09-28 for #11605 (link-time runtime feature installs): `gc/mod.rs` registers the interpreter root scanner through its always-present forwarder (`dyn_eval_hooks::scan_dyn_eval_roots_mut`, which calls the real scanner through a slot the `dyn-eval` install fills) instead of a feature-gated direct registration, and `js_gc_init` ends by running the program\u2019s feature installer, which only stores fn pointers into `Hook` slots at startup, before any user code or collection. Neither alters mark/sweep control flow or runs inside the mark-complete to sweep-entry window. Re-audited 2026-10-01 after main integration: the pinned census, cycle, policy and progress files are byte-identical to current main. The only gc/mod.rs delta replaces the interpreter scanner registration with its installed-slot forwarder and invokes the installer at js_gc_init startup, before user code and any collection. No change executes between mark completion and sweep entry. Re-audited 2026-09-28 for the release-runtime instrument strip: every changed line in `gc/census.rs`, `gc/cycle.rs`, `gc/mod.rs` and `gc/policy.rs` renames a `feature = \"diagnostics\" | \"gc-instruments\" | \"hot-diag\"` gate to the build-script cfg `perry_diagnostics` / `perry_gc_instruments` / `perry_hot_diag`, which `perry-runtime/build.rs` sets exactly when the feature is on unless PERRY_RELEASE_STRIP_INSTRUMENTS=1. With the cfg set the compiled code is unchanged; with it unset (release packages) the census that fills this snapshot is not compiled, so the window never opens. No mark/sweep control flow changes. Re-audited 2026-10-01 while integrating the repaired #11605 parent: all five pinned files are byte-identical to that reviewed parent after normalizing only the diagnostics/instrument cfg names; its current census_field_repr module and startup installer hooks remain intact. The synchronous mark-complete to sweep-entry window is unchanged. Re-audited for A2 inherited-read-cache deletion: gc/mod.rs removes only its old root-scanner registration and adjacent comments. The method-site and read-holder scanners still run during root scan before mark completion; no collection phase or callback was added inside the mark-complete to sweep-entry window. Re-audited 2026-09-30 for the setter-site root scanner: gc/mod.rs adds only its reg_scanner! registration in the root-scan setup. That scanner visits per-site key and holder slots before mark propagation completes; it runs no JS and adds no relocation or callback between census pass1 and sweep entry. The synchronous-full snapshot window and both boundaries remain unchanged. Re-audited 2026-10-01 against main a8f4f3dd76: census.rs, cycle.rs, policy.rs and progress.rs are byte-identical to reviewed #11629 head 85e0fe18ec. The only pinned-file delta in gc/mod.rs removes the deleted inherited-read-cache scanner, retains chain-store/method-site/read-holder scanners, and registers the setter-site scanner during gc_init. These registrations run during root scanning before mark completion. Both link-time feature installers and release instrument cfgs are preserved; neither census boundary nor the non-moving synchronous-full interval changes. Re-audited 2026-09-28 for #11605 (link-time runtime feature installs): `gc/mod.rs` registers the interpreter root scanner through its always-present forwarder (`dyn_eval_hooks::scan_dyn_eval_roots_mut`, which calls the real scanner through a slot the `dyn-eval` install fills) instead of a feature-gated direct registration, and `js_gc_init` ends by running the program\u2019s feature installer, which only stores fn pointers into `Hook` slots at startup, before any user code or collection. Neither alters mark/sweep control flow or runs inside the mark-complete to sweep-entry window. Re-audited 2026-10-01 after main integration: the pinned census, cycle, policy and progress files are byte-identical to current main. The only gc/mod.rs delta replaces the interpreter scanner registration with its installed-slot forwarder and invokes the installer at js_gc_init startup, before user code and any collection. No change executes between mark completion and sweep entry. Re-audited 2026-10-01 while integrating main 7b5912d4e7 into #11605: census.rs, cycle.rs, policy.rs and progress.rs are byte-identical to that main. The only gc/mod.rs differences register the existing interpreter root scanner through its installed-slot forwarder and run the selected feature installer at js_gc_init startup before user code. The current main method/read-holder/setter-site scanners are preserved. No work was added between mark completion and sweep entry; the synchronous non-moving snapshot window is unchanged. Re-audited 2026-10-02 for the SPECIAL ConstFn verifier: gc/mod.rs adds only a feature-gated forwarding-helper re-export. The helper follows existing validated forwarding and runs no JS, allocation or collection. No holder, scanner registration or mark/sweep control flow is added; both census boundaries and their synchronous window remain unchanged. Re-audited 2026-10-03 for the prototype-in-shape lane: gc/mod.rs adds one reg_scanner! registration, scan_shape_prototype_words_mut, which visits the shape records' prototype words and their identity-index values as strong roots during root scanning (before mark propagation). It runs no JS, performs no GC allocation or collection, and adds no relocation or callback between census pass1 and sweep entry; both census boundaries and their synchronous window remain unchanged.", + "why": "Real GC header addresses, deliberately untraced so the diagnostic does not keep its observed objects alive. Populated only at the end of mark propagation of a synchronous full cycle; consumed at sweep entry in the same run_to_completion invocation. The intervening full-cycle phases do not relocate or run JS callbacks. The Vec is used for membership comparisons and dropped with the census before sweep. Budgeted and minor cycles skip both boundaries. Pin re-audited 2026-09-05 after #9760 touched `gc/mod.rs`: that change is `mod heap_stats;` plus a `pub(crate) use` re-export and alters no mark/sweep control flow. `heap_stats()` is reached only from `js_bun_jsc_heap_stats` (the JS-facing `bun:jsc.heapStats()`), i.e. from mutator code, never inside a cycle, and its own module contract forbids allocation or collection during its walk. The mark-complete \u2192 sweep-entry window is unchanged. Re-audited 2026-09-05 (train125) after #9769 and #9771 touched pinned files. #9769 adds one `reg_scanner!` registration to `gc/mod.rs`; #9771 adds a feature-gated `alloc_census_init()` there and a feature-gated Rust-heap dump inside `take_census`. `alloc-census` is not in the default feature set, and decisively: `census_take_if_armed_at_full_sweep_start` does `PASS1_MARKED.with(|p| p.borrow_mut().take())` BEFORE calling `take_census`, so the snapshot has already left the thread-local by the time #9771's code runs \u2014 it cannot affect the window. Neither change alters mark/sweep control flow. Re-audited 2026-09-06 after #9831 touched `gc/policy.rs`. Its hunks are (a) the tiny-parse pressure guard's pricing (`tiny_parse_pressure_headroom_bytes`, `tiny_parse_pressure_due*`, a `Cell` byte-count base) consulted from JSON.parse's mutator-side boundaries (`gc_bump_malloc_trigger`, `gc_collect_pending_suppressed_parse`, `gc_schedule_parse_boundary_collection_if_pressure`), none of which is reachable from inside a cycle, and (b) one extra `Cell` store in `note_collection_finished_arena_occupancy`, which runs from `publish_reclaim_outcome` in the Publish subphase \u2014 after `step_sweep` has already consumed the snapshot. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Re-audited 2026-09-05 (train126) after #9755 restructured `gc/cycle.rs`. Its hunks are all root-scan machinery (`RootScanSubphase`, `RootScanCycleState`, the mutable-scanner iteration state), which runs BEFORE mark propagation completes; `gc/mod.rs` gains only a `mod young_log;` declaration. The bracketing is unchanged \u2014 `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep` \u2014 and a synchronous full mark-sweep still moves nothing between them. Re-pinned 2026-09-05 for the #9740 hot-TLS conversion of this file: the sole change is `thread_local!` \u2192 `crate::perry_thread_local!`, a macro-name swap with identical declaration syntax and `.with()` call sites. No control flow, no phase boundary, and no storage semantics change. Re-audited 2026-09-06 (train128) after #9794's GC diagnostics touched `gc/mod.rs` and `gc/policy.rs`: both gain diagnostic module declarations and counters only \u2014 no mark/sweep control flow, and the census bracketing in `step_mark_propagation` / `step_sweep` is unchanged. Re-audited for #9794's GC diagnostics: `gc/mod.rs` gains `mod diag_sites;` / `mod survival_diag;`, a re-export, a `diag_sites::full_started(...)` call at TRIGGER time (before mark propagation begins), and exit-time reporting. Nothing executes between mark-complete and sweep-entry, so the window is unchanged. Re-audited 2026-09-06 for the retained array-growth verifier fix: the cycle.rs change passes the existing non-copying evacuation verifier an explicit all-forwarded policy. That call remains in minor finalization, outside the synchronous full-cycle census window; its root and heap reads do not allocate GC objects, move objects, or invoke JS callbacks. The mark-complete and sweep-entry boundaries are unchanged. Re-audited 2026-09-05 after #9830 touched `gc/policy.rs`. That change is (a) six `thread_local! {` blocks rewritten as `crate::perry_thread_local! {` and (b) one `#[cfg(test)]` accessor listing the trigger path's hot-slot indices. The macro keeps the same storage, the same `.with()` at every read and write, and the same destructor registration (the teardown guard exists exactly when `needs_drop` holds, which is what `std::thread_local!` already decided); no value, predicate or branch in the file changes, so no mark or sweep control flow does. The one new behaviour is on a declaration's FIRST read: `HotKey::resolve_and_cache` takes a mutex and allocates a key through the GLOBAL allocator. Even if a first read landed inside this window it would be sound \u2014 the window's contract is that nothing relocates and no JS callback runs, and a mimalloc allocation does neither. `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`; the bracketing is untouched. Re-audited 2026-09-06 (train132) after #9860 and #9845 touched `gc/mod.rs`. Both hunks are re-export lists and nothing else: #9860 adds `idle_reclaim_elapsed_starts` / `IDLE_RECLAIM_REARM_MS`, and #9845 adds `owner_is_dead_copied_minor_from_space_of_type`. No mark or sweep control flow changes. #9845's substantive work sits in `gc/oldgen.rs` and `gc/copying.rs`, neither pinned: the copying-minor arm (`finalize_dead_copied_minor_from_space_regexps`) runs on a MINOR, which skips both census boundaries; the full-cycle arm (`collect_dead_registered_regexps_post_trace`, from `with_dead_collection_finalize`) walks the RegExp registry building a Vec of addresses \u2014 no GC allocation, no JS callback, so it cannot relocate the snapshot's subjects \u2014 and it is reached from the sweep body, i.e. AFTER `census_take_if_armed_at_full_sweep_start` has already `take()`n the snapshot out of the thread-local. The mark-complete -> sweep-entry window is unchanged. Re-audited 2026-09-07 for #9965 after 1ec9e0e8a touched `gc/cycle.rs` and `gc/mod.rs`: `gc/mod.rs:216-217` only declares and imports the failure-attribution module, while `gc/cycle.rs:1414-1417` reads the trigger and diagnostic counters immediately before evacuation verification inside `atomic_finalize_minor_prelude`. Full cycles bypass `MinorPrelude` at `gc/cycle.rs:1192-1196`; evacuation remains guarded by the minor-only context at `gc/cycle.rs:1330-1372`. The snapshot store remains at `gc/cycle.rs:963-964` after synchronous full marking, and its take remains at `gc/cycle.rs:1454-1457` before sweep. No new write, relocation, collection, or JS callback was added to that full-cycle interval, so the PASS1_MARKED window is unaffected. Re-audited 2026-09-07 for the regex census rows: all new work is in `take_census` after `census_take_if_armed_at_full_sweep_start` has taken PASS1_MARKED out of TLS; neither boundary nor the intervening cycle control flow changed. Re-audited 2026-09-08 (train144) after #9976 and #9977 touched pinned files. `gc/mod.rs` gains exactly three lines: `mod copying_phase;` and `mod regex_census;` (declarations) and one `reg_scanner!(regex::site_test::scan_roots_mut)` registration. A scanner registration adds a root SOURCE for the mutable-root walks; it does not move either census boundary and runs nowhere between them. `gc/census.rs` widens `side_tables()` to `pub(super)`, extends it with regex rows and adds a test module \u2014 all census REPORTING, which runs from the diagnostic dump, not inside a cycle. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Re-audited 2026-09-08 for #9849 JSON construction deferral. `gc/mod.rs` adds the `json_defer` module/re-export and a trusted-header layout helper used only by already-validated JSON emitters; neither changes or runs in collector phase control flow. `gc/policy.rs` adds JSON completion scheduling, construction-grace checks, and safepoint deferral predicates. These are called from mutator-side JSON allocation/output boundaries and ordinary safepoint entry; they do not alter `step_mark_propagation`, `step_sweep`, or invoke callbacks or relocation between the census boundaries. The mark-complete to sweep-entry window is unchanged. The follow-up adds a cfg(test)-only one-shot boolean for deterministic explicit-pressure fixtures; it is absent from production builds and cannot affect the census window. The first predicate read consumes it, so post-parse accounting exercises normal pricing. Re-audited 2026-09-09 for bounded tiny-JSON completion polling. The policy.rs changes split the mutator-side pending-parse check into an inlined empty fast path plus an outlined debt-service path, and amortize the mutator-side arena-pressure read across 64 bounded parse completions. Neither function is reachable from step_mark_propagation or step_sweep; neither census boundary nor the synchronous full-cycle interval between them changes. Re-audited 2026-09-09 for lazy JSON record batches: policy.rs only widens gc_budgeted_cycle_active visibility from pub(super) to pub(crate). Its body remains a read-only Cell query. The new caller is lazy_get materialization in the mutator; run_to_completion, step_mark_propagation, census snapshot consumption at step_sweep, and the synchronous non-moving window are unchanged. Re-audited 2026-09-09 for completed JSON-output debt: the added gc_service_json_output_sweep function calls the existing trigger check from a rooted mutator boundary and reports whether its malloc-count request remains due. It is not called from any census or collector phase; the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-09 for the JSON byte-debt carry: the same mutator-only service helper now distinguishes requests satisfied before its call from those satisfied by its trigger check. The added enum contains no payload, both count reads are scalar, and no census boundary or collector phase changed. Re-audited 2026-09-11 for #10055: gc/mod.rs only registers the weak UTF-16 index scanner during gc_init. It neither marks strings nor allocates GC objects or runs JS; offset vectors use the Rust allocator. The mark-complete to sweep-entry census window and cycle control flow are unchanged. Re-audited 2026-09-11 for #10054: gc/mod.rs adds only the trim-cache mutable-root scanner registration in gc_init. Its scanner visits two existing string slots without allocating or invoking JS. Root scanning still precedes mark completion, and neither census boundary nor the synchronous mark-complete to sweep-entry window changes. Re-audited 2026-09-11 for #10060: the census array classifier now reads the logical element start and bounds its scan by the remaining capacity. The helper only reads the existing GC/header words and performs pointer arithmetic; it cannot allocate, collect, or call JS. This classifier runs in take_census after PASS1_MARKED has been taken out of TLS. Neither census boundary nor the mark-complete to sweep-entry control flow changed. Re-audited for #8512: gc/mod.rs only enables the existing PTY mutable-root scanner on Windows; it changes no mark/sweep phase or census boundary. The scanner visits NaN-boxed slots without running JS callbacks. Re-audited 2026-09-12 for the single regular-expression engine: `gc/mod.rs` changes `mod prefetch;` to `pub(crate) mod prefetch;` so the RegExp owner-table walks can prefetch headers, a visibility change with no new call in collector control flow; `gc/census.rs` changes only its `#[cfg(test)]` `regex_census_tests` module, dropping assertions for the previous engine's cache rows. Neither boundary (`census_pass1_if_armed` in `step_mark_propagation`, `census_take_if_armed_at_full_sweep_start` in `step_sweep`) nor the synchronous mark-complete to sweep-entry interval changes. Re-audited 2026-09-13 after the #10169 fix touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` gains only `pub(crate) use` re-exports (`policy::note_young_leaf_born_old`, `policy::young_generation_holds_a_nursery`, `promote_in_place::{young_generation_measured_dying, young_generation_measured_retained}`, and cfg(test) survival seeders). `gc/policy.rs` gains a `Cell` thread-local (`GC_YOUNG_LEAF_BORN_OLD`, no pointer), its setter, a pure predicate over `copying_from_space_in_use_bytes` vs the base nursery cap, and a consumed-once branch at the top of `gc_budgeted_due_trigger` that may answer `YoungScavengeCap` ahead of `OldReclaim`. That branch decides WHICH collection a safepoint starts (a minor instead of a full); it runs before any cycle begins and never inside one, so the mark-complete \u2192 sweep-entry window of a synchronous full \u2014 where PASS1_MARKED is populated and consumed within one `run_to_completion` \u2014 is unchanged, and neither hunk adds an allocation, a JS callback, or a relocation to it. Re-audited 2026-09-13 for the heap generation (#10164 cross-call search positions): `gc/mod.rs` only declares `pub(crate) mod heap_generation;`. `gc/cycle.rs` wraps the `Sweep` and `Reclaim` arms of `GcCycleState::step` in a `HeapChange` scope and opens one inside `atomic_finalize_minor_prelude`'s evacuation branch (with a nested one around old-page defrag). Opening and closing a scope only increments two thread-local integer cells (`HEAP_GENERATION`, `OPEN_HEAP_CHANGES`); a first thread-local read may allocate a key through the global allocator, which neither relocates nor runs JS. The `Sweep` scope opens immediately before `step_sweep`, i.e. before `census_take_if_armed_at_full_sweep_start` takes PASS1_MARKED out of TLS, and adds no relocation, collection or JS callback to the synchronous mark-complete to sweep-entry window; the minor-prelude scope is unreachable from a full cycle, which bypasses `MinorPrelude`. Neither boundary nor the intervening control flow changed. Re-audited 2026-09-13 for #10182 block-granular reclamation, which touched `gc/cycle.rs`. Two hunks: (a) in the `RememberedSetRebuild` subphase of AtomicFinalize \u2014 INSIDE the window \u2014 the require-marked old-to-young rebuild is now constructed with `OldToYoungRememberedRebuildState::new_skipping`, whose cursor never enters blocks the census recorded as holding no reached, pinned or pre-marked object (`BlockCensus::unmarked_blocks`); computing that list reads `arena_block_snapshots()` and allocates one `Vec` through the global allocator. It visits a subset of the same objects the rebuild already walked (every skipped object would have been rejected as unmarked), and it neither allocates a GC object, relocates anything, nor runs a JS callback. (b) In `step_sweep`, `IncrementalSweepState::with_block_skip` runs after `census_take_if_armed_at_full_sweep_start` has already taken PASS1_MARKED out of TLS. Neither boundary moved and the synchronous mark-complete to sweep-entry interval gains no relocation, collection or callback. Re-audited 2026-09-11 for the startup memory profile: gc/mod.rs only retains the pre-main allocator-policy constructor in js_gc_init. The constructor applies process allocation options, without invoking GC or JS. No census boundary, collector phase, or mark-complete to sweep-entry control flow changed. Re-audited 2026-09-13 for #10179: census.rs only adds a native regex cache metadata row and its unit assertion; snapshot consumption and the full-cycle window are unchanged. Re-audited 2026-09-14 for the GC due-check fast path, which touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` only changes the safepoint re-exports: `gc_runtime_safepoint` becomes cfg(test) and `gc_runtime_safepoint_poll` is added. `gc/policy.rs`: the budgeted step returns a debt-free `GcStepReport` (debt is attached by the FFI and test entry points after the step returns) and moves cycle start/step into an out-of-line `gc_budgeted_start_or_step`; `gc_check_trigger` reuses a repeatable due-trigger answer through `DueTriggerMemo`, placed after its `GC_FLAG_IN_ALLOC` and suppression early returns; the young scavenge cap reuses the old-gen pressure value the due trigger already read and checks the census-seeded flag first. All of it runs from mutator safepoints, allocation-point trigger checks and the host step API, before a cycle starts or between budgeted steps. None of it is reachable between `census_pass1_if_armed` in `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` in `step_sweep` of a synchronous full: an allocation inside that window reaches `gc_check_trigger` with `GC_FLAG_IN_ALLOC` set and returns before the changed code. No allocation, relocation, collection or JS callback is added to the window. Re-audited 2026-09-14 for the tiny-parse nursery-cap boundary, which touched `gc/policy.rs`. It adds `tiny_parse_generational_collection_due`, a pure predicate (the existing `tiny_parse_pressure_due` OR the existing `young_scavenge_cap_due` read), and calls it instead of `tiny_parse_pressure_due` from `gc_bump_malloc_trigger_inner` and `gc_collect_pending_suppressed_parse_slow` (generational branch only) and from `gc_schedule_parse_boundary_collection_if_pressure`. All three are JSON.parse mutator-side boundaries, none reachable from `step_mark_propagation` or `step_sweep`; the predicate reads counters and allocates nothing. Neither census boundary nor the synchronous mark-complete to sweep-entry interval changed. Re-audited 2026-09-13 for #10182's full-collection throughput follow-up, which touched `gc/cycle.rs` in one hunk, INSIDE the window: the `RememberedSetRebuild` subphase of a synchronous full now first asks `verify::full_remembered_rebuild_provably_empty` and, when it holds, installs `OldToYoungRememberedRebuildState::provably_empty()` (an empty sticky set, no walk) instead of the require-marked rebuild. The predicate reads `arena_block_snapshots()` (one `Vec` through the global allocator), the census's per-block reached/pre-marked facts and the malloc registry's length; the constructor bumps a `Cell` counter and prints one line under `PERRY_GC_DIAG`. None of it allocates a GC object, relocates anything, collects, or runs a JS callback, and both census boundaries stay where they were. Re-audited 2026-09-14 for #10182's pacing-full work, which touched `gc/cycle.rs`, `gc/mod.rs` and `gc/policy.rs`. `gc/cycle.rs`: `GcCycleState::new_full` no longer calls `materialize_all_promoted_page_runs`; that call ran in the constructor, before the census and far before `census_pass1_if_armed`, and removing it adds nothing to the window. `gc/mod.rs`: one `mod promoted_cohort;` declaration. `gc/policy.rs`: (a) `credit_promoted_bytes_to_old_baseline` also credits a `Cell` cohort counter (it runs after a copying minor completes); (b) `finish_full_old_reclaim_baseline` also records the verified old live bytes and resets that counter (Publish, after `step_sweep` consumed the snapshot); (c) `gc_safepoint_moving_minor` arms and disarms the promotion-census record around its nursery minor and calls `run_promoted_cohort_full_if_due`, which starts a synchronous full through the same `gc_collect_full_mark_sweep_with_trigger` entry and reads byte counters before and after it. All of it runs before a cycle starts or after it completes; none of it runs between mark completion and sweep entry, allocates a GC object, relocates anything, or calls into JS. The census the promoted-cohort full may adopt from the promotion walk is built in `BuildValidPointerSet`, before either boundary. Both boundaries are unchanged. Re-audited 2026-09-14 for the #10182 dead-stack scrub in `gc/cycle.rs`: `step_build_valid_pointer_set` now calls `scrub_dead_stack_below`, which zeroes a local array in its own frame (dead stack below the caller), right after the census finishes \u2014 in `BuildValidPointerSet`, before the root scan and far before `census_pass1_if_armed`. It writes no heap memory, allocates nothing, relocates nothing and calls no JS; both boundaries are unchanged. Re-audited 2026-09-14 for #10241 (cohort survival), which touched `gc/cycle.rs` and `gc/policy.rs`. `gc/cycle.rs`: one call, `promoted_cohort::survival::check_minor_view_at_full_sweep_start()`, in `step_sweep` immediately AFTER `census_take_if_armed_at_full_sweep_start` has taken PASS1_MARKED out of TLS, i.e. outside the window. It is a no-op unless a promoted-cohort full armed its survival probe; when armed it walks the old page index over the preceding minor's dirty pages (`old_arena_walk_objects_on_pages`, Rust-allocator Vecs), reads GC headers' mark flags and the slots of unmarked ones, and records one enum. It writes no heap memory, allocates no GC object, relocates nothing and calls no JS. `gc/policy.rs`: `run_promoted_cohort_full_if_due` arms the probe before `gc_collect_full_mark_sweep_with_trigger` and takes it after the full returns (feeding `note_full_measured_promotion_survival` and one diagnostic line); both run before a cycle starts or after it completes. Both boundaries are unchanged. Re-audited 2026-09-14 for #10241's in-place-only cohort: `gc/policy.rs` drops the `promoted_cohort::note_promoted` call from `credit_promoted_bytes_to_old_baseline` (the copying minor now calls `promoted_cohort::note_minor_promotion` itself, after the credit). Both run at the end of a copying minor, outside any full cycle; the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-14 for the parse-boundary side-allocation band (medium-parse pacing), which touched `gc/policy.rs`. Three hunks: (a) a `Cell` thread-local (`GC_LAST_COLLECTION_EXTERNAL_SIDE_BYTES`, a byte COUNT, no pointer) plus three pure predicates over it and `external_side_live_bytes()`; (b) that predicate added as a third disjunct of `tiny_parse_generational_collection_due`, which is read only from the three JSON.parse mutator-side boundaries (`gc_bump_malloc_trigger_inner`, `gc_collect_pending_suppressed_parse_slow`, `gc_schedule_parse_boundary_collection_if_pressure`), none of them reachable from `step_mark_propagation` or `step_sweep`; and (c) one extra `Cell` store in `note_collection_finished_arena_occupancy` plus two extra reads in the `PERRY_GC_DIAG` tiny-parse line. `note_collection_finished_arena_occupancy` runs from `publish_reclaim_outcome` in the Publish subphase, i.e. AFTER `step_sweep` has already `take()`n the snapshot out of the thread-local, exactly as #9831's store on the same line does. Nothing added allocates a GC object, relocates anything, or runs a JS callback, and neither census boundary moved. Re-audited 2026-09-14 for the drained-bytes counterweight to that band, which touched `gc/policy.rs` again. Four hunks: a second `Cell` thread-local (`GC_EXTERNAL_SIDE_DRAINED_SINCE_FULL`, a byte COUNT); one increment of it inside `gc_note_external_side_free`; a pure read (`external_side_old_reclaim_pressure_bytes`) substituted for `external_side_live_bytes()` at the four old-reclaim pressure sites; and one `Cell` store at the top of `finish_full_old_reclaim_baseline`. None of it can run between the census boundaries. `gc_note_external_side_free` is also reached by mutator-side tape materialization, regex scratch teardown, native-addon adjustments and buffer replacement. Its added operation is only a saturating increment of a scalar Cell, with no GC allocation, relocation, collection or JS callback, so this wider caller set does not invalidate the census window. `finish_full_old_reclaim_baseline` runs from `publish_reclaim_outcome` in the Publish subphase, the same place #9831's store already sits. The pressure reads happen at trigger decisions, before a cycle starts. No allocation, relocation, collection or JS callback is added to the mark-complete -> sweep-entry window, and neither boundary moved. Re-audited 2026-09-15 for turnloop P0, which touched `gc/mod.rs` with one added call: `crate::event_pump::shutdown_wait_driver()` inside `js_gc_release_current_thread_collection_side_allocations`, the process-exit funnel. That function runs once no more JavaScript can run on the thread, never from inside a collection cycle; the added call drops the thread's turnloop wait loop (closing its kqueue/epoll descriptor) and may print a diagnostic line. It allocates no GC object, relocates nothing, starts no collection and runs no JS callback. The census boundaries and the mark-complete -> sweep-entry window are untouched.. Re-audited 2026-09-16 for the copying minor's per-parent weak-holder fact: `gc/mod.rs` gains exactly one line, `mod copying_parent_facts;`, a module declaration. The module it declares holds `weak_holder_fact` (a read of the parent's `obj_type`/`class_id` via `weakref::is_weak_holder_header`) and the copying minor's `visit_slot_with_parent`, moved verbatim out of `gc/copying.rs` for the 2000-line lint. Both run only inside a COPYING MINOR, which skips both census boundaries (`census_pass1_if_armed` in `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` in `step_sweep` are synchronous-full only). Nothing was added to any full-cycle phase, and the declaration itself executes no code. Neither boundary moved and the synchronous mark-complete to sweep-entry window gains no allocation, relocation, collection or JS callback. Re-audited 2026-09-18 for the #10532 follow-up argument-list rooting fix, which touched `gc/mod.rs`. The only change there is `mod collection_points;` plus a `pub(crate) use collection_points::collection_point;` re-export (and, under `#[cfg(test)]`, `arm_collection_point`). `collection_point` is an inline no-op outside `cfg(test)`; under test it only runs a copying minor when called from ordinary MUTATOR code (`proxy.rs`'s `Reflect.apply` rebind path and `registry.rs`'s rest-array bundler), never from inside `step_mark_propagation` or `step_sweep`. Neither `census_pass1_if_armed` nor `census_take_if_armed_at_full_sweep_start` is reachable from it, so the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-18 (same PR, round 2) for the added `arm_collection_point_after` re-export in `gc/mod.rs`: another pure re-export line, same as the `collection_point`/`arm_collection_point` one already covered above. `arm_collection_point_after` only changes test-only arming state in `collection_points.rs` (which named site fires and on which hit); it still runs no mark/sweep control flow. Re-audited 2026-09-19 for #10735 (require.main threading): gc/mod.rs gains exactly one line, `reg_scanner!(crate::module_require::scan_cjs_main_module_root_mut);`, registering the new CJS_MAIN_MODULE thread-local's mutable-root scanner beside the existing `scan_module_path_roots_mut` registration. A scanner registration adds a root SOURCE for the mutable-root walks; it runs during root scanning, before mark propagation completes, and does not execute between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start`. Neither census boundary moved and the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-20 for #10834 (inherited-property read cache). `gc/mod.rs` gains exactly one line: `reg_scanner!(crate::object::inherited_read_cache::scan_inherited_read_cache_roots_mut);` in `gc_init()`. A scanner registration adds a root SOURCE for the mutable-root walks. The walk runs inside `RootScanCycleState::step_current_subphase`, i.e. entirely within the RootScan phase: `step_root_scan` only sets `self.phase = GcCyclePhase::MarkPropagation` once that loop reports done (`gc/cycle.rs:958-961`), and `census_pass1_if_armed()` fires at the END of `step_mark_propagation` (`gc/cycle.rs:982`). The scanner therefore runs strictly BEFORE the window opens and can never execute between the boundaries. Its body is a bounded walk of a fixed 512-entry thread-local array calling `visit_tagged_usize_slot` / `visit_usize_slot`; it allocates nothing, relocates nothing and runs no JS callback. Same shape as #9769, #9976/#9977, #10054, #10055 and #10735, all previously cleared. The PR also adds an `INHERITED_READ_CACHE` entry to `DEAD_KEY_PRUNES` in `gc/dead_owner.rs` (not a pinned source). That registry is consumed by `IncrementalSweepState::with_dead_collection_finalize` at `gc/cycle.rs:1548`, which is AFTER `census_take_if_armed_at_full_sweep_start` at `gc/cycle.rs:1505` has already `take()`n the snapshot out of the thread-local -- the same argument that cleared #9845's `collect_dead_registered_regexps_post_trace`. The prune reads addresses and zeroes entries; no GC allocation, relocation or callback. Both additions sit outside the window, on opposite sides of it. Neither boundary moved and the synchronous mark-complete to sweep-entry interval is unchanged. Re-audited 2026-09-22 for #10399 (per-thread module init), which touched `gc/mod.rs`. Two hunks, both init-time: a new free function `raise_default_thread_stack_floor()` and one call to it at the top of `js_gc_init`, before `enter_current_thread_image`'s successor statements. The function reads `RUST_MIN_STACK` from the environment and, only when it is unset, sets it to 32 MiB so a thread spawned against a multi-megabyte static TLS block still has usable stack (glibc carves static TLS out of the thread's stack mapping). It touches no heap object, allocates no GC object, relocates nothing and runs no JS callback. `js_gc_init` is the first runtime call of a compiled `main`, so it runs once before any cycle exists, and it is not reachable from `step_mark_propagation` or `step_sweep`. Same shape as the 2026-09-11 startup-memory-profile re-audit, which cleared the pre-main allocator-policy constructor in the same function. Neither census boundary moved and the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-23 (size/runtime-decouple, #11135) after the binary-size branch touched `gc/census.rs`, `gc/mod.rs` and `gc/policy.rs`. census.rs: `census_pass1_if_armed` / `census_take_if_armed_at_full_sweep_start` keep their bodies verbatim, moved into `_impl` functions compiled only with the new `gc-instruments` feature (without it both are empty and `census_path()` is `None`, so nothing is ever armed); the take still empties PASS1_MARKED before `take_census`. gc/mod.rs: `gc_init` gains a startup env check that aborts when an instrument knob is set without the feature, before any cycle exists. gc/policy.rs: env-knob OnceLock caches now initialize through `crate::once_init::get_or_init` (same closures, same values). No mark/sweep control flow between the two census boundaries changed; the window is unchanged. Re-audited for Fetch handle reclamation: cycle.rs only redirects the incomplete-cycle Drop cancellation hook to also cancel the Fetch trace. The full-trace finish hook removes native records and cached slots without allocating GC objects or invoking JS; it cannot relocate the census addresses before sweep entry. Re-audited 2026-09-22 for #10928 (one proportional old-reclaim rule), which touched `gc/policy.rs`. Six hunks. (a) Two new thread-locals, `GC_OLD_RECLAIM_PRE_IN_USE_BYTES` (`Cell`) and `GC_OLD_RECLAIM_BACKOFF_SHIFT` (`Cell`): both are byte/shift COUNTS, neither holds a pointer. (b) `gc_old_reclaim_growth_band_bytes` gains a `Cell` read and a left shift -- pure arithmetic over byte counts. (c) `old_reclaim_pressure_due` loses the #7937 absolute first-crossing arm, splits its pure form out as `old_reclaim_pressure_due_inner`, and calls `note_old_reclaim_cycle_started()` when the answer is true. That predicate is read at TRIGGER decisions only -- the allocation-point `gc_check_trigger` and `gc_budgeted_due_trigger` at safepoints -- i.e. before a cycle starts, never between the boundaries; an allocation inside the window reaches `gc_check_trigger` with `GC_FLAG_IN_ALLOC` set and returns before this code, the same argument the 2026-09-14 due-check fast-path re-audit made for the same function. Even if it did run there it would be sound: `note_old_reclaim_cycle_started` stores one scalar `Cell` from `pacing_arena_in_use_bytes()` (a read of `arena_live_allocated_bytes`), which allocates no GC object, relocates nothing and runs no JS callback -- the window's contract. (d) `update_old_reclaim_backoff` is called only from `finish_full_old_reclaim_baseline`, which runs from `publish_reclaim_outcome` in the Publish subphase, AFTER `step_sweep` has already `take()`n the snapshot out of the thread-local -- exactly where #9831's store and the medium-parse pacing store already sit. (e) `gc_old_reclaim_debt_bytes` drops the absolute arm it mirrored; it remains pure arithmetic read at debt/trigger decisions. (f) `#[cfg(test)]` seams, absent from production builds. `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback between them; the change alters only WHEN a collection is scheduled, never what runs inside one. Neither boundary moved and the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-24 for #10960 (growth-aware old-reclaim backoff), which touched `gc/policy.rs` again. One new thread-local, `GC_OLD_RECLAIM_LAST_POST_IN_USE_BYTES` (`Cell`), a byte COUNT that holds no pointer. It is written only by `update_old_reclaim_backoff`, which runs from `finish_full_old_reclaim_baseline` in the Publish subphase, after `step_sweep` has already taken the snapshot out of the thread-local; the change there is pure integer arithmetic deciding whether to widen the band. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback, and neither window boundary moved. Re-audited 2026-09-24 after the class-capture environment added one `reg_scanner!` registration (`scan_class_env_roots_mut`, visiting each guarded class environment's owner class object) to `gc/mod.rs`: a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-25 after the inherited-access lane touched `gc/mod.rs`: the change is one `reg_scanner!` registration for `object::chain_store::scan_chain_store_roots_mut`, a root scanner that visits store-site chain verdicts (one interned key pointer each) during root scanning. It runs at the start of a cycle, never between mark completion and sweep entry, relocates nothing and runs no JS callback. The synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-25 for #10698 (allocation-point trigger watermark), which touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` gains `pub(crate) mod trigger_watermark;` and a `pub(crate) use` re-export -- no control flow. In `gc/policy.rs`: (a) `gc_check_trigger` becomes an out-of-line wrapper over an inlined fast path that reads the watermark cell, the malloc registry's length through an unguarded shared borrow and the inline allocator's offset, then either returns or runs the previous body unchanged (`gc_check_trigger_evaluate`). An allocation inside the window reaches it with `GC_FLAG_IN_ALLOC` set: the fast path returns without acting, which is the outcome of the `GC_FLAG_IN_ALLOC` early return it would otherwise reach, and the slow path still takes that early return. (b) The due-trigger evaluation also returns a watermark -- integer arithmetic over values it already read -- published only on the no-action return past every entry guard. (c) The ladder's thresholds and flags are retyped `Cell` -> `TriggerInput`, whose writes add one store retiring the watermark; heap-generation advances and `Arena::set_current` retire it too. (d) `#[cfg(test)]` seams. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback; `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`, so the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-25 for #10498 (class-accessor cache), which adds one `reg_scanner!` registration (`scan_class_accessor_cache_roots_mut`, marking and rewriting the cache's key strings) to `gc/mod.rs`: again a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-26 for #10572: `gc/mod.rs` gains one `#[cfg(not(feature = \"hot-diag\"))]` call to `hot_diag::refuse_knobs_without_hot_diag()` in `gc_init`, a startup-only knob check that runs before any cycle and alters no mark/sweep control flow; the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-27 for #11416: `gc/mod.rs` loses the `reg_scanner!` registration for `class_accessor_cache::scan_class_accessor_cache_roots_mut` (the cache is deleted); removing a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 for the method-calls lane, which adds one `reg_scanner!` registration (`scan_method_site_roots_mut`, marking and rewriting the method closures inherited method-site entries hold) to `gc/mod.rs`: a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-28 (charter step 5, P0): the field-representation census adds `mod census_field_repr;` to `gc/mod.rs` and, in `census.rs`, one Rust-owned accumulator fed from `visit_object` inside `take_census`, which runs after `census_take_if_armed_at_full_sweep_start` has already taken PASS1_MARKED; it reads slots and the per-object layout maps (try_borrow), allocates nothing on the JS heap and alters no mark/sweep control flow. The window is unchanged. Re-audited 2026-09-27 for #11416: `gc/mod.rs` loses the `reg_scanner!` registration for `class_accessor_cache::scan_class_accessor_cache_roots_mut` (the cache is deleted); removing a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 for the method-calls lane, which adds one `reg_scanner!` registration (`scan_method_site_roots_mut`, marking and rewriting the method closures inherited method-site entries hold) to `gc/mod.rs`: a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 (class constructors as function objects, #11414): `gc/mod.rs` gains one `reg_scanner!` registration (`object::class_value::scan_class_value_roots_mut`, the per-agent class function-object table, visited and rewritten) \u2014 a root-scanner registration that alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-29 after #11659 added `verify::verify_array_hole_tails_at_collection()` to `gc/mod.rs` and `gc/policy.rs`. Every call sits at a collection or budgeted-cycle ENTRY, beside `roots::ensure_stack_maps_built()`, i.e. before mark begins and so before `census_pass1_if_armed` opens the window. The call is a read-only walk of array headers (debug builds, or release with PERRY_GC_VERIFY_ARRAY_HOLES): it neither allocates GC memory, relocates, runs JS, nor reads or writes PASS1_MARKED, and it panics rather than continuing on a violation. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Pin re-audited 2026-09-29 after PR #11646 added one call inside that window, in `cycle.rs` beside `census_take_if_armed_at_full_sweep_start`: `object::shapes::store_kind::audit_heap_at_full_sweep_start()`. It compiles to nothing unless the `shape-fact-audit` feature is on; when on it walks the arena read-only (`gc::for_each_live_object_at_sweep_start`, in `gc/verify.rs`) and reads each marked object header and its shape record. It allocates no GC object, moves nothing, runs no JS callback and holds no address past the walk, so PASS1_MARKED stays valid across it; a disagreement panics rather than continuing. Re-audited 2026-09-29 after the pinned-roots fix touched `gc/cycle.rs` and `gc/mod.rs`: the block-persistence live-block predicate now reads GC_FLAG_MARKED alone (pinned objects are marked as roots, so the set of live blocks is unchanged), and `gc/mod.rs` adds one `reg_scanner!` registration. Neither relocates, runs JS, or moves the mark-complete to sweep-entry window. Re-audited again 2026-09-29 (same fix, copying-minor follow-up): `BlockPersistCycleState`'s force-mark in `gc/cycle.rs` no longer skips an unmarked pinned header, so it marks and pushes it like any other object of a live block. That is marking inside the budgeted cycle's persistence step: nothing relocates, no JS runs, and the budgeted path still skips both census boundaries. The window is unchanged. Re-audited 2026-09-29 for this-as-a-parameter stage 3: `gc/mod.rs` registers `scan_dispatch_binding_roots_mut` in place of `scan_implicit_this_roots_mut` (the implicit-`this` cell is deleted; the same scanner body keeps `new.target`, the static-`this` override and the static private-owner stack) and rewords its comment; a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-28 for the JsFunctionInfo lane: `gc/census.rs`'s `side_tables()` loses its `closure_registry_census()` row (the closure-body registry is deleted); a census report row, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-29 for the read-site holder entry: `gc/mod.rs` adds one `reg_scanner!` registration, `read_holder::scan_read_holder_roots_mut`, which visits the holder and hop words of registered read-site caches during ROOT SCAN, before mark propagation completes. It rewrites root slots only through the visitor (as every registered scanner does), runs no JS, and nothing it does executes between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start`. The window is unchanged. Re-audited 2026-09-30 for Step 5 P4: census.rs only removes the typed-layout count, and gc/mod.rs removes shape-install registration/re-export and an init-time typed-intact verifier gate. The snapshot remains stored after mark propagation and taken at sweep entry; none of these edits relocates objects or invokes callbacks in that interval. Re-audited 2026-09-29 (#11549 trace cost): `gc/mod.rs` gains one `mod copying_object_scan;` declaration, the copying minor's plain-object slot walk. It is reached only from the copying drain, never from a full cycle, and changes no mark or sweep control flow. The window is unchanged. Re-audited 2026-09-30 after rebasing P4 over #11549/#11676: gc/mod.rs retains the copying_object_scan declaration and P4 removes typed-layout registration; both are module wiring outside the synchronous full mark-complete to sweep-entry window. The census boundaries and the window owner in cycle.rs are unchanged. Re-audited 2026-09-30 for the scope-context change: gc/mod.rs only removes the box root-scanner registration and exit-time box statistics. The former ran during root scan, before mark completion, and the latter runs at process exit. Neither changes the synchronous mark-complete to sweep-entry window. Re-audited 2026-09-28 for #11605 (link-time runtime feature installs): `gc/mod.rs` registers the interpreter root scanner through its always-present forwarder (`dyn_eval_hooks::scan_dyn_eval_roots_mut`, which calls the real scanner through a slot the `dyn-eval` install fills) instead of a feature-gated direct registration, and `js_gc_init` ends by running the program\u2019s feature installer, which only stores fn pointers into `Hook` slots at startup, before any user code or collection. Neither alters mark/sweep control flow or runs inside the mark-complete to sweep-entry window. Re-audited 2026-10-01 after main integration: the pinned census, cycle, policy and progress files are byte-identical to current main. The only gc/mod.rs delta replaces the interpreter scanner registration with its installed-slot forwarder and invokes the installer at js_gc_init startup, before user code and any collection. No change executes between mark completion and sweep entry. Re-audited 2026-09-28 for the release-runtime instrument strip: every changed line in `gc/census.rs`, `gc/cycle.rs`, `gc/mod.rs` and `gc/policy.rs` renames a `feature = \"diagnostics\" | \"gc-instruments\" | \"hot-diag\"` gate to the build-script cfg `perry_diagnostics` / `perry_gc_instruments` / `perry_hot_diag`, which `perry-runtime/build.rs` sets exactly when the feature is on unless PERRY_RELEASE_STRIP_INSTRUMENTS=1. With the cfg set the compiled code is unchanged; with it unset (release packages) the census that fills this snapshot is not compiled, so the window never opens. No mark/sweep control flow changes. Re-audited 2026-10-01 while integrating the repaired #11605 parent: all five pinned files are byte-identical to that reviewed parent after normalizing only the diagnostics/instrument cfg names; its current census_field_repr module and startup installer hooks remain intact. The synchronous mark-complete to sweep-entry window is unchanged. Re-audited for A2 inherited-read-cache deletion: gc/mod.rs removes only its old root-scanner registration and adjacent comments. The method-site and read-holder scanners still run during root scan before mark completion; no collection phase or callback was added inside the mark-complete to sweep-entry window. Re-audited 2026-09-30 for the setter-site root scanner: gc/mod.rs adds only its reg_scanner! registration in the root-scan setup. That scanner visits per-site key and holder slots before mark propagation completes; it runs no JS and adds no relocation or callback between census pass1 and sweep entry. The synchronous-full snapshot window and both boundaries remain unchanged. Re-audited 2026-10-01 against main a8f4f3dd76: census.rs, cycle.rs, policy.rs and progress.rs are byte-identical to reviewed #11629 head 85e0fe18ec. The only pinned-file delta in gc/mod.rs removes the deleted inherited-read-cache scanner, retains chain-store/method-site/read-holder scanners, and registers the setter-site scanner during gc_init. These registrations run during root scanning before mark completion. Both link-time feature installers and release instrument cfgs are preserved; neither census boundary nor the non-moving synchronous-full interval changes. Re-audited 2026-09-28 for #11605 (link-time runtime feature installs): `gc/mod.rs` registers the interpreter root scanner through its always-present forwarder (`dyn_eval_hooks::scan_dyn_eval_roots_mut`, which calls the real scanner through a slot the `dyn-eval` install fills) instead of a feature-gated direct registration, and `js_gc_init` ends by running the program\u2019s feature installer, which only stores fn pointers into `Hook` slots at startup, before any user code or collection. Neither alters mark/sweep control flow or runs inside the mark-complete to sweep-entry window. Re-audited 2026-10-01 after main integration: the pinned census, cycle, policy and progress files are byte-identical to current main. The only gc/mod.rs delta replaces the interpreter scanner registration with its installed-slot forwarder and invokes the installer at js_gc_init startup, before user code and any collection. No change executes between mark completion and sweep entry. Re-audited 2026-10-01 while integrating main 7b5912d4e7 into #11605: census.rs, cycle.rs, policy.rs and progress.rs are byte-identical to that main. The only gc/mod.rs differences register the existing interpreter root scanner through its installed-slot forwarder and run the selected feature installer at js_gc_init startup before user code. The current main method/read-holder/setter-site scanners are preserved. No work was added between mark completion and sweep entry; the synchronous non-moving snapshot window is unchanged. Re-audited 2026-10-02 for the SPECIAL ConstFn verifier: gc/mod.rs adds only a feature-gated forwarding-helper re-export. The helper follows existing validated forwarding and runs no JS, allocation or collection. No holder, scanner registration or mark/sweep control flow is added; both census boundaries and their synchronous window remain unchanged. Re-audited 2026-10-04 after the class-method chain memo touched `gc/mod.rs`: one more `reg_scanner!` registration, scan_chain_memo_roots_mut, which visits the prototype addresses site chain memos record as strong roots during root scanning (before mark propagation). It runs no JS, performs no GC allocation or collection, and adds no relocation or callback between census pass1 and sweep entry; both census boundaries and their synchronous window remain unchanged. Re-audited 2026-10-03 for the prototype-in-shape lane: gc/mod.rs adds one reg_scanner! registration, scan_shape_prototype_words_mut, which visits the shape records' prototype words and their identity-index values as strong roots during root scanning (before mark propagation). It runs no JS, performs no GC allocation or collection, and adds no relocation or callback between census pass1 and sweep entry; both census boundaries and their synchronous window remain unchanged.", "window": { "start": { "file": "crates/perry-runtime/src/gc/census.rs", @@ -360,7 +360,7 @@ "sources": { "crates/perry-runtime/src/gc/census.rs": "34d3054f47720478dfe90708c8a65895cb21d1e10d83b71dcce4742e3f480a59", "crates/perry-runtime/src/gc/cycle.rs": "ecc6ff4833cd3a49ad2a4cb93df4de58032e0b0bacd07892263cc0043145c991", - "crates/perry-runtime/src/gc/mod.rs": "d0e25319e6dfedea92bb9ee8a082efaf12a8ff8cfa07f8335c0b481b0efef6c3", + "crates/perry-runtime/src/gc/mod.rs": "fa8998539a8e1f1386b99140e278178ee5d7745f216daca4a1e175444c92e296", "crates/perry-runtime/src/gc/policy.rs": "59bd224294917f513b792398f9a399da577c9133653960a0c0b2d362cce19682", "crates/perry-runtime/src/gc/progress.rs": "a5ad3971bbe4047229ca57325234780daa85921dbc778e1c08dff4ad07ccfb96" } @@ -3645,14 +3645,6 @@ "file": "crates/perry-runtime/src/object/arguments.rs", "name": "TEST_ARGUMENTS_REGISTRY_PROBES" }, - { - "file": "crates/perry-runtime/src/object/class_registry/dispatch.rs", - "name": "OBJ_DISPATCH_IC" - }, - { - "file": "crates/perry-runtime/src/object/class_registry/dispatch.rs", - "name": "VTABLE_IC" - }, { "file": "crates/perry-runtime/src/object/class_registry/prototype_methods.rs", "name": "CLASS_PROTOTYPE_METHODS" diff --git a/scripts/thread_exit_address_globals.json b/scripts/thread_exit_address_globals.json index 2082b00809..9893203fa7 100644 --- a/scripts/thread_exit_address_globals.json +++ b/scripts/thread_exit_address_globals.json @@ -4017,6 +4017,14 @@ "verdict": "process_global_allocation", "why": "The addresses of read-site `PicCache`s, each allocated by `field_get_set::ic_slot::pic_arena_alloc` from `std::alloc::alloc_zeroed` chunks that are never freed and belong to no thread's arena, so thread exit cannot free or reuse them. Primary-heap holder and hop addresses are written only by the primary agent and traced by its scan_read_holder_roots_mut until the atomic worker-start gate is set. Thereafter no agent reads or scans stale holder words, allowing their objects to collect." }, + { + "file": "crates/perry-runtime/src/object/method_site/chain_memo.rs", + "names": [ + "CHAIN_MEMOS" + ], + "verdict": "process_global_allocation", + "why": "The addresses of site chain memos (`ChainMemo`), each allocated by `field_get_set::ic_slot::pic_slot_resolve` from the process-lifetime PicArena (`std::alloc::alloc_zeroed` chunks that are never freed and belong to no thread's arena), so thread exit cannot free or reuse them. Primary-heap prototype addresses in a memo are written only by the primary agent and traced by its scan_chain_memo_roots_mut until the atomic worker-start gate is set. Thereafter no agent reads, records or scans a memo, allowing their objects to collect." + }, { "file": "crates/perry-stdlib/src/common/handle_lifecycle.rs", "names": [ diff --git a/test-files/test_gap_10502_class_method_chain_memo.ts b/test-files/test_gap_10502_class_method_chain_memo.ts new file mode 100644 index 0000000000..5bc2ca0688 --- /dev/null +++ b/test-files/test_gap_10502_class_method_chain_memo.ts @@ -0,0 +1,88 @@ +// A by-name class method call repeats from its site's chain memo +// (receiver word, every prototype hop's word, the holder's slot). Every +// change to an object the memo names must be seen on the next call. + +class A { m(x: number): string { return "A" + x; } n(x: number): string { return "nA" + x; } } +class B extends A {} +class C extends B {} +class D extends C {} + +function computed(o: any, k: string, x: number): string { return o[k](x); } +function named(o: any, x: number): string { return o.m(x); } + +const keyM = ["m"].join(""); +const keyN = ["n"].join(""); +const out: string[] = []; +const d = new D(); + +// 1. computed key, holder three hops up: record, then repeat. +for (let i = 0; i < 4; i++) out.push(computed(d, keyM, i)); +// 2. an intermediate prototype gains the name: its word changes. +(B.prototype as any).m = function (x: number) { return "B" + x; }; +for (let i = 0; i < 3; i++) out.push(computed(d, keyM, i)); +// 3. ... and loses it again. +delete (B.prototype as any).m; +for (let i = 0; i < 3; i++) out.push(computed(d, keyM, i)); +// 4. the holder's value is overwritten in place. +(A.prototype as any).m = function (x: number) { return "A2:" + x; }; +for (let i = 0; i < 3; i++) out.push(computed(d, keyM, i)); +// 5. two keys through one site. +for (let i = 0; i < 4; i++) out.push(computed(d, i & 1 ? keyN : keyM, i)); +// 6. a prototype hop is relinked. +class X { m(x: number): string { return "X" + x; } } +Object.setPrototypeOf(C.prototype, X.prototype); +for (let i = 0; i < 3; i++) out.push(computed(d, keyM, i)); +// 7. the receiver gets an own property. +const d2: any = new D(); +for (let i = 0; i < 3; i++) out.push(computed(d2, keyM, i)); +d2.m = function (x: number) { return "own" + x; }; +for (let i = 0; i < 3; i++) out.push(computed(d2, keyM, i)); + +// 8. a fixed-name call on an untyped receiver, deep holder. +class P { m(x: number): string { return "P" + x; } } +class Q extends P {} +class R extends Q {} +class S extends R {} +const s: any = new S(); +for (let i = 0; i < 4; i++) out.push(named(s, i)); +(R.prototype as any).m = function (x: number) { return "R" + x; }; +for (let i = 0; i < 3; i++) out.push(named(s, i)); +(Q.prototype as any).m = function (x: number) { return "Q" + x; }; +delete (R.prototype as any).m; +for (let i = 0; i < 3; i++) out.push(named(s, i)); + +// 9. many receiver classes through one site. +class K0 { k(): number { return 0; } } +class K1 extends K0 { k(): number { return 1; } } +class K2 extends K0 {} class K3 extends K1 {} class K4 extends K3 {} +const ks: any[] = [new K0(), new K1(), new K2(), new K3(), new K4()]; +let sum = 0; +for (let i = 0; i < 200; i++) sum += ks[i % 5][["k"].join("")](); +out.push("sum=" + sum); +(K1.prototype as any).k = function () { return 10; }; +sum = 0; +for (let i = 0; i < 200; i++) sum += ks[i % 5][["k"].join("")](); +out.push("sum2=" + sum); + +// 10. a direct class-method site whose name another prototype assigns +// (the compiled arm misses on every call: its miss edge repeats from the memo). +function Fn(this: any) {} +Fn.prototype.plus = function (y: number) { return y; }; +class Dec { v: number; constructor(v: number) { this.v = v; } plus(y: number): Dec { return new Dec(this.v + y); } } +class Dec2 extends Dec {} +let x: Dec = new Dec2(1); +for (let i = 0; i < 50; i++) x = x.plus(i); +out.push("dec=" + x.v); +(Dec.prototype as any).plus = function (this: Dec, y: number) { return new Dec(this.v - y); }; +let z: Dec = new Dec2(1000); +for (let i = 0; i < 5; i++) z = z.plus(i); +out.push("dec2=" + z.v + " " + (new (Fn as any)()).plus(3)); + +// 11. garbage between calls: the memo's prototypes may move. +let junk: any[] = []; +for (let r = 0; r < 30; r++) { + for (let j = 0; j < 2000; j++) junk.push({ a: j, b: [j], c: "s" + j }); + if (junk.length > 20000) junk = []; + out.push(computed(d, keyM, r) + named(s, r)); +} +console.log(out.join("\n")); From aadc9fa906014e6553c34644e2bb6c44977db785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 4 Oct 2026 10:05:49 +0000 Subject: [PATCH 2/2] changelog: key the fragment to PR 11902 --- ...ethod-lookup-shapes.md => 11902-class-method-lookup-shapes.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{PENDING-class-method-lookup-shapes.md => 11902-class-method-lookup-shapes.md} (100%) diff --git a/changelog.d/PENDING-class-method-lookup-shapes.md b/changelog.d/11902-class-method-lookup-shapes.md similarity index 100% rename from changelog.d/PENDING-class-method-lookup-shapes.md rename to changelog.d/11902-class-method-lookup-shapes.md