-
-
Notifications
You must be signed in to change notification settings - Fork 151
perf(repsel): make Ptr<Shape> return-shape facts reachable inside the CJS IIFE (#7170 R1) #7233
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f362195
37aff1a
efe1ea1
6b21bdd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| // Liveness fixture for the `Ptr<Shape>` census key **inside an IIFE** (#7170 R1). | ||
| // | ||
| // `fixture_ptr_shape.ts` is the same proof at module scope, where `mk` and | ||
| // `run` are `hir.functions` entries. This file wraps identical code in | ||
| // `(function () { … })()` and nothing else. That one difference is what Perry's | ||
| // own `cjs_wrap` does to *every* CommonJS module (`compile/cjs_wrap/wrap.rs`, | ||
| // `const _cjs = (function() { … })();`), and #7170 §6 measured what it costs: | ||
| // | ||
| // probe wrapper result for `const p = mk(i)` | ||
| // p7_esm.ts none selected, and consumed | ||
| // p8_iife.ts (function(){ … })() NOT EVEN A CANDIDATE | ||
| // | ||
| // Inside the IIFE `mk` is not a function declaration the compiler can see — | ||
| // it lowers to `Stmt::Let { init: Expr::Closure }`, and `mk(i)` to | ||
| // `Call { callee: LocalGet(id) }`. #7107's producer walked `hir.functions` | ||
| // (empty here) and its caller-side seed accepted only `Expr::FuncRef`, so the | ||
| // whole return-shape mechanism was structurally unreachable across CommonJS — | ||
| // 91.6% of dependency-JS allocation sites (#7170 §2). | ||
| // | ||
| // This fixture is what makes that reachability falsifiable. Reverting either | ||
| // half of R1 — the closure arm of `collect_return_shape_functions`, or | ||
| // `callee_names_one_function`'s `LocalGet` arm — takes its `ptr-shape` count | ||
| // to zero while `fixture_ptr_shape.ts` stays green, because that one is at | ||
| // module scope and never needed either. | ||
| // | ||
| // Do not "tidy" this file: | ||
| // | ||
| // * Removing the IIFE turns it back into `fixture_ptr_shape.ts` and it stops | ||
| // testing anything R1 added. | ||
| // * Removing `p.x = p.x + 1` makes the object non-escaping, `escape_news.rs` | ||
| // deletes it outright, and the promotion becomes `unconsumed — | ||
| // scalar_replaced` (#7170 §6.1). The `ptr-shape-consumed` floor is what | ||
| // catches that, and the store is what satisfies it. | ||
| // * Reassigning `mk`, or declaring it twice, disqualifies the callee binding | ||
| // (`single_binding_closure_locals`) and takes the count to zero. | ||
| // * Deleting `maybe` removes the fixture's only UNSERVED return-position | ||
| // allocation, and with it the census's ability to catch | ||
| // `codegen/closure.rs` reporting every closure as served. See | ||
| // ALLOC_BUCKET_FLOORS in `scripts/compiler_output_harness/repsel_census.py` | ||
| // — this file is the only workload that lands both bucket rows in a | ||
| // `closure` region, and no compiler unit test can reach that wiring | ||
| // (they all set the report scope by hand). | ||
|
|
||
| const _cjs = (function () { | ||
| function mk(i: number) { | ||
| return { x: i, y: i + 1 }; | ||
| } | ||
| // Deliberately NOT a return-shape producer: the second return is not a fresh | ||
| // allocation, so the returns disagree and `producer_return_class` refuses. | ||
| // Its `{ tag: n }` is therefore an unserved return-position allocation in a | ||
| // closure region — the anti-vacuity half of the served classification. | ||
| function maybe(n: number) { | ||
| if (n > 2) { | ||
| return { tag: n }; | ||
| } | ||
| return null; | ||
| } | ||
| function run(n: number): number { | ||
| let total = 0; | ||
| for (let i = 0; i < n; i++) { | ||
| const p = mk(i); | ||
| p.x = p.x + 1; | ||
| total = total + p.x + p.y; | ||
| } | ||
| if (maybe(n) !== null) { | ||
| total = total + 1; | ||
| } | ||
| return total; | ||
| } | ||
| return run(4); | ||
| })(); | ||
|
|
||
| console.log("ptr_shape_cjs_iife:" + _cjs); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| ### Representation selection: `Ptr<Shape>` return-shape facts now reach inside Perry's CommonJS IIFE (#7170 R1) | ||
|
|
||
| `compile/cjs_wrap/wrap.rs` emits every CommonJS module body inside | ||
| `const _cjs = (function () { … })();`. Inside that wrapper a module-level | ||
| `function` declaration never reaches `hir.functions` — it lowers to | ||
| `Stmt::Let { init: Expr::Closure }`, and a call to it to | ||
| `Call { callee: LocalGet(id) }`. #7107's return-shape mechanism walked | ||
| `hir.functions` for producers and accepted only a bare `Expr::FuncRef` callee | ||
| for consumers, so it was **structurally unreachable across the whole CommonJS | ||
| ecosystem**: 91.6 % of dependency-JS `Ptr<Shape>` allocation sites sit in | ||
| `closure` regions (#7170 §2/§6). This is the third time Perry's own CJS | ||
| scaffolding turned out to be the wall, after #7139 (the wrap preamble arming | ||
| the rule-5 barrier) and #7152/#7171 (`__cjs_module`). | ||
|
|
||
| Both halves are extended, because both missed it: | ||
|
|
||
| * **Producer** (`collectors/ptr_shape_returns.rs`): every `Expr::Closure` in | ||
| the module is now a candidate body, keyed by the `FuncId` the closure already | ||
| carries — the same module-wide `fresh_func` counter as `hir.functions`, so no | ||
| key can mean two things. A `Function` and a closure are the same thing to | ||
| this proof but carry differently-spelled context flags | ||
| (`Function::was_plain_async` versus `Module::async_step_closures`), so both | ||
| are projected onto one `ProducerBody` view and the closure arm cannot prove | ||
| something weaker than the function arm. | ||
| * **Consumer**: an `Expr::LocalGet` callee resolves through a new module-wide | ||
| binding proof, `collectors/spec_abi_sites.rs::single_binding_closure_locals` | ||
| — exactly one `Stmt::Let` with a closure init, never reassigned at any depth | ||
| in any body, never also a parameter or a `catch` binding. That is the same | ||
| statement `Expr::FuncRef` makes directly, and it is the only property the | ||
| seed needs of a callee: *which body runs*. Deliberately **not** | ||
| `FnCtx::local_closure_func_ids`, which `lower_call` pairs with a runtime | ||
| `js_typed_feedback_closure_direct_call_guard` because it is populated in | ||
| statement order. | ||
|
|
||
| Box-backed bindings are admitted on purpose: a hoisted inner `function` | ||
| referenced from a sibling closure is `PreallocateBoxes`-boxed by construction | ||
| (`lower_decl/block.rs`), and that is the entire dependency-JS population. | ||
| Freshness is unchanged — the full Phase 3b proof still re-runs over the | ||
| producer's body. | ||
|
|
||
| **Measured, on a real transpiled CommonJS module**, both arms pinned at one | ||
| SHA and compared on emitted IR with call sites checked: `Ptr<Shape>` goes from | ||
| `selected 0 / consumed 0` to `selected 1 / consumed 2`, and the emitted IR | ||
| loses **25 opaque `js_*` call sites** — `js_object_get_field_by_name_f64` | ||
| 19 → 13, and three whole typed-feedback guard diamonds | ||
| (`js_typed_feedback_object_get_field_by_name_f64` / | ||
| `observe_property_get` / `record_guard_pass` / `record_guard_fail` / | ||
| `record_fallback_call`, each 10 → 7). Three call sites are *added* and are | ||
|
Comment on lines
+44
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reconcile the emitted-call-site totals. The listed reductions account for 21 calls: 6 from 🤖 Prompt for AI Agents |
||
| reported as the promotion's own cost: the guard-free store emits a direct | ||
| `js_write_barrier_slot` where `js_put_value_set_dyn_ic` did the barrier | ||
| internally, plus one slot-layout note and one string addref. | ||
|
|
||
| **On the 197-module dependency corpus the mechanism fires 10 more times and | ||
| promotes nothing more**, and that is reported rather than smoothed: the | ||
| `return` allocation bucket moves 231 → 221 unserved and 4 → 14 served, while | ||
| corpus `selected`/`consumed` stay at 3/11. The producers R1 reaches are | ||
| *exported* helpers with no same-module `const x = f(…)` call site — #7170 R2's | ||
| cross-module half, which is structurally blocked because | ||
| `Expr::ExternFuncRef` carries no `FuncId`. A throwaway instrumented compiler | ||
| put a number on the residual wall: over the same corpus the first refusing | ||
| conjunct is the **return form** in 1397 of 1971 refusals, while | ||
| "can fall off the end" refuses exactly **one** body — so widening the producer | ||
| to conditional returns whose arms agree (R0 §3b measured 88 such sites) is the | ||
| next increment, not more consumer reach. | ||
|
Comment on lines
+59
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Resolve the measurement-status contradiction. This fragment reports a refusal distribution of 1,397/1,971 and one fall-through body. The PR objectives state that refined producer-refusal distributions remain unmeasured. Keep the quantified claim only if this measurement is now part of the reported evidence; otherwise remove it. 🤖 Prompt for AI Agents |
||
|
|
||
| Also fixes a latent hole this proof would otherwise have inherited: | ||
| `Expr::WithSet` carries its fallback `LocalId` in `WithSetFallback` rather than | ||
| in a child expression, so `spec_abi_sites::record_expr_use` — whose every other | ||
| arm delegates to the exhaustive `walk_expr_children` — had never recorded | ||
| `with (o) { x = v }` as a reassignment. `reassigned_locals` had been wrong | ||
| about that since it was written; the fix can only make it more conservative, | ||
| and only in a module containing `with`. | ||
|
|
||
| New gates: census liveness fixture | ||
| `benchmarks/repsel_census/fixtures/fixture_ptr_shape_cjs_iife.ts` (floors held | ||
| in code) and behavioural gap test | ||
| `test-files/test_gap_repsel_cjs_iife_return_shape.ts`, registered in | ||
| `test-parity/gc_repsel_corpus.txt`. The fixture deliberately lands **two** | ||
| allocation buckets from inside one IIFE — a served return and an unserved one — | ||
| because the served flag for a closure region is set in `codegen/closure.rs`, | ||
| which no compiler unit test can reach: hard-coding it `false` or `true` was a | ||
| green hole across all 526 of them and is red only in the census. | ||
|
Comment on lines
+79
to
+82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win The unit-test count disagrees with the census file. This line says the sabotage arms were "a green hole across all 526 of them". The docstring added to 🤖 Prompt for AI Agents |
||
Uh oh!
There was an error while loading. Please reload this page.