Skip to content

land #8833: ECS specializations, with the argument-route guard restored - #8837

Merged
proggeramlug merged 4 commits into
mainfrom
land-8833-ecs
Aug 25, 2026
Merged

land #8833: ECS specializations, with the argument-route guard restored#8837
proggeramlug merged 4 commits into
mainfrom
land-8833-ecs

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Lands #8833 (perf: complete ECS benchmark specializations) with a real soundness bug fixed, not a test updated.

The two failing tests were a symptom, and the fix was in the producing code

#8833 as submitted failed two perry-codegen tests. The second one —

a call-argument escape must be reported as such, not as a bare reference:
Some("passed to a call that does not preserve whole-lifetime containment. A guarded
`$pshape_args` route may still specialize field reads performed before the callee
publishes the argument…")

— was not a stale assertion. The reword was the visible surface of a substantive change: the PR widened the $pshape_args route in four mutually load-bearing ways and removed the runtime class+ShapeId guard in the same change.

widening where
callee may publish the parameter (ReadOnlyParamUsePrefixContainedParamUse) collectors/proven_args.rs
caller's post-call containment requirement dropped (require_post_call_containment = false) collectors/ptr_shape.rs
rule 5's module-wide §5.2 barrier kill bypassed (CollectionPurpose::GuardedArgumentRoute) collectors/ptr_shape_entry.rs
runtime guard elided (ptr_shape_argument_route_fact returned (fact, false)) expr/mod.rs

ptr_shape.rs's own doc says rule 5 is "belt-and-braces against analysis blind spots", justified by rule 2's containment — and the PR removed containment in the same breath. Its doc comments contradicted its code in three further places (hir_facts.rs: "must never license guard-free field access"; ptr_shape_entry.rs: "consumable only beside a live guard"; proven_args.rs: "every route revalidates the live class and shape").

Root cause

PrefixContainedParamUse proves a temporal property — reads happen before publication. But guarded_argument_route_locals is a HashMap<u32, PtrShapeLocal> keyed by local id, i.e. flow-insensitive, and is consulted at every route site for that local, including sites executing after the alias exists. A per-local map cannot express "before". The PR's own wording — "the caller cannot retain an unguarded Ptr<Shape> fact after that call" — states a constraint its data structure cannot enforce. This is the #7773 family: a declared property is not a proof; only a live guard is.

Demonstrated, not argued

A probe fixture (module with an unattributable Object.defineProperty; callee reading entity.id then returning entity; two registry.read(entity) sites) made the pre-fix compiler emit:

PROBE routed=true fallback_present=false n_clone_calls=2
%r153 = call double @...__Registry__read$pshape_args(...)
%r197 = call double @...__Registry__read$pshape_args(...)

Two unguarded direct calls into a clone performing fixed-offset field loads — the second on an object the compiler itself recorded as published to an alias it cannot see, in a module its own rule 5 says carries an unbounded shape barrier. On clean main that fixture routes nowhere.

The fix

  1. expr/mod.rs — route-only facts now require the guard ((fact, true)). Lookup order is proven_shape_params (guard) → shape_proven_ptr_local (elide) → guarded_argument_route_local (guard). Elision survives only where it is justified: the caller already holds the broad Ptr<Shape> fact, proven in a barrier-free module under full containment, so it is already licensed to read those fields at fixed offsets and the guard is tautological.
  2. proven_args.rs / ptr_shape.rs — route admission always requires preserves_containment; the require_post_call_containment knob, whose only other mode was unsound, is deleted rather than left selectable (CLAUDE.md's kill policy: a mode that still exists is a decision that hasn't been made).
  3. ptr_shape_report.rs — restored "passed as a call argument"; the test assertion is untouched and its discrimination intact.
  4. Testsunrelated_module_shape_barrier_keeps_guarded_argument_route now asserts the guard/fallback is retained; field_read_before_terminal_publication_gets_only_the_guarded_routepublishing_clone_gets_no_caller_side_route; new published_argument_in_a_barrier_module_never_reaches_an_unguarded_clone, which asserts its own subject is live (the clone must still be emitted) and was sabotage-checked against the pre-fix code, where it fails with 2 clone calls / 0 guard blocks.

Failure 1 — allowlist deliberately not widened

The $pshape_args offenders (hir_facts.rs, ptr_shape_report.rs, and ptr_shape_entry.rs which the merge exposed) mention the fragment only in prose — a doc comment and a diagnostic string. A doc comment cannot register a vtable entry, and any code that builds the name goes through pshape_args_method_name in the already-allowlisted proven_args.rs. Allowlisting three prose files would permanently exempt them from the real check, so the prose was reworded instead. The allowlist stays at 10 entries.

A fifth gate failure, not in the original report

local_binding_type_audit was also red: #8833 hoisted unique_global out of codegen/mod.rs into helpers::unique_class_keys_global, moving the attribution of an unchanged module_local_types read to compile_module. Allowlist entry refreshed; count, classification and reason unchanged.

Validation

  • all 30 lint-job gates pass, plus addr_class_inventory --self-test and local_binding_type_audit --self-test
  • perry-codegen 1265, perry-runtime 2697, perry-stdlib 120, perry-hir 337 — all 0 failed
  • issue_8774_argument_shape_clones (Node parity + forced-moving-GC arms): 2 passed, 0 failed
  • Measured routes unaffected: the perform-ecs/Wolf fixtures are fresh contained locals in barrier-free modules and keep the elided guard, so the e2e's !ir.contains("pshape_arg.fallback") assertions still hold. The perf(codegen): propagate Ptr<Shape> facts into guarded argument clones #8774 slice carried no speed claim ("0.24% slower (flat), 4/11 wins"), so removing the widening costs nothing measurable.

An earlier run of the e2e failed with ENOSPC at 412Mi free; those results were discarded rather than interpreted, and the numbers above come from a clean re-run with 18Gi, verified by df before and after.

Summary by CodeRabbit

  • New Features
    • Improved optimized method calls across modules, including short spread calls and imported object methods.
    • Added safer specialization for exact-shape argument clones, with runtime checks retained when needed.
    • Enhanced packed-array and array-subclass spread handling, including iterator-compatible fallbacks for custom arrays and proxies.
    • Added live validation for packed-loop assumptions after observable changes.
  • Bug Fixes
    • Corrected mixed fixed-and-spread Math.min and Math.max calls.
    • Null and undefined spread values now correctly contribute no arguments.
  • Tests
    • Added broad regression coverage for cross-module dispatch, closures, loops, spreads, and method caching.

Ralph Küpper added 4 commits August 25, 2026 22:44
…ll is bypassed

#8833 widened the `$pshape_args` route in three directions at once and removed
the runtime class+ShapeId guard at the same time. Individually each widening is
arguable; composed, they left an unguarded fixed-offset read of an object the
compiler itself had recorded as published to an alias it cannot see.

Reproduced as a codegen ratchet: a module with an unattributable
`Object.defineProperty`, a callee that publishes its parameter after its
licensed read, and two route sites on the same caller local emitted two
unguarded direct calls into the clone (2 clone calls, 0 guard blocks).

Two changes restore the invariant the PR's own doc comments assert:

- The route-only fact is collected with rule 5's module-wide shape-barrier kill
  bypassed, so it must never license guard-free field access. Its route now
  keeps the runtime guard plus the generic fallback. Guard elision is retained
  only where the caller holds the broad `Ptr<Shape>` representation fact, which
  was proven in a barrier-free module under full containment and where the
  guard is therefore tautological.

- Route admission requires the clone to preserve containment for the
  parameter's whole lifetime. `PrefixContainedParamUse` proves a temporal
  property ("the reads happen before the publication"), but the fact map that
  carries a caller-side route is keyed by local id and is flow-insensitive, so
  a fact kept past a publishing call is consulted again at every later route
  site. The `require_post_call_containment` knob, whose only other mode was
  unsound, is deleted rather than left selectable.

Also refreshes the `local_binding_type_audit` allowlist: #8833 hoisted
`unique_global` out of `codegen/mod.rs`, moving the attribution of an unchanged
`module_local_types` read to `compile_module`.

The measured `perform-ecs` and Wolf routes are unaffected (fresh contained
locals in barrier-free modules); the #8774 slice carried no speed claim.
@proggeramlug
proggeramlug merged commit 8a55a26 into main Aug 25, 2026
16 of 20 checks passed
@proggeramlug
proggeramlug deleted the land-8833-ecs branch August 25, 2026 21:08
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 15f25f76-74cd-46cb-b9aa-34e9899d878d

📥 Commits

Reviewing files that changed from the base of the PR and between 32cc6ea and 6a87729.

📒 Files selected for processing (96)
  • changelog.d/8833-argument-route-guard-invariant.md
  • changelog.d/8833-ecs-integration-followthrough.md
  • crates/perry-codegen/src/codegen/argument_shape_clone_tests.rs
  • crates/perry-codegen/src/codegen/closure.rs
  • crates/perry-codegen/src/codegen/emission_order_tests.rs
  • crates/perry-codegen/src/codegen/entry.rs
  • crates/perry-codegen/src/codegen/entry/tests.rs
  • crates/perry-codegen/src/codegen/function.rs
  • crates/perry-codegen/src/codegen/helpers.rs
  • crates/perry-codegen/src/codegen/method.rs
  • crates/perry-codegen/src/codegen/mod.rs
  • crates/perry-codegen/src/codegen/number_exactness_tests.rs
  • crates/perry-codegen/src/codegen/opts.rs
  • crates/perry-codegen/src/collectors/hir_facts.rs
  • crates/perry-codegen/src/collectors/object_literal_exports.rs
  • crates/perry-codegen/src/collectors/proven_args.rs
  • crates/perry-codegen/src/collectors/proven_this_routing_tests.rs
  • crates/perry-codegen/src/collectors/ptr_shape.rs
  • crates/perry-codegen/src/collectors/ptr_shape_entry.rs
  • crates/perry-codegen/src/collectors/ptr_shape_report.rs
  • crates/perry-codegen/src/collectors/scalar_method_dispatch.rs
  • crates/perry-codegen/src/expr/array_push_guard_tests.rs
  • crates/perry-codegen/src/expr/call_spread_short.rs
  • crates/perry-codegen/src/expr/call_spread_short_tests.rs
  • crates/perry-codegen/src/expr/class_field_barrier_tests.rs
  • crates/perry-codegen/src/expr/class_method_arguments_object_tests.rs
  • crates/perry-codegen/src/expr/conforming_layout_note_tests.rs
  • crates/perry-codegen/src/expr/mod.rs
  • crates/perry-codegen/src/expr/property_get/tests.rs
  • crates/perry-codegen/src/gc_call_effects.rs
  • crates/perry-codegen/src/lib.rs
  • crates/perry-codegen/src/lower_call/alloc_hot_tests.rs
  • crates/perry-codegen/src/lower_call/method_override.rs
  • crates/perry-codegen/src/lower_call/property_get.rs
  • crates/perry-codegen/src/lower_call/property_get/imported_object.rs
  • crates/perry-codegen/src/lower_call/typed_shape_bake_tests.rs
  • crates/perry-codegen/src/native_root_coverage/mod.rs
  • crates/perry-codegen/src/root_reload.rs
  • crates/perry-codegen/src/runtime_decls/objects.rs
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-codegen/src/stmt/boxed_slot_no_root_tests.rs
  • crates/perry-codegen/src/stmt/class_field_loop_tests.rs
  • crates/perry-codegen/src/stmt/element_shape_loop_tests.rs
  • crates/perry-codegen/src/stmt/prealloc_module_global_tests.rs
  • crates/perry-codegen/src/stmt/stable_packed_loop.rs
  • crates/perry-codegen/src/temp_root_coverage/mod.rs
  • crates/perry-codegen/src/type_analysis/numeric/tests.rs
  • crates/perry-codegen/tests/app_window_config_options.rs
  • crates/perry-codegen/tests/argless_builtin_extra_args.rs
  • crates/perry-codegen/tests/class_field_store_pointer_test.rs
  • crates/perry-codegen/tests/class_keys_gc_root.rs
  • crates/perry-codegen/tests/constructor_recursion.rs
  • crates/perry-codegen/tests/destructure_call_location.rs
  • crates/perry-codegen/tests/i64_spec_ternary_recursion.rs
  • crates/perry-codegen/tests/ios_platform_api_lowering.rs
  • crates/perry-codegen/tests/large_object_barriers.rs
  • crates/perry-codegen/tests/loop_safepoint_purity.rs
  • crates/perry-codegen/tests/macos_bundle_chdir_gate.rs
  • crates/perry-codegen/tests/native_proof_buffer_views.rs
  • crates/perry-codegen/tests/native_proof_regressions.rs
  • crates/perry-codegen/tests/node_test_mock_property_presence.rs
  • crates/perry-codegen/tests/perry_builtin_name_collision.rs
  • crates/perry-codegen/tests/release_boxes_lowering.rs
  • crates/perry-codegen/tests/scalar_replaced_slot_roots.rs
  • crates/perry-codegen/tests/shadow_slot_hygiene.rs
  • crates/perry-codegen/tests/static_symbol_hygiene.rs
  • crates/perry-codegen/tests/temp_root_operand_temporaries.rs
  • crates/perry-codegen/tests/typed_feedback.rs
  • crates/perry-codegen/tests/typed_shape_declared_at_allocation.rs
  • crates/perry-codegen/tests/typed_shape_descriptor.rs
  • crates/perry-codegen/tests/typed_shape_descriptors.rs
  • crates/perry-hir/src/lower/expr_call/module_static.rs
  • crates/perry-hir/src/lower/expr_call/native_module_spread_tests.rs
  • crates/perry-runtime/src/array/flat_clone.rs
  • crates/perry-runtime/src/array/mod.rs
  • crates/perry-runtime/src/array/spread_dense_tests.rs
  • crates/perry-runtime/src/array/subclass.rs
  • crates/perry-runtime/src/object/native_call_method.rs
  • crates/perry-runtime/src/typed_feedback.rs
  • crates/perry-runtime/src/typed_feedback/guards.rs
  • crates/perry-runtime/src/typed_feedback/tests.rs
  • crates/perry/src/commands/compile/object_cache.rs
  • crates/perry/src/commands/compile/object_cache/object_cache_tests.rs
  • crates/perry/src/commands/compile/run_pipeline.rs
  • crates/perry/tests/issue_8772_short_packed_spread.rs
  • crates/perry/tests/issue_8773_closure_capture_packed_loops.rs
  • crates/perry/tests/issue_8774_argument_shape_clones.rs
  • crates/perry/tests/issue_8775_imported_object_specialization.rs
  • scripts/gc_root_dominance_check.py
  • scripts/local_binding_type_allowlist.json
  • scripts/shape_descriptor_census_baseline.json
  • test-files/fixtures/issue_8772_short_packed_spread/generic.ts
  • test-files/fixtures/issue_8772_short_packed_spread/reverse.ts
  • test-files/fixtures/issue_8774_argument_shapes/main.ts
  • test-files/fixtures/issue_8775_imported_object/main.js
  • test-files/fixtures/issue_8775_imported_object/runner.js

📝 Walkthrough

Walkthrough

The change adds guarded argument-shape routing, cross-module method capability propagation, live packed-loop revalidation, iterator-aware spread fallback, and regression coverage across code generation, runtime helpers, cache keys, and integration fixtures.

Changes

Argument-route guards

Layer / File(s) Summary
Containment analysis and clone routing
crates/perry-codegen/src/collectors/*, crates/perry-codegen/src/expr/mod.rs, crates/perry-codegen/src/lower_call/method_override.rs, crates/perry-codegen/src/codegen/argument_shape_clone_tests.rs
Argument routes now record containment preservation. Guard elision applies only to fresh, exact-class, contained arguments. Other routes retain runtime guards and generic fallback.
Imported argument classes
crates/perry-codegen/src/codegen/mod.rs, crates/perry/tests/issue_8774_argument_shape_clones.rs, test-files/fixtures/issue_8774_argument_shapes/main.ts
Shape-argument clone analysis can use visible imported class metadata. Tests cover barriers, publication, reassignment, forwarding, and imported clone routes.

Cross-module method capabilities

Layer / File(s) Summary
Capability metadata and compilation wiring
crates/perry-codegen/src/codegen/opts.rs, crates/perry-codegen/src/codegen/mod.rs, crates/perry-codegen/src/collectors/object_literal_exports.rs, crates/perry/src/commands/compile/run_pipeline.rs
Compilation now builds shared short-spread and object-literal method registries. Producers publish method targets and ShapeId globals. Consumers receive the registries through CompileOptions and FnCtx.
Short-spread and object-method lowering
crates/perry-codegen/src/expr/call_spread_short.rs, crates/perry-codegen/src/lower_call/property_get.rs, crates/perry-codegen/src/lower_call/property_get/imported_object.rs
Lowering supports local and foreign shapes, guarded direct calls, live own-method caches, candidate chaining, and generic fallback. Operand rooting is limited to paths that can collect.
Runtime guard support and cache identity
crates/perry-runtime/src/typed_feedback/guards.rs, crates/perry-runtime/src/typed_feedback/tests.rs, crates/perry-codegen/src/runtime_decls/objects.rs, crates/perry/src/commands/compile/object_cache.rs
Runtime helpers validate exact closure identity and own-method cache misses. Cache keys include producer method targets and candidate metadata.
Capability and options fixtures
crates/perry-codegen/src/codegen/*tests.rs, crates/perry-codegen/src/expr/*tests.rs, crates/perry-codegen/tests/*, crates/perry-codegen/src/lib.rs, crates/perry-codegen/src/root_reload.rs, scripts/*
Test options initialize the shared registries. Re-exports, keepalive declarations, non-collecting classifications, and tracking metadata are updated.

Packed-loop revalidation

Layer / File(s) Summary
Live loop and read fallback
crates/perry-codegen/src/stmt/stable_packed_loop.rs, crates/perry-runtime/src/array/subclass.rs, crates/perry-codegen/src/runtime_decls/strings.rs
Nested and captured packed loops use live revalidation. Failed nested reads use one generic indexed read and rejoin without replaying earlier effects. Spilled length layouts are admitted.
Loop regression coverage
crates/perry/tests/issue_8773_closure_capture_packed_loops.rs
Tests cover captured-loop field layouts and getter-triggered deletion during nested reads under both GC modes.

Spread lowering and integration coverage

Layer / File(s) Summary
Math and runtime spread behavior
crates/perry-hir/src/lower/expr_call/module_static.rs, crates/perry-runtime/src/array/flat_clone.rs, crates/perry-runtime/src/object/native_call_method.rs, crates/perry-runtime/src/array/mod.rs
Single-spread Math calls use dedicated HIR nodes. Mixed fixed/spread calls use generic spread lowering. Nullish spread sources produce zero arguments. Other values use the iterator protocol.
Integration fixtures and tests
crates/perry-hir/src/lower/expr_call/native_module_spread_tests.rs, crates/perry-runtime/src/array/spread_dense_tests.rs, crates/perry/tests/issue_8772_short_packed_spread.rs, test-files/fixtures/issue_8772_short_packed_spread/*, crates/perry/tests/issue_8775_imported_object_specialization.rs, test-files/fixtures/issue_8775_imported_object/*
Regression tests cover reverse dependencies, mixed Math spreads, moving GC, imported object specialization, iterator fallback, and generated method targets.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: jdalton, thehypnoo

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch land-8833-ecs

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant