Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
6ea5366
feat(runtime): canonical keys arrays -- one array per ordered key lis…
perry-bot Sep 22, 2026
c369483
fix(runtime): canonical keys arrays must be LONGLIVED -- the shape ca…
perry-bot Sep 22, 2026
55f1f9a
feat(runtime): LiveObject -- a receiver that a collecting call has to…
perry-bot Sep 22, 2026
a4203b1
feat(runtime): SharedLayout -- interning requires PROOF the receiver …
perry-bot Sep 22, 2026
f3b35d7
fix(runtime): the latch test helpers must RESTORE, not assume the def…
perry-bot Sep 22, 2026
05f7709
fix(runtime): the dictionary latch STATE is per_test_global! (#10868 …
perry-bot Sep 22, 2026
69d7cbe
fix(runtime): scan and prune CLASS_KEYS_BY_ID -- the side table the c…
perry-bot Sep 22, 2026
02185e2
test(runtime): delete in_place_owned_append_leaves_one_descriptor_per…
perry-bot Sep 22, 2026
52a654c
fix(runtime): longlived ONLY for the shape cache entry -- the invaria…
perry-bot Sep 22, 2026
995d779
test(runtime): repair distinct-layout fixtures for canonical keys
Sep 22, 2026
5335202
fix(runtime): compare canonical key slots without JavaScript hole con…
perry-bot Sep 22, 2026
59ebf89
test(runtime): a re-add after a tombstone publishes a successor shape…
Sep 22, 2026
c9369a9
fix(runtime): satisfy canonical shape identity merge gates (#10969)
Sep 22, 2026
00ae916
Hoist canonical shape proofs for non-dispatching property read loops
Sep 22, 2026
72d4533
Keep scalar-replaced receivers on their existing field lowering
Sep 22, 2026
1f7a90a
Resolve canonical loop shapes once and preserve baseline misses
Sep 22, 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
22 changes: 22 additions & 0 deletions changelog.d/10969-canonical-shape-fixtures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Repair two runtime fixtures whose distinct-class premise ceased to imply
distinct layouts after canonical key interning. The numeric-write guard now
sees the same target names in different ordered slots (`a,b,c,d` versus
`a,b,d,c`). The imported typed-shape fixture uses different key names at the
same slot count, independently of its existing slot-count mismatch. Both
assert distinct key arrays and retain their rejection assertions.

Validation uses release builds with `CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16`,
the 24 GiB suite cap, and `--test-threads=1`. Each repaired test passes in its
own process. Disabling the production uniform-ShapeId check or the imported
keys-validation check makes the corresponding repaired test fail on its own
rejection assertion; both sabotages are reverted.

The delete-lane seam remains deliberately unconverted. A temporary probe of
`tombstone_hole_count_survives_readd_append` observed the re-add replacing an
owned keys array with a different, shared canonical array and changing the
ShapeId, while retaining one hole. Its unchanged hole-accounting and churn
bound assertions passed: 60 churn cycles ended with 20 stored slots. The
original stable-identity assertion remains intact and failing. Resolving the
owned delete/re-add contract requires the deferred delete-lane conversion;
no special-case bypass of canonicalization is added. The requested stop at
that boundary leaves the tsc A/B unrun; no performance result is claimed.
33 changes: 33 additions & 0 deletions changelog.d/10969-canonical-shape-gates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Make canonical shape identity pass the merge-train custody and inventory gates.

Split basic object allocation and coercion into `object/alloc_basic.rs`,
with explicit named re-exports preserving both existing entry-point paths.
Canonical key allocation reloads its parent and appended key through
`RuntimeHandle::across_*`; key-list reads use scoped `with_const_ptr`.
`LiveObject::across` and `js_object_set_keys` likewise couple the receiver
reload to the operation that can collect. No raw-handle ceiling is added.

Canonical-array shared flags use the tracked-header resolver instead of
unchecked GcHeader casts. Keep existing birth-flag helpers in alloc.rs to
avoid adding ownership probes to unchanged object-construction paths. The canonical
trie's production TLS uses `perry_thread_local!`. Move canonical-key tests
and their slot-read counter into a cfg(test) module, using Perry TLS there
as well. The counter increments once per candidate slot in `probe`, reached
from `extend_slot` (via `extend_key` and `canonicalize`); no counter storage,
call or branch is compiled in production, including before this change.

Route both weak layout scanners through the existing object-cache scanner,
keeping the collector's pinned source unchanged from main and preserving
weak visitation before the other object caches. Name the slot extension
`extend_slot` to distinguish it from collection `extend` in the holder
checker's conservative name-based call graph; the previous collision falsely
made the regex EXEC_LOOKUPS test counter look scanner-covered. No holder
inventory or diagnostic-source pin is relaxed.

Refresh only the two changed object/mod.rs declarations using
`shape_descriptor_census.py --emit-baseline`: shape_cache_insert's former
single-line declaration becomes one additional `keys_array: *mut ArrayHeader,`
entry (2 -> 3) because it now accepts/returns LiveObject along with canonical
keys; test_shape_cache_insert's declaration now returns `*mut ArrayHeader`.
Callers must receive the canonical array, whose identity can differ from the
incoming private list. All other census entries and summary counts stay fixed.
29 changes: 29 additions & 0 deletions changelog.d/astra-licm25-canonical-read-loops.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Counted numeric property-read loops resolve each predicted ordered key list once
at module initialization. A registered guard-shape global holds the expectation;
its external-carrier descriptor roots the keys even with no live receiver. The
preheader compares the receiver's header shape once, and the body uses constant
slot offsets. A miss runs the original loop lowering, including its existing
read optimizations. Calls observed only with short constant bounds keep that
lowering without a preheader, so frequent short misses pay no versioning cost.

Admission is a closed grammar: one numeric addition reduction, an unchanged
receiver, a primitive bound and counter, and no calls or existing-object stores.
Numeric prechecks exclude implicit coercion callbacks. A fresh array of primitive
literals/counter values may be assigned to a separate, otherwise unread binding;
that allocation cannot dispatch JavaScript or alter the receiver. Direct module
roots have the same proof as local roots. Captures and boxed bindings are refused.
Moving polls remain enabled, and each iteration derives the receiver address
again from its registered root.

Shape identity is not universally immutable: dictionary key lists and private
stable-tombstone epochs can change while retaining their IDs. The prediction
resolves an ordinary, generation-zero, hole-free, entirely inline canonical
shape, so neither exception can match it. At the base SHA dictionary restamping
still supplies `Ordinary`; exclusion depends on its distinct generation and
keyless descriptor, not that enum tag. GC relocates payloads and shape key edges
without changing the receiver's ShapeId or slot order.

Coverage includes emitted guard/constant-slot assertions, refusal of stores,
calls and receiver reassignment, canonical identity across construction paths,
dictionary exclusion, Node differential cases for getters/proxies/coercions and
non-objects, and an allocating moving-GC fixture in the root-dominance corpus.
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,8 @@ pub(super) fn compile_closure(
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
canonical_read_loop: None,
canonical_read_short_bounds: std::collections::HashSet::new(),
element_shape_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
numeric_accumulator_f64_slots: HashMap::new(),
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,8 @@ pub(super) fn compile_module_entry(
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
canonical_read_loop: None,
canonical_read_short_bounds: std::collections::HashSet::new(),
element_shape_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
numeric_accumulator_f64_slots: HashMap::new(),
Expand Down Expand Up @@ -1639,6 +1641,8 @@ pub(super) fn compile_module_entry(
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
canonical_read_loop: None,
canonical_read_short_bounds: std::collections::HashSet::new(),
element_shape_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
numeric_accumulator_f64_slots: HashMap::new(),
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1257,6 +1257,8 @@ pub(super) fn compile_function(
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
canonical_read_loop: None,
canonical_read_short_bounds: cross_module.canonical_read_short_bounds.get(&f.id).cloned().unwrap_or_default(),
element_shape_loop_facts: Vec::new(),
// Specialized entries seed the canonical-i32 registry with their raw
// i32 params (empty otherwise — identical to the pre-phase behavior).
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,8 @@ pub(super) fn compile_method(
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
canonical_read_loop: None,
canonical_read_short_bounds: std::collections::HashSet::new(),
element_shape_loop_facts: Vec::new(),
i32_counter_slots: index_i32_param_slots,
numeric_accumulator_f64_slots: HashMap::new(),
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/method_static.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,8 @@ pub(in crate::codegen) fn compile_static_method(
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
canonical_read_loop: None,
canonical_read_short_bounds: std::collections::HashSet::new(),
element_shape_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
numeric_accumulator_f64_slots: HashMap::new(),
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2456,6 +2456,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
}

let mut cross_module = CrossModuleCtx {
canonical_read_short_bounds: crate::stmt::canonical_read_profit::short_bounds(hir),
namespace_imports: opts.namespace_imports.iter().cloned().collect(),
namespace_member_nested: opts.namespace_member_nested.iter().cloned().collect(),
namespace_member_prefixes: opts.namespace_member_prefixes,
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,7 @@ impl ImportedCtor {
/// adding five more individual parameters to every compile_* function.
/// Built once in `compile_module` from `CompileOptions`.
pub(crate) struct CrossModuleCtx {
pub canonical_read_short_bounds: std::collections::HashMap<u32, std::collections::HashSet<u32>>,
pub namespace_imports: std::collections::HashSet<String>,
/// #7189: `(namespace local, member)` pairs whose member is itself a MODULE
/// NAMESPACE, from `export * as ns from "./m.ts"` in the imported module.
Expand Down
29 changes: 29 additions & 0 deletions crates/perry-codegen/src/codegen/string_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,17 @@ pub(super) fn emit_string_pool(
}
}

// One rooted shape expectation per ordered prediction, resolved at module init.
for (keys, name) in &strings.canonical_read_shapes {
let bytes: Vec<u8> = keys.iter().flat_map(|k| k.bytes().chain(Some(0))).collect();
let encoded: String = bytes.iter().map(|b| format!("\\{b:02X}")).collect();
llmod.add_raw_global(format!("@{name} = internal global i32 -1, align 4"));
llmod.add_raw_global(format!(
"@{name}_keys = private constant [{} x i8] c\"{encoded}\", align 1",
bytes.len()
));
}

// #5391 function splitting: a large bundle interns ~190K strings AND
// registers tens of thousands of closures/classes/functions; emitting all of
// that into ONE `__perry_init_strings` function produced a single ~32MB /
Expand All @@ -436,6 +447,24 @@ pub(super) fn emit_string_pool(
ops_per_chunk,
);

for (keys, name) in &strings.canonical_read_shapes {
chunker.roll_if_full();
let blk = chunker.current_block();
let length: usize = keys.iter().map(|k| k.len() + 1).sum();
// js_object_shape_id_for_keys marks these keys as an external carrier:
// the weak canonical trie cannot collect the remembered descriptor.
let shape = blk.call(
I32,
"js_canonical_read_shape",
&[(PTR, &format!("@{name}_keys")), (I32, &length.to_string())],
);
blk.store(I32, &shape, &format!("@{name}"));
blk.call_void(
"js_register_class_guard_shape",
&[(PTR, &format!("@{name}"))],
);
}

for entry in strings.iter() {
chunker.roll_if_full();
let blk = chunker.current_block();
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-codegen/src/expr/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// one. Handlers that care receive it as an argument, because they consult
// it after lowering their operands — by which point the field is gone.
let value_discarded = std::mem::take(&mut ctx.discard_this_expr);
if let Some(value) = crate::stmt::canonical_read_loop::lower_reduction(ctx, expr)? {
return Ok(value);
}
if let Some(value) = super::suffix_cursor::try_lower(ctx, expr)? {
return Ok(value);
}
Expand Down
7 changes: 7 additions & 0 deletions crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,12 @@ pub(crate) struct FnCtx<'a> {
/// current iteration.
pub class_field_loop_facts: Vec<ClassFieldLoopFact>,

/// A canonical ordinary-shape proof scoped to a non-dispatching loop.
/// Carries local/key identities only, never a raw heap address.
pub canonical_read_loop: Option<crate::stmt::canonical_read_loop::Fact>,
/// Bound parameters observed only in short calls; a profitability hint.
pub canonical_read_short_bounds: std::collections::HashSet<u32>,

/// repsel #7480 / #5093: scoped loop-versioning facts for element-shape
/// loops (`for (…) sum += arr[i].field`). Pushed only around the FAST
/// clone of `lower_element_shape_versioned_for`
Expand Down Expand Up @@ -3049,6 +3055,7 @@ pub(crate) mod element_shape_guard;
pub(crate) mod element_shape_reads;
mod js_runtime;
mod literals_vars;
pub(crate) use literals_vars::bind_lowered_value_to_local;
mod logical_collections;
mod math_simple;
mod misc_methods;
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/runtime_decls/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,8 @@ pub fn declare_phase_b_strings(module: &mut LlModule) {
I64,
&[I32, I32, I32, I64, I32],
);
module.declare_function("llvm.expect.i1", I1, &[I1, I1]);
module.declare_function("js_canonical_read_shape", I32, &[PTR, I32]);
module.declare_function("js_build_class_keys_array", I64, &[I32, I32, PTR, I32]);
module.declare_function("js_object_shape_id_for_keys", I32, &[I64, I32]);
module.declare_function("js_register_class_guard_shape", VOID, &[PTR]);
Expand Down
Loading
Loading