Repository navigation
Realm production A2b: one declaration per native namespace (owners, names, Node member order); module vtable is a shape fact - #13967
Draft
proggeramlug wants to merge 42 commits into
Draft
proggeramlug wants to merge 42 commits into
proggeramlug wants to merge 42 commits into
Conversation
…ate input; the gate asserts the seed and runs in CI
… in; owner truth test
…mes; uses are spelled as the declaration
realm_decl! gains the item form `namespace SYM = "owner" { names = [...]; members = { key; key = (value); } }`.
It expands to the module's vtable static: the shared NATIVE_MODULE_VTABLE_BASE plus the module_keys the item
lists, in Node's order. The fs namespace moves to fs/namespace.rs; FS_NAMESPACE_EXPORT_KEYS is gone and
Object.keys reads NM_FS.module_keys.
The producer reads the item as source text and births its cell through the native namespace birth, which
names the item births with the entry `namespaces = native_namespaces`. The cell records the item's source,
line and vtable (crate::fs::namespace::NM_FS). An item without that one birth, a duplicate owner, or a
member value that is not one paren group fails by name.
Test 6.3, generated next to the owner truth test: the vtable keys are the declaration's own key order, and
Object.keys of the namespace the owner yields is exactly those keys.
…spaces/<module>.rs, http2 in node_http2_constants.rs); their hand key tables and arms are gone
…e members perry serves (Q7: Object.keys no longer leaks __module__); their inline lists leave the native birth crypto.constants (54), crypto.subtle (0, extends SubtleCrypto.prototype), crypto.webcrypto (0, extends Crypto.prototype), fs.constants (56), os.constants (5), readline (7), stream/promises (2), string_decoder (1), v8.promiseHooks (5), v8.startupSnapshot (4), streams (0): Node v26.12.1 order, keys perry does not serve left out. Test every_declared_namespace_key_is_served.
… and 3 two-name splits leave their hand tables
Each Node module whose enumerable keys were a hand table behind a
`native_module_enumerable_keys` arm is now an item-level
`realm_decl! { namespace ... }` beside its module, and replaces its
births-only entry in the native birth block: dgram, async_hooks, sea,
module, path (+ posix, win32), dns (+ promises), child_process, cluster,
stream, process (namespace, default), querystring, punycode (+ default,
ucs2), timers (+ promises), os, url, util, events, readline/promises,
wasi, inspector, inspector/promises, repl. 20 hand tables are deleted.
The legacy util members refer to node:sys.<k>; events carries the data
values of node:events; punycode gets its bound methods and version as nodes.
Deferred (non-Node module declarations): sqlite, sqlite.constants, bun,
bun:ffi, bun:jsc, ffi, node-pty keep their arms and tables.
…option a, stage S1) perry-abi native_vtable_brand defines the brand band 1<<60 once (producer and reader). The namespace birth brands its shape with the module vtable, so each module's namespace has its own shape and the existing transitions carry the vtable to successor shapes. Readers take the vtable off the receiver's shape; the global NATIVE_MODULE_VTABLE_PTR is gone. Keys live on the vtable (ModuleKeys head+tail, no OnceLock). Runtime instance objects carry the unkeyed vtable. The name -> vtable match still picks the vtable at the by-name birth; S2 deletes it. Tests: own shape per module, twin births share it, unmatched name is unkeyed, define/delete, preventExtensions/seal/freeze and the sealed `module` namespace keep the vtable, the brand is not a key.
…e; the default namespace vtable lives on the base vtable; codegen passes vtables (Q8 S2 runtime and codegen)
… expect the _vt entry (Q8 S2)
…value, payload prototypes and payload families (Q8 S2 step 2)
…ry their vtable; IR test: no by-name birth without a dynamic require (Q8 S2)
…ule_namespace, js_native_module_esm_export_value, js_native_module_named_esm_export_value) with their anchors and wasm rows (Q8 S2 Q1)
… vtable base link, one clause sets both links (Q8 S2 Q2)
…_vt); the deleted extern's name no longer stands for it
…PROCESS (main added the call after the by-name birth was deleted)
…main's gate; references regenerated (names from this head's R, cells from the restricted dumper source); the dumper skips the identity block when its module import is bound out of R
…rocess namespace birth changed); thread-exit custody entries for the module row table (NATIVE_MODULE_ROWS/ROW_COUNT replace NATIVE_MODULE_VTABLE_PTR)
A birth publishes its first keyed shape through shapes::birth_publish_branded_keys: the shape is the vtable's root shape (keys plus brand), so no brand transition runs and no unbranded shape is published first. The shared publishes (mod.rs, field_ops.rs, field_get_set.rs) are main's code byte for byte; the brand is applied only by the birth entry.
… is the CJS value (Stream, EventEmitter, Module) import d from "node:stream" gives the module.exports value, so the declaration refs its cell rather than the ESM namespace of the module (which pulled ReadableStream and WritableStream into node-imports R). Gate references regenerated (node-imports loses 7 names); gap test test_gap_node_default_import_is_cjs_value checks default import, namespace .default and the named export agree, as in node 26.8.1.
The NoBaseNamespace import the namespace macro emits is allowed to be unused where a vtable has its own base; dead module bindings, two needless unsafe blocks and the dead CRYPTO_NAMESPACE_KEYS table are removed; rustfmt on the files whose main version is formatted.
…rocess.default, constants.default, dgram.default, http.default, https.default, net.default, process (and process.namespace, process.default), repl.default, sys.default, tls.default, url.default, worker_threads.default The declaration is the one list of each namespace (the vtable key table and Object.keys derive from it): shared members are placed in Node v26 insertion order, members only perry has keep their place against their neighbours.
…n reorder, same members)
…createInterface) at the submodule birth
…r (Node order) instead of a second hand list
…he three SPKAC methods), statics in Node order ownKeys listed prototype while the descriptor was undefined. The function stays a plain function cell (no constructor kind), the prototype is born with the function like X509Certificate.prototype; test_gap_crypto_certificate_prototype compares typeof, ownKeys, the prototype descriptor and its members with node.
…e; gap test test_gap_native_member_order (16 cells, shared keys, node oracle)
…ble segment; a code-size difference past a page moved the read-only segment start)
…ssageEvent global and dropped the Object.prototype names from Object; the old-to-new change equals main's own 6e5c6e1-to-ced229822d change except on the cells this branch changes)
…one member list per namespace, the gate compares it with Node's Object.keys per cell (native-order.node.json, Node v26.12.1), cells with no Node value listed, the disagreeing set equals ORDER_DISAGREES
added 12 commits
November 4, 2026 06:42
…table) rows a program with a dynamic import carries (test-only with_test_module_rows installs and restores them)
…no inner block); string_decoder drops the unused module name
…an error by name (the producer fills none in); the 8 receivers declare their owner
…ble through one local use (state.rs back under the file-size limit)
… (the shape census tracks the word keys_array as a raw member; the census baseline is unchanged)
…mespace) exports only for its own invocations; the wrapper check and the other arms skip it (self-test added)
…lated to A2b; both files were already over the size limit on main)
…c (a thread_local is a GC-holder census row)
…ugh with_mut_ptr (raw-handle debt ceiling for native_module.rs stays at 4)
… js_nm_set_module_table are A2b rows; 6 more rows were missing on main already
…-export line in fs/mod.rs, which is over the size limit on main)
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Realm production A2b: one declaration per native namespace (owner, names, members in Node's order); the module vtable is a shape fact
Draft. Lands together with PR B (owner ruling 11-04): this PR opens as a draft and goes ready only with PR B. The startup residue below is removed by PR B; it is not accepted on its own.
Follows A2a (#13793: Set bucket contract, array storage, realm-diff gate in CI). Design: WRT A2-DESIGN §2-§7, rulings §9 (Q6-Q10, each open to the owner's veto).
What changed, by item
Item 0: the restricted realm reach unites the program's own names with the gate input. A top program's scan was closed to the whole catalog before the test-only
PERRY_REALM_REACH_NAMESinput ran, so its own names were lost and the R-restricted dumper aborted (runtime layout EVENT_EMITTER_PROTOTYPE is born, but the program's realm does not reach it). The compiler keeps the pre-closure names (own), R = file names ∪ own names; the gate asserts the seed is exactly that union and that the restricted dump equals the reference. The gate runs in CI.Item 1: every native birth declares its owner; the producer fills none in. The by-name owner fallback is deleted.
realm_owners::every_native_cell_of_the_runtime_has_exactly_one_ownerfails by cell name on a missing or misnamed owner. Manifest delta: 58 cells gain the owner field and nothing else. Owner truth test (runtime, cfg(test)): the real access path of every declared name yields a value whose module is the declared owner.Item 2: one object, one declaration. A native birth lists its other names (
node:X.defaultand%NativeNamespace:X%are one cell); the uses are spelled as the declaration.Item 3: a native namespace is declared as an item beside its module (
crate::realm_decl! { namespace NM_X = "x" { ... } },namespaces/<module>.rs). The vtable static carries the keys;Object.keysreads them.Item 4: all namespaces moved. 45 namespaces with hand key tables, the 11 zero-member namespaces (Q7), and the 26 live key-table arms (Q10) plus 3 two-name splits are item declarations. The hand key tables, the 89-arm
native_module_enumerable_keysmatch and the inline key lists at the native birth are deleted. The two dead arms (console.default,http.default) are gone with them.Q8, the module vtable as a shape fact (option a). A namespace is born into its vtable's branded root shape (
perry-abi native_vtable_brand, one band); readers take the vtable off the receiver's shape; the globalNATIVE_MODULE_VTABLE_PTRand the name-to-vtable match are deleted. Codegen passes the vtable symbol at every static site (_vtentries); the by-name externs nothing emits are deleted with their anchors and wasm rows. The one dynamic path (require(expr),getBuiltinModule(id)): codegen emits a constant(name, vtable)array@perry_native_modulesonly in a program with a dynamic require, installed byjs_nm_set_module_table. IR test: no by-name birth and no array without a dynamic require. Owner veto point: this array is the one new emitted table of the PR (it replaces main's always-linked match); PR B turns it into a lookup over the image's native-module record.Item 5: member order, fixed forward. The declaration is the one list, so the order is fixed in the declaration.
node:sys.default(0), tls (0), url (6), worker_threads (0), process with process.namespace/process.default (0), readline/promises (0, at its submodule birth). assert/strict's CJS value iterated a second hand list; it now iterates the declaration. Test:test-files/test_gap_native_member_order.ts== node; sabotage (dgram swapped back) red.__module__leaks:Object.keyswas["__module__"]on 13 cells (11 declarations). Each now declares the members perry serves, in Node order (lists taken by reading eachget_own_fieldmatch): crypto.constants (54 members, two names), crypto.subtle (0), crypto.webcrypto (0), fs.constants (56), os.constants (5), readline (7), stream/promises (2), string_decoder (1), v8.promiseHooks (5, two names), v8.startupSnapshot (4, two names), streams (0). Testevery_declared_namespace_key_is_served(over-declaration is red).crypto.Certificate: a realprototypedata property (constructor and the three SPKAC methods), statics in Node order. Testtest_gap_crypto_certificate_prototype== node.Item 6: the agreement check is an identity (A2-DESIGN 6.4). One member list per namespace means declared order and observed order are one list. The full dumper writes one
@orderline per static import's module value and per sub-namespace; the gate compares the shared members, in order, with Node'sObject.keysover the same access path (realm-dumps/native-order.node.json, Node v26.12.1, regenerated only with--regenerate-node). Result: 54 cells agree, 2 have no Node value and are listed by name (node:streams,node:test), 9 disagree and are listed exactly inORDER_DISAGREES(a cell that starts or stops disagreeing is red). None of the 9 is one of A2's namespace declarations: function-valued modules with own statics (assert, events, module), submodule export objects (diagnostics_channel, fs/promises, stream/consumers, stream/web, test/reporters), and the util object served for sys. Sabotage: dgram reversed in the Node file, gate red namingnode:dgram.Q6 identity record (no cache added). The dump records, per declaration, whether every access path yields one object; the gate asserts the minting set equals exactly these 17 cells (PR B empties the list and deletes the mechanism):
console,%NativeNamespace:{buffer, crypto, domain, readline, string_decoder, tls, tty, v8, worker_threads, zlib}%,node:buffer.constants,node:crypto.constants,node:http2.constants,node:perf_hooks.constants,node:v8.promiseHooks,node:v8.startupSnapshot.Gates (head vs main ced2298)
ORDER_DISAGREES(listed in the gate), no Node value for 2node:dgramrealm_diff_gate_tests.pyrealm_*migration python tests, wgraph gateplantedinstead ofperf_hooks)%NativeNamespace:perf_hooks%: native cell declares owner planted but is named for perf_hooks; restored: green--features target-wasinet_transportsabotage passes alone (suite flake), 2global_testsfail on main toohandle_lifecycle_tests), both fail on main tootest_gap_fetch_streaming_clone(Date.now()-before < 180, a timing assertion). Three runs each, alone: head 3/3 PASS, main 3/3 PASS.-D warningsperry-runtime liblayout_idnever used, plus the crate summary): main'sLint gates (
scripts/run_lint_gates.sh; each failing lint compared by content, head vs main, sorted failure lines)set_native_namespace_keysnamed a parameterkeys_array, a census-tracked raw member word. The parameter iskey_list; the baseline is unchanged.js_sqlite_stmt_columnsnot recordedwrapper macro native_submodule_exports invokes an export macro:realm_decl!'s namespace arm now exports theperry_nm_vt_*family and the scanner took everyrealm_decl!use for an export. The scanner now treats a keyword-picked export arm as exporting only for invocations that start with its keyword (self-test added). Head now fails on the same sqlite row as main (116 recorded collisions on both, no new one).--emit-wasm-abi: the_vtentry rows andjs_nm_set_module_tableare this PR's; six more rows (js_ext_zlib_subclass_init,js_fs_read_callback_short,js_perry_net_link_adopt_descriptor,js_set_native_subclass_init,perry_ffi_gc_*x2) were already missing on mainADD_BRAND_TRANSITIONSthread_local; it is a brand-keyed atomic nownative_module.rsat 5 against a ceiling of 4;set_native_namespace_keysreturns the receiver throughwith_mut_ptrstate.rs(was ours) is back under 2000; rustfmt-only churn indialect/mod.rsundone;fs/mod.rsis 2102 against 2101 on main (themod namespace;line; main's offender)dyn_index.rs9 vs 8 on both-D warningsworkspaceStartup (callgrind Ir, head vs main ced2298, pinned; irdiff measured at the owners/lint commits, codegen unchanged since)
Attribution (per function, call counts from the callgrind edges): the namespace birth into the branded root shape (
set_native_namespace_keysN calls replacesjs_object_set_keys+ publish + live-rep + tombstone, same N, net cheaper); +1 root-shape mint per distinct vtable (shape_descriptor_mint_fresh,publish_dynamic,family_append_fresh: +14 calls in fastify, +3 in lad_07);BrandList::insert+fold_brandsper birth (55 fastify, 20 lad_07); the vtable argument per call (export_begin+4.9/call,bound_native_callable_export_value+14/call,createRequire+3/call);shape_descriptor_intern+0.19/call over 13.6k calls (larger shape table);ensure_function_prototype_object+65/call x16 (not root-caused). Removed by PR B (branded root shapes in the realm image): this residue is not accepted on its own, and the PR does not leave draft before PR B.Linked hello (head vs main,
llvm-nm -S): functions +4086 B, data +4608 B. The data is 64 newperry_nm_vt_NM_*module vtable statics x 72 B (the vtable as a shape fact). Functions:set_native_namespace_keys+4241 replacesnative_module_enumerable_keys-4101;create_native_module_namespace+1355 replacesjs_create_native_module_namespace-1295;js_object_set_keys-1120;BrandList::insert+517,add_brand-651; assert prototype attach +1888 againstassert_cjs_export_value-1479;js_class_static_method_call+1047; namespace_vtable/object_native_vtable/namespace_has_enumerable_key about +1040;OnceLock<Vec<&[u8]>>init -1902. Nothing here is A2b code that hello should leave out: hello links the whole runtime today (WRT DCE and PR B remove the unreached births).Failures that are main's
global_tests::global_object_is_born_on_its_static_layout_and_population_keeps_itandglobal_object_keys_are_atoms_and_a_miss_proves_absencefail on main ced2298 too (MessageEvent is not an image atom).handle_lifecycle_tests::native_property_bag_follows_a_reachable_reclaimable_payloadandnative_property_bag_to_handle_cycle_dies_with_its_last_js_ownerfail on main too (receipts:t-std-main.log, same 2 names).sso_unbox_inventoryfails on main only.-D warningson perry-runtime: 4 errors on both arms, identical text.Open questions
node:<m>cells keep their owner from their name (the name is the declaration); only receivers (%NativeNamespace:x%) declare one, and a receiver without one is an error by name.Object.keys(performance)is 14 perf_hooks keys in perry and[]in Node (Node's are prototype getters).ORDER_DISAGREEScells are export-order fixes outside A2's declarations; each is a fix-forward candidate that leaves the list when fixed.