Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
d287c0d
realm: the restricted reach unites the program's own names with the g…
Nov 2, 2026
d9f699d
realm: every native birth declares its owner; the producer fills none…
Nov 2, 2026
2e915dc
realm: one object, one declaration: a native birth lists its other na…
Nov 3, 2026
92755b5
realm: a native namespace declared as an item beside its module (fs)
Nov 3, 2026
20b9237
realm: 45 native namespaces declared as items beside their keys (name…
Nov 3, 2026
22d05b9
realm: the 11 zero-member native namespaces declared as items with th…
Nov 3, 2026
beb2109
realm: the live key-table arms declared as items (Q10); 26 namespaces…
Nov 3, 2026
0280b89
realm: the native-module vtable is a fact of the namespace shape (Q8 …
Nov 3, 2026
1edf7ae
realm: births name their vtable; by-name reads take the codegen vtabl…
Nov 3, 2026
14d9742
realm: the seal fact reads the namespace birth; named-import IR tests…
Nov 3, 2026
1e00ef8
realm: thread the module vtable through bound_native_callable_export_…
Nov 3, 2026
5bf85b6
realm: delete the unkeyed by-name namespace birth; static exports car…
Nov 3, 2026
ba547f8
realm: delete the by-name externs nothing emits (js_create_native_mod…
Nov 3, 2026
2fb594c
realm: a <mod>.default callable births its base namespace through the…
Nov 3, 2026
1a2f409
hir: the named-import marker is spelled as the extern codegen emits (…
Nov 3, 2026
d6ecdb4
stdlib: the worker process notification births its namespace with NM_…
Nov 3, 2026
3005409
realm-diff gate: carry the S1 identity check and own-names seed onto …
Nov 3, 2026
772a363
lint pins: SSO review pin of node_stream_event_emitter.rs (only the p…
Nov 3, 2026
fc22992
runtime: a native namespace is born into its vtable's branded shape (Q8)
Nov 4, 2026
28bc998
realm: the default member of node:stream, node:events and node:module…
Nov 4, 2026
da91b7a
runtime: warnings and formatting in the files the realm work touches
Nov 4, 2026
7de7d2b
realm: native namespace members in Node order: assert.strict, child_p…
Nov 4, 2026
45b1c6b
realm: fs.default and http2.default members in Node order (declaratio…
Nov 4, 2026
0830b1a
realm: readline/promises exports in Node order (Interface, Readline, …
Nov 4, 2026
1321995
realm: assert/strict CJS export sets its members in the declared orde…
Nov 4, 2026
3f5e111
crypto.Certificate: a real prototype data property (constructor and t…
Nov 4, 2026
5c5717b
realm-dumps regenerated for the member order and Certificate.prototyp…
Nov 4, 2026
1da82c9
irdiff linkpad: pad perry_js_text (compiled JS bodies end the executa…
Nov 4, 2026
c8d9df0
realm-dumps regenerated on the rebase onto ced229822d (main added the…
Nov 4, 2026
7ec0105
realm-diff gate: the agreement check is an identity (A2-DESIGN 6.4): …
Nov 4, 2026
bb704fc
runtime test: a deferred builtin import resolves through the (name, v…
Nov 4, 2026
5e33bcb
warnings: the net vtable static is read inside the enclosing unsafe (…
Nov 4, 2026
87d66bf
changelog: realm owners and member order (A2b)
Nov 4, 2026
9fd739b
realm owners: a native receiver with no declared native_namespace is …
Nov 4, 2026
b4f2162
class_registry: the reserved native parent arms name their module vta…
Nov 4, 2026
1dcd04f
native_module: set_native_namespace_keys names its parameter key_list…
Nov 4, 2026
d62b2ae
native_export_collisions: a keyword-picked export arm (realm_decl! na…
Nov 4, 2026
b84b3eb
undo rustfmt-only churn in codegen dialect/mod.rs and fs/mod.rs (unre…
Nov 4, 2026
ee73bbd
shapes: the test-only brand-transition counter is a brand-keyed atomi…
Nov 4, 2026
da2f4a3
native_module: set_native_namespace_keys hands back the receiver thro…
Nov 4, 2026
ae3b5e1
runtime_abi.tsv regenerated (--emit-wasm-abi): the _vt entry rows and…
Nov 4, 2026
03f02d0
fs: the module vtable statics are read as fs::namespace::NM_FS (no re…
Nov 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5221,6 +5221,19 @@ jobs:
PERRY_RUNTIME_DIR: ${{ github.workspace }}/target/release
run: python3 scripts/realm_wgraph_gate.py --perry target/release/perry --output target/realm-wgraph --self-test

# WRT3p-A2 (DESIGN §3.2, "The R-restricted realm-diff gate"): the realm
# dumper's descriptor graph equals the checked-in base-arm references in
# realm-dumps/, byte for byte, in full mode and R-restricted for hello,
# lad-04-classes and lad-07-json. PERRY_REALM_REACH_NAMES is a test-only
# compiler-driver input the gate sets for the restricted dumper; the gate
# asserts it took effect (--self-test: a dumper built without it fails).
# References change only through `realm_diff_gate.py --regenerate` on main.
- name: Realm diff (full and R-restricted, test-only PERRY_REALM_REACH_NAMES)
if: runner.os == 'Linux'
env:
PERRY_RUNTIME_DIR: ${{ github.workspace }}/target/release
run: python3 scripts/realm_diff_gate.py --perry target/release/perry --output target/realm-diff --self-test

- name: Upload link absence evidence
if: always()
uses: ./.github/actions/perry/actions/upload-artifact # v7
Expand Down
1 change: 1 addition & 0 deletions changelog.d/0-realm-owners-and-member-order.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Every native namespace has one declaration beside its module: its owner, its other names and its members in Node's order. `Object.keys` reads that one list, so the hand key tables and the 89-arm match are gone, thirteen namespaces no longer leak an internal `__module__` key, sixteen namespaces list their members in Node's order, and `crypto.Certificate.prototype` is a real prototype. The module vtable is a fact of the namespace shape; a by-name lookup exists only in programs with a dynamic `require`. The realm-diff gate compares each namespace's member order with Node's.
1 change: 1 addition & 0 deletions crates/perry-abi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod accessor_guards;
mod builtin_proto;
pub use builtin_proto::{BuiltinProto, FunctionProtoKind};
pub mod intrinsic_serials;
pub mod native_vtable_brand;
pub mod object_meta_flags;
mod shape_kind;
pub use shape_kind::ShapeObjectKind;
Expand Down
67 changes: 67 additions & 0 deletions crates/perry-abi/src/native_vtable_brand.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! The native-module vtable as a shape fact.
//!
//! A native namespace object (`NATIVE_MODULE_CLASS_ID`) carries the vtable of
//! its module on its SHAPE, as one entry of the shape record's brand list:
//! `NATIVE_VTABLE_BRAND | address`. A brand is part of shape identity and is
//! carried by every transition, so two namespaces with the same keys but
//! different modules have different shapes, and the vtable survives
//! defineProperty, delete, preventExtensions, seal and freeze.
//!
//! The band `[1 << 60, 1 << 61)` (top nibble 1) is produced by nothing else:
//! template brands are u32, the Array exotic brand is `(1 << 61) | 1`, the
//! evaluation holder is bit 62, fresh private brands bit 63, and NaN-boxed
//! values (the private evaluation brand) have top nibble 7 or F. A static
//! address is below `1 << 48`, so `(1 << 60) | address` is unambiguous.
//!
//! This is the one definition: the runtime birth and the image-row
//! registration produce the brand with [`native_vtable_brand`], every reader
//! decodes it with [`native_vtable_of`]. The brand is not a heap pointer: no
//! collector traces it (only the bit-62 holder is a traced edge).

/// The brand band of a native-module vtable address.
pub const NATIVE_VTABLE_BRAND: u64 = 1 << 60;

/// The brand that records vtable `address` on a shape.
#[inline]
pub const fn native_vtable_brand(address: usize) -> u64 {
let address = address as u64;
debug_assert!(address != 0 && address < (1 << 48));
NATIVE_VTABLE_BRAND | address
}

/// Is `brand` a native-module vtable brand?
#[inline]
pub const fn is_native_vtable_brand(brand: u64) -> bool {
brand >> 60 == 1
}

/// The vtable address a sorted brand list records, if any.
#[inline]
pub fn native_vtable_of(brands: &[u64]) -> Option<usize> {
let at = brands.partition_point(|&brand| brand < NATIVE_VTABLE_BRAND);
let brand = *brands.get(at)?;
is_native_vtable_brand(brand).then_some((brand & !NATIVE_VTABLE_BRAND) as usize)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn round_trips_and_ignores_other_bands() {
let vt = 0x5555_1234_5678usize;
let brand = native_vtable_brand(vt);
assert!(is_native_vtable_brand(brand));
assert_eq!(
native_vtable_of(&[7, brand, (1 << 61) | 1, 1 << 62]),
Some(vt)
);
assert_eq!(
native_vtable_of(&[7, (1 << 61) | 1, 1 << 62, 1 << 63]),
None
);
assert_eq!(native_vtable_of(&[]), None);
assert!(!is_native_vtable_brand(0x7FFD_0000_0000_0001));
assert!(!is_native_vtable_brand(u64::MAX - 2));
}
}
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/codegen/artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ pub(super) fn emit_module_artifacts(
for prefix in foreign_prefixes {
// #3938: `__native_mod__<name>` / `__node_submod__<key>` sentinel
// prefixes are resolved at the `Expr::DynamicImport` dispatch site
// via runtime namespace builders (`js_create_native_module_namespace`
// via runtime namespace builders (`js_create_native_module_namespace_vt`
// / `js_node_submodule_namespace`); they have no compiled-module
// `@__perry_ns_<prefix>` global or `<prefix>__init` function, so the
// extern decls below are dead. Worse, for slash-bearing submodule
Expand Down
40 changes: 31 additions & 9 deletions crates/perry-codegen/src/codegen/buffer_alias_metadata_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ fn module_with_scopes(per_function: &[usize]) -> (LlModule, Vec<u32>) {
fn numbered_definitions(ir: &str) -> HashMap<u32, usize> {
let mut defs = HashMap::new();
for line in ir.lines() {
let Some(rest) = line.strip_prefix('!') else { continue };
let Some((id, _)) = rest.split_once(" = ") else { continue };
let Some(rest) = line.strip_prefix('!') else {
continue;
};
let Some((id, _)) = rest.split_once(" = ") else {
continue;
};
if let Ok(id) = id.parse::<u32>() {
*defs.entry(id).or_insert(0) += 1;
}
Expand All @@ -45,11 +49,22 @@ fn more_than_a_hundred_buffer_scopes_define_every_id_once() {
assert_eq!(scopes.len(), 150);
let ir = llmod.to_ir();
let defs = numbered_definitions(&ir);
let twice: Vec<_> = defs.iter().filter(|(_, n)| **n > 1).map(|(id, _)| *id).collect();
assert!(twice.is_empty(), "metadata ids defined more than once: {twice:?}");
let twice: Vec<_> = defs
.iter()
.filter(|(_, n)| **n > 1)
.map(|(id, _)| *id)
.collect();
assert!(
twice.is_empty(),
"metadata ids defined more than once: {twice:?}"
);
for &scope in &scopes {
for id in [scope, scope + 1, scope + 2] {
assert_eq!(defs.get(&id), Some(&1), "!{id} of scope !{scope} is not defined once");
assert_eq!(
defs.get(&id),
Some(&1),
"!{id} of scope !{scope} is not defined once"
);
}
// The node is a scope, its list names it, its noalias list omits it.
let line = |id: u32| {
Expand All @@ -60,8 +75,10 @@ fn more_than_a_hundred_buffer_scopes_define_every_id_once() {
};
assert!(line(scope).contains("distinct"), "{}", line(scope));
assert_eq!(line(scope + 1), format!("!{} = !{{!{scope}}}", scope + 1));
assert!(!line(scope + 2).contains(&format!("!{scope},"))
&& !line(scope + 2).ends_with(&format!("!{scope}}}")));
assert!(
!line(scope + 2).contains(&format!("!{scope},"))
&& !line(scope + 2).ends_with(&format!("!{scope}}}"))
);
}
}

Expand All @@ -82,7 +99,9 @@ fn llvm_parses_a_module_with_more_than_a_hundred_buffer_scopes() {
let ctx = inkwell::context::Context::create();
let module = crate::inprocess::parse_ir_text(&ctx, &ir, "alias_scopes")
.unwrap_or_else(|e| panic!("LLVM rejected the alias tables: {e:#}"));
module.verify().unwrap_or_else(|e| panic!("{}", e.to_string()));
module
.verify()
.unwrap_or_else(|e| panic!("{}", e.to_string()));
}

#[test]
Expand All @@ -91,5 +110,8 @@ fn loop_ids_follow_the_alias_tables_from_the_same_counter() {
let after = llmod.allocate_metadata_id();
let last = *scopes.iter().max().unwrap();
// scope ids, then their lists, then the domain: all below the next id.
assert!(after > last + 2, "counter {after} is not past scope !{last}'s lists");
assert!(
after > last + 2,
"counter {after} is not past scope !{last}'s lists"
);
}
11 changes: 11 additions & 0 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,14 @@ pub(super) fn compile_module_entry(
}
None => Vec::new(),
};
// A program that reaches a builtin by a runtime string
// (`process.getBuiltinModule(id)`, `require(expr)`, `createRequire`)
// carries the `(name, vtable)` rows of every native module; no other
// program names a module it does not import.
let native_module_table = cross_module
.app_metadata
.native_module_table
.then(|| crate::nm_install::emit_native_module_table(llmod));
let main = if is_dylib {
llmod.define_function("perry_module_init", VOID, vec![])
} else {
Expand Down Expand Up @@ -573,6 +581,9 @@ pub(super) fn compile_module_entry(
}
// #10428/#10429: linked providers register their module-export
// dispatchers before any module init (see `native_provider_installs`).
if let Some((table, count)) = &native_module_table {
blk.call_void("js_nm_set_module_table", &[(PTR, table), (I64, count)]);
}
for install in &cross_module.app_metadata.native_provider_installs {
blk.call_void(install, &[]);
}
Expand Down
9 changes: 7 additions & 2 deletions crates/perry-codegen/src/codegen/export_value_wrappers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,15 @@ pub(super) fn emit_export_value_wrappers(c: ExportValueWrapperCtx<'_>) {
}
let source_value = blk.load(DOUBLE, &source_handle);
let imported_value = blk.load(DOUBLE, &imported_handle);
let vtable = crate::nm_install::nm_vtable_ref(source);
let value = blk.call(
DOUBLE,
"js_native_module_named_esm_export_value",
&[(DOUBLE, &source_value), (DOUBLE, &imported_value)],
"js_native_module_named_esm_export_value_vt",
&[
(crate::types::PTR, &vtable),
(DOUBLE, &source_value),
(DOUBLE, &imported_value),
],
);
blk.ret(DOUBLE, &value);
}
Expand Down
5 changes: 3 additions & 2 deletions crates/perry-codegen/src/codegen/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1452,10 +1452,11 @@ pub(super) fn emit_namespace_populator(
if let Some(install) = crate::nm_install::nm_install_symbol(name) {
blk.call_void(install, &[]);
}
let vtable = crate::nm_install::nm_vtable_ref(name);
blk.call(
DOUBLE,
"js_create_native_module_namespace",
&[(PTR, &name_global), (I64, &name_len)],
"js_create_native_module_namespace_vt",
&[(PTR, &vtable), (PTR, &name_global), (I64, &name_len)],
)
}
};
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,9 @@ mod ctor_arity;
pub use ctor_arity::{
context_free_ctor_abi, context_free_ctor_param_count, CtorAbi, UNRESOLVED_PARENT_FWD_ARITY,
};
mod checked_integer;
#[cfg(test)]
mod buffer_alias_metadata_tests;
mod checked_integer;
#[cfg(test)]
mod declared_string_add_tests;
#[cfg(test)]
Expand Down
4 changes: 3 additions & 1 deletion crates/perry-codegen/src/codegen/native_namespace_exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ pub(super) fn emit_native_namespace_reexport_getters(
if let Some(install) = crate::nm_install::nm_install_symbol(source) {
blk.call_void(install, &[]);
}
let vtable = crate::nm_install::nm_vtable_ref(source);
let value = blk.call(
DOUBLE,
"js_create_native_module_namespace",
"js_create_native_module_namespace_vt",
&[
(PTR, &vtable),
(PTR, &format!("@{}", source_global)),
(I64, &source_len.to_string()),
],
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-codegen/src/codegen/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ pub struct AppMetadata {
/// `createRequire` — already reach the provider's export dispatcher.
/// (#10428, #10429)
pub native_provider_installs: Vec<String>,
/// The program reaches builtins by runtime strings: the entry emits the
/// `(name, vtable)` rows of every native module (`@perry_native_modules`).
pub native_module_table: bool,
}

impl Default for AppMetadata {
Expand All @@ -59,6 +62,7 @@ impl Default for AppMetadata {
entry_source_path: None,
entry_declared_esm: None,
native_provider_installs: Vec::new(),
native_module_table: false,
}
}
}
Expand Down
24 changes: 24 additions & 0 deletions crates/perry-codegen/src/codegen/realm_image/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,30 @@ mod tests {
assert!(function_has_prototype_state(&plan.cells["AggregateError"]).unwrap());
}

#[test]
fn constructor_prototype_entries_follow_the_declared_writable_attribute() {
// The bag entry is the image's descriptor: a function-style class
// (Buffer, declared `attrs = {prototype = writable}`) keeps a writable
// `prototype`, and the error constructors, which declare none, do
// not; both agree with the heap and with Node.
let hir = perry_parser::parse_typescript("with ({}) {}", "realm.cjs").unwrap();
let reach = perry_hir::realm_reach::scan(&hir, crate::realm_manifest::REALM);
let plan = crate::realm_plan::RealmPlan::from_reach(&reach).unwrap();
let writable = |name: &str| {
let property = plan.cells[name]
.properties
.iter()
.find(|p| !p.symbol && p.key == "prototype")
.unwrap_or_else(|| panic!("{name} has no prototype property"));
property_entry(property) & 1 == 0
};
assert!(writable("Buffer"));
assert!(writable("node:events.EventEmitter"));
for name in ["Error", "TypeError", "SyntaxError", "RangeError"] {
assert!(!writable(name), "{name}.prototype is non-writable");
}
}

#[test]
fn body_kind_defaults_resolve_manifest_cells_and_overrides_need_state() {
let hir = perry_parser::parse_typescript("with ({}) {}", "realm.cjs").unwrap();
Expand Down
20 changes: 13 additions & 7 deletions crates/perry-codegen/src/collectors/ptr_shape_elements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -571,9 +571,7 @@ pub(crate) fn collect_element_shape_facts(
&& !module_globals.contains_key(&carrier)
}
};
if !index_ok
|| boxed_vars.contains(&read.index)
|| module_globals.contains_key(&read.index)
if !index_ok || boxed_vars.contains(&read.index) || module_globals.contains_key(&read.index)
{
continue;
}
Expand Down Expand Up @@ -728,7 +726,10 @@ fn runtime_call<'e>(e: &'e Expr, name: &str) -> Option<&'e [Expr]> {
}

fn is_record_index_zero(e: &Expr) -> bool {
matches!(runtime_call(e, "arrayRecordIndex"), Some([Expr::Integer(0)]))
matches!(
runtime_call(e, "arrayRecordIndex"),
Some([Expr::Integer(0)])
)
}

/// `arrayRecordPayload(s, p := mode ? GetIterator(s) : s, s := undefined)`
Expand Down Expand Up @@ -810,7 +811,11 @@ impl<'a> ArrayWalk<'a> {
/// `v = ForValue(mode, v, A[arrayRecordIndex(i)]); state = n; const r = v`.
/// On a match the read is licensed (with `v` as its carrier) and the rest
/// of the body is returned for the ordinary walk.
fn record_binding_prefix<'b>(&mut self, body: &'b [Stmt], bound: (u32, u32)) -> Option<&'b [Stmt]> {
fn record_binding_prefix<'b>(
&mut self,
body: &'b [Stmt],
bound: (u32, u32),
) -> Option<&'b [Stmt]> {
let (index, root) = bound;
let [Stmt::Expr(Expr::LocalSet(value, read)), Stmt::Expr(Expr::LocalSet(state, state_value)), Stmt::Let {
id: local,
Expand All @@ -820,8 +825,9 @@ impl<'a> ArrayWalk<'a> {
else {
return None;
};
let [mode, Expr::LocalGet(previous), Expr::IndexGet { object, index: key, .. }] =
runtime_call(read, "arrayRecordForValue")?
let [mode, Expr::LocalGet(previous), Expr::IndexGet {
object, index: key, ..
}] = runtime_call(read, "arrayRecordForValue")?
else {
return None;
};
Expand Down
17 changes: 11 additions & 6 deletions crates/perry-codegen/src/collectors/sealed_buffers_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,14 +428,19 @@ fn a_returned_parameter_follows_its_arguments() {
fresh(10),
fresh(11),
Stmt::Expr(get(10, call(1, vec![Expr::Integer(2)]))),
Stmt::Expr(get(
11,
call(2, vec![Expr::String("buffer".to_string())]),
)),
Stmt::Expr(get(11, call(2, vec![Expr::String("buffer".to_string())]))),
],
vec![
func(1, vec![param(20)], vec![Stmt::Return(Some(Expr::LocalGet(20)))]),
func(2, vec![param(30)], vec![Stmt::Return(Some(Expr::LocalGet(30)))]),
func(
1,
vec![param(20)],
vec![Stmt::Return(Some(Expr::LocalGet(20)))],
),
func(
2,
vec![param(30)],
vec![Stmt::Return(Some(Expr::LocalGet(30)))],
),
],
);
let exposed = buffer_exposure(&m).exposed;
Expand Down
Loading
Loading