Skip to content

codegen: Number locals are one scoped rule; the shape-field leaf is unconditional (step 5L, P5) - #11662

Merged
proggeramlug merged 7 commits into
mainfrom
perf-number-locals-one-rule
Sep 29, 2026
Merged

proggeramlug merged 7 commits into
mainfrom
perf-number-locals-one-rule

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Charter step 5L, slice P5: Number locals as ONE scoped rule.

What

  • One query: type_analysis::local_is_number answers from the function-wide number_by_construction_locals set plus a scope opened by each guarded loop clone. The scope closes with dematerialize_scope, like the table's other scoped facts. It is the only way to ask whether a local is a Number.
  • Deleted: the five per-family accumulator vectors (element-shape, stable-packed, packed-f64, masked-window, string-window), their LocalGet special cases in is_numeric_expr, and the sixth set, masked_region_scalar_locals. A masked-window fast copy now admits a flow-refined local into its own scope and withdraws it at the first write it can't prove is a Number.
  • Now asks the query: the raw-double predicate (expr_produces_canonical_raw_f64) and the shadow-slot skip.
  • The perf: a read's result type is not propagated through a const binding — 'const v = O.a' costs 29 instructions, 'const v = O.a * 1' costs 8 and beats node #10777 shape-field leaf is unconditional: PERRY_L14_NBC_ORDER (fix(codegen): compute numeric provenance after the Ptr<Shape> receiver proofs it depends on #10929, default off) is deleted, along with its build-cache and object-cache keys. With it off, shape_numeric_inputs returned empty sets, so a proven numeric field read (acc += o.a with o = new C(...)) never made its local a Number.

Results (Linux x86_64, base = main 58d8680, outputs identical to node)

base this PR
fpxnum fixture (instructions/iteration) 25.00 10.00 (no js_dynamic_string_or_number_add; run() root barriers 12 → 4)
tsc instructions, n=5 (full collections) 72.568G (82) 72.499G (82)
Zod instructions, n=5 1.0190G 1.0176G
tsc/Zod call-site census (dynamic adds, raw-double conversions, root barriers, shadow binds) 1391 / 367 / 160,850 / 12,559 identical

tsc and Zod don't contain the pattern the leaf targets. The slices that move the param/lit matrix rows are P6 (barrier elision) and P7 (region F64 reads).

Verification

  • codegen 2311/0; runtime (serial) shows the same single host-DNS failure as base
  • gap subset (443): 0 differences
  • gc-root-dominance with stale 0/10; wasm abi, sso, file size, fmt and api-doc pass
  • the new gap fixture test_gap_number_local_shape_field_leaf.ts covers where the field proof must refuse (a conditional or missing ctor store, delete, a string store through any, an escaping sink, a computed key, a method store, Object.assign, a getter, NaN)
  • sabotage: a withdraw that does nothing, and shape_numeric_inputs returning empty, each turn their unit tests red

Summary by CodeRabbit

  • Performance
    • Numeric accumulations involving shape-proven object fields can run with fewer instructions and avoid unnecessary runtime type checks.
    • Number values refined inside optimized loops can retain efficient numeric handling until a write changes their type.
  • Bug Fixes
    • Improved handling of reassigned fractional Number values used in arithmetic and array elements.

Ralph Küpper and others added 5 commits September 29, 2026 05:11
…p 5L, P5)

A local is Number in scope S iff every write reaching a read in S is
Number-producing. The function scope is number_by_construction_locals; a
guarded loop clone opens its own scope in the receiver descriptor table
(materialize_number_locals), ended by dematerialize_scope like every other
scoped payload. type_analysis::local_is_number is the one query.

Deleted: ElementShapeLoopFact::numeric_accumulator,
StablePackedLoopFact::numeric_accumulators, PackedF64LoopFact and
MaskedWindowArrayFact numeric_accumulators,
StringWindowArrayFact::numeric_accumulator, and the per-family LocalGet
disjunction in is_numeric_expr.

Wired to the query: is_numeric_expr, expr_produces_canonical_raw_f64 (the
LocalGet arm admitted integer locals only), the shadow-slot mirror skip, the
non-pointer shadow value test, temp-root inertness, the Update coerce skip,
the bitwise leaf and the declared-only violability test.
… (charter step 5L, P5)

The sixth Number-local set, FnCtx::masked_region_scalar_locals, is deleted.
A masked-window fast copy now admits a flow-refined local to its own scope
(ReceiverDescriptorTable::admit_number_local) after the statement that wrote
a Number, and withdraws it at the first write it cannot prove Number; the
copy's dematerialize_scope ends the rest. type_analysis::local_is_number is
the one query: the shadow-mirror skip no longer has a second arm, and every
consumer of the query (is_numeric_expr, raw-f64 LocalGet, bitwise leaf,
Update coerce, temp-root inertness) now sees the refinement too.

The refinement lands strictly after its statement and the region admits only
top-level scalar LocalSet/Update/pure statements, so a member holds a Number
at every read inside the scope.

Tests: receiver_regions_tests a_flow_refined_number_local_joins_and_leaves_its_copy_scope;
gap fixture test_gap_masked_region_number_scope.ts (un-refine to a numeric string,
refined reads in and after the region, ta_i32 / plain_f64 / slow copies).

Sabotage: withdraw_number_local as a no-op turns the unit test red.
…al (charter step 5L)

h = h + o.a with o = new C(...) never admitted h as a Number: the #10777
shape-field leaf (a read of a proven-numeric field on a shape-proven
receiver) was computed in the right order but its inputs were gated behind
PERRY_L14_NBC_ORDER, default off, so collect_number_by_construction_locals
always saw empty shape members. The knob, its build-cache entry and its
object-cache key are deleted; shape_numeric_inputs always hands the fixpoint
the receivers and the intersection of their numeric fields.

The fields come from prove_numeric_fields (every reachable store is
Number-producing by construction, never the declared type), the same proof
that already licenses a bare load double of o.a.

fpxnum (5L fixture): 25 -> 10 instr/iter; the loop no longer calls
js_dynamic_string_or_number_add and h takes no root barrier.

Tests: gap fixture test_gap_number_local_shape_field_leaf.ts (conditional and
missing constructor stores, delete, string stores through any, escaping sink,
computed key, method store, Object.assign, getter, NaN, the accumulator).
Sabotage: shape_numeric_inputs returning empty sets turns
shape_inputs_intersect_numeric_fields_before_proving_property_locals red.
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 6ad200ee-1b9a-4170-9278-c9337accfa6e

📥 Commits

Reviewing files that changed from the base of the PR and between 8430213 and eda5bc0.

📒 Files selected for processing (3)
  • crates/perry-codegen/src/stmt/element_shape_loop.rs
  • crates/perry-runtime/src/box.rs
  • crates/perry/src/commands/compile/build_cache.rs
 ___________________________________________________________
< This loop is doing cardio. Your users are doing timeouts. >
 -----------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: af78609b-4cd6-4a25-9fcd-57c8267cbd7e

📥 Commits

Reviewing files that changed from the base of the PR and between 0abf224 and 8430213.

📒 Files selected for processing (30)
  • changelog.d/11662-number-local-shape-field-leaf.md
  • changelog.d/11662-number-locals-masked-region-scope.md
  • changelog.d/11662-number-locals-one-rule.md
  • crates/perry-codegen/src/codegen/closure.rs
  • crates/perry-codegen/src/codegen/entry.rs
  • crates/perry-codegen/src/codegen/function.rs
  • crates/perry-codegen/src/codegen/method.rs
  • crates/perry-codegen/src/codegen/method_static.rs
  • crates/perry-codegen/src/collectors/hir_facts.rs
  • crates/perry-codegen/src/collectors/number_by_construction.rs
  • crates/perry-codegen/src/collectors/receiver_regions.rs
  • crates/perry-codegen/src/collectors/receiver_regions_tests.rs
  • crates/perry-codegen/src/expr/literals_vars.rs
  • crates/perry-codegen/src/expr/mod.rs
  • crates/perry-codegen/src/expr/shadow_slot.rs
  • crates/perry-codegen/src/rooting/temp_root.rs
  • crates/perry-codegen/src/stmt/element_shape_loop.rs
  • crates/perry-codegen/src/stmt/element_shape_native.rs
  • crates/perry-codegen/src/stmt/loops.rs
  • crates/perry-codegen/src/stmt/masked_window_region.rs
  • crates/perry-codegen/src/stmt/stable_packed_loop.rs
  • crates/perry-codegen/src/stmt/string_length_loop.rs
  • crates/perry-codegen/src/type_analysis.rs
  • crates/perry-codegen/src/type_analysis/numeric.rs
  • crates/perry-codegen/src/type_analysis/numeric/tests.rs
  • crates/perry-codegen/src/type_analysis/pod.rs
  • crates/perry/src/commands/compile/build_cache.rs
  • crates/perry/src/commands/compile/object_cache.rs
  • test-files/test_gap_masked_region_number_scope.ts
  • test-files/test_gap_number_local_shape_field_leaf.ts
💤 Files with no reviewable changes (7)
  • crates/perry-codegen/src/codegen/method_static.rs
  • crates/perry-codegen/src/codegen/entry.rs
  • crates/perry/src/commands/compile/object_cache.rs
  • crates/perry-codegen/src/codegen/method.rs
  • crates/perry-codegen/src/codegen/function.rs
  • crates/perry/src/commands/compile/build_cache.rs
  • crates/perry-codegen/src/codegen/closure.rs

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

Codegen now uses one query to determine whether a local holds a Number, combining function-wide proofs with proofs scoped to guarded clones. Fast-loop and masked-copy lowering manage scoped proofs. Shape-based numeric inputs no longer depend on PERRY_L14_NBC_ORDER.

Changes

Number-local proof and lowering

Layer / File(s) Summary
Scoped Number-local query
crates/perry-codegen/src/collectors/receiver_regions.rs, crates/perry-codegen/src/type_analysis/*, crates/perry-codegen/src/expr/*, crates/perry-codegen/src/rooting/temp_root.rs, crates/perry-codegen/src/collectors/receiver_regions_tests.rs
Receiver-descriptor scopes now track Number-local proofs. Numeric analysis and code-generation consumers use a shared query. Tests cover proof lifetime and reassigned fractional Number locals.
Fast-loop accumulator scopes
crates/perry-codegen/src/stmt/element_shape_loop.rs, crates/perry-codegen/src/stmt/element_shape_native.rs, crates/perry-codegen/src/stmt/loops.rs, crates/perry-codegen/src/stmt/stable_packed_loop.rs, crates/perry-codegen/src/stmt/string_length_loop.rs, crates/perry-codegen/src/expr/mod.rs
Fast-loop lowerings register accumulator locals in proof scopes and remove those scopes after clone lowering. Loop facts no longer store accumulator lists.
Masked-copy Number refinements
crates/perry-codegen/src/stmt/masked_window_region.rs, crates/perry-codegen/src/expr/mod.rs, crates/perry-codegen/src/codegen/*, test-files/test_gap_masked_region_number_scope.ts, changelog.d/11662-number-locals-masked-region-scope.md
Masked-copy lowering admits and withdraws Number-local proofs within a copy scope. The fixture exercises refined and unrefined local values across typed, plain-number, and mixed arrays.
Shape-field Number inputs and validation
crates/perry-codegen/src/collectors/{hir_facts,number_by_construction}.rs, crates/perry/src/commands/compile/*cache.rs, test-files/test_gap_number_local_shape_field_leaf.ts, changelog.d/11662-number-local-shape-field-leaf.md, changelog.d/11662-number-locals-one-rule.md
Shape-based numeric inputs no longer use PERRY_L14_NBC_ORDER, and build and object cache keys no longer include it. The fixture covers field accumulation cases. The changelog reports the 5L fixture’s instruction-count change.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Refactor

Merge Risk: ⚪ Minimal · up to 84302

The change unifies how code generation decides that a local holds a Number and makes shape-field inputs unconditional. No concrete merge-blocking issue was identified in the supplied material, so it is mergeable after normal checks.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 84302

The change affects how compiled programs classify numeric values and maintain GC roots. The reviewed proof and cleanup paths have safeguards, and no specific security defect was established, but the wider activation and security-sensitive consumers warrant design review.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The affected scope is compiler output for programs reaching the qualifying fast-loop, masked-copy, or shape-field paths. No tenant boundary, service privilege, or infrastructure authority change was established.

Trust Boundaries and Controls

  • observed — Shape inputs require proven receivers and fields numeric across all such receivers. The remaining shape collector has an independent gate and invalidation checks; removing the numeric-order gate does not remove those controls.

Resilience and Maintainability Implications

  • observed — Masked-copy cleanup withdraws remaining admissions, and the reviewed fast-loop path removes its scope before lowering the slow clone. A lowering error can bypass some caller teardown, but that path aborts compilation rather than proceeding to a fallback with those facts.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.77% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 52 functions across 20 files. (3 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the two main changes: consolidating Number locals into one scoped rule and making the shape-field leaf unconditional. It is specific and relevant, although slightly long.
Description check ✅ Passed The description provides a detailed summary, concrete changes, performance results, verification results, and regression coverage. It does not use the template headings exactly and omits an explicit r…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 55.77% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 52 functions across 20 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

@proggeramlug
proggeramlug merged commit cbabd00 into main Sep 29, 2026
23 of 24 checks passed
@proggeramlug
proggeramlug deleted the perf-number-locals-one-rule branch September 29, 2026 10:11
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