Skip to content

refactor(codegen): defuse the guarded-wildcard trap in accumulator_rhs_is_numeric - #9308

Closed
proggeramlug wants to merge 1 commit into
PerryTS:mainfrom
proggeramlug:fix/guarded-wildcard-catchall
Closed

refactor(codegen): defuse the guarded-wildcard trap in accumulator_rhs_is_numeric#9308
proggeramlug wants to merge 1 commit into
PerryTS:mainfrom
proggeramlug:fix/guarded-wildcard-catchall

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Semantically identical one-hunk refactor, agreed with @ECS1 as the backstop for #9303's rebase.

_ if offset_reads_inlined => was a guarded wildcard: any match arm placed after it is reachable only while the flag is false, and the compiler cannot warn — reachability depends on the runtime guard. #9303's first cut added an index form below it and verified as a complete no-op in the range tier while passing every flag-false test, which is exactly how such an arm fails: the flag-false tests look like coverage.

The guard moves into the body of a single unguarded catch-all; future index forms extend that body, where one set of tests covers both flag states.

Refs #9279 (which introduced the arm), #9303 (which rebases onto this).

Summary by CodeRabbit

  • Refactor
    • Streamlined handling for indexed numeric expressions during packed-load processing.
    • Preserved existing behavior while making future index cases easier to extend.

…s_is_numeric

`_ if offset_reads_inlined =>` was a guarded wildcard: any match arm
placed after it was reachable only while the flag was false, with no
possible compiler warning since reachability depends on the runtime
guard. PerryTS#9303's first cut added an index form below it and verified as a
complete no-op in the range tier while passing every flag-false test.

Semantically identical — the guard moves into the body of a single
unguarded catch-all, and future index forms extend that body where the
same tests cover both flag states.

Two-liner agreed with the PerryTS#9303 author, who rebases on it.
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 905f2e20-a2c8-4806-bbe1-19ff29dae2e8

📥 Commits

Reviewing files that changed from the base of the PR and between 54f7b6f and 1a4718d.

📒 Files selected for processing (1)
  • crates/perry-codegen/src/stmt/stable_packed_accumulator.rs

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


📝 Walkthrough

Walkthrough

The change consolidates the non-local Expr::IndexGet match logic in accumulator_rhs_is_numeric. It preserves the existing admission conditions and adds a comment for future index forms.

Changes

Numeric index admission

Layer / File(s) Summary
Consolidate index admission logic
crates/perry-codegen/src/stmt/stable_packed_accumulator.rs
A single catch-all arm now requires offset_reads_inlined and a packed loop index matching counter_id.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Merge Risk: ⚪ Minimal · up to 1a471

This refactor consolidates equivalent guard behavior without changing the intended result, so no actionable merge-blocking risk remains beyond normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the refactor, its rationale, semantic equivalence, and related issues. It does not follow the repository template because it omits the required Summary, Changes, Related issue… Rewrite the description using the repository template. Add the required sections and document the commands run and their results, mark the applicable checklist items, and identify the related issues using the expected format.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main refactor: moving the guard out of a wildcard match arm to prevent future conditionally unreachable arms.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files.
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: Description check

Explanation

The description explains the refactor, its rationale, semantic equivalence, and related issues. It does not follow the repository template because it omits the required Summary, Changes, Related issue, Test plan, and Checklist sections, including verification results.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Copy link
Copy Markdown
Contributor Author

Reviewed — the reasoning is right, and I've confirmed the rewrite is exactly semantics-preserving: _ if flag => X followed by _ => false computes the same thing as _ => flag && X, and the combined form is the one that can't silently strand a later arm.

It turns out #9303 already carries this same fix as part of its own change, with the masked-reads arm added inside the combined catch-all. Since #9303 is what motivated the trap in the first place, I've taken it from there rather than merging the two versions of the same edit — this branch conflicts against it.

Closing as superseded, with the fix landing via #9303. The failure mode you named is worth keeping in mind generally: a guarded catch-all makes reachability depend on a runtime flag, so the compiler can't warn, and a later arm can verify as a complete no-op while every flag-false test still passes.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Correction to what I said above — I was wrong that #9303 already carried this fix, and you should have the accurate version.

#9303 adds the combined _ => arm but leaves the old _ => false behind it, so the trailing arm becomes an unreachable pattern. That's a warning, and with -D warnings in CI it's a build failure. I only caught it because the release build of the batch surfaced it:

warning: unreachable pattern
  --> crates/perry-codegen/src/stmt/stable_packed_accumulator.rs:86:17

So your change wasn't a subset of #9303 — it was the complete version. Removing that second arm is exactly what makes the combined form correct, and #9303 alone doesn't compile warning-free without it.

I've applied the deletion in the batch that carries #9303, so the fix lands there and CI stays green. Leaving this closed only because the code merges by the other route, not because the change was unnecessary — the diagnosis in your description was right, and it caught something #9303 got wrong.

proggeramlug added a commit that referenced this pull request Aug 31, 2026
* codegen: a float accumulator over masked reads earns the dense range clone

17_loop_data_dependent: 475 ms -> 219 ms against node's 220 ms on an idle
Mac mini -- parity, from 2.16x. Sums bit-identical across 100M data-dependent
float recurrence steps.

    sum = sum * x[i & 63] + x[(i * 7) & 63]   // rejected
    sum = sum * x[i & 63]                     // admitted

The discriminator was the accumulator's static numeric proof. `+` can be
concatenation, so the dense tier's per-statement proof demands both operands
numeric; a reassigned accumulator has no such proof, because its own writes
read the guarded array, whose element proof only exists once the guard has
run. A chicken-and-egg that `*` never faces -- multiplication needs only the
weaker inert fact. Confirmed by instrumenting the two conjuncts of the dense
LocalSet arm: the failing one is the proof, on exactly the fixtures whose
accumulator writes contain a plain-array read.

The matcher now peels the accumulator: when the proof fails on the LocalSet
target of a self-accumulating write, it retries with the target treated as
numeric BY CONTRACT, records it pending, and then verifies every pending
local with the same collector the lowering runs
(`collect_numeric_accumulators`), rejecting the whole dense match with its
own named trace reasons (`accumulator_needs_single_array`,
`accumulator_not_provable`) if the two disagree -- so the clone can never
contain a dynamic `+` under facts that forbid one.

The contract is enforced at run time twice over: the clone's entry emits a
genuine-double tag check on the accumulator, and the dense entry guard
validates the whole masked window hole-free. A string-seeded accumulator and
a string element both route to the slow copy and produce node's
concatenation, verified under PERRY_GC_FORCE_EVACUATE.

Supporting changes:

* `accumulator_rhs_is_numeric` accepts masked static-window reads of the
  tracked array (`masked_reads_validated`), sound because the dense guard
  validated the window union hole-free. Fixing that exposed a match-arm
  reachability bug: `_ if offset_reads_inlined` was a guarded catch-all, so
  ANY arm placed after it was unreachable whenever the flag was set -- the
  first version of this change sat exactly there and verified as a no-op.
  The two tests are now one combined catch-all.
* `emit_range_loop_accumulator_admission` admits a masked-only single array
  (counter-bearing arrays keep priority; multiple arrays still decline).
* `MaskedWindowArrayFact` carries `numeric_accumulators` so `is_numeric_expr`
  sees admitted accumulators while the clone lowers -- without this the add
  inside the clone would stay dynamic, which is a collecting call under facts
  that assume none (the #9259 cascade shape). Mirrors the string-window
  fact's field (#9160).

perry-codegen lib 1378/0; packed-loop integration suite 59/0 across 11 files;
3 new regression tests (admission + node-identical result, string-seeded
accumulator, string element), each under forced evacuation.

Claude-Session: https://claude.ai/code/session_01Pcq6j6y57TdKSR2Zx2D187

* refactor(runtime): split ic_miss.rs and array/tests.rs under the file cap

#9302 and #9307 each tipped a file that was already within ~35 lines of the
2000-line gate. Extracts the C3C PIC test module and the Array.prototype
method-discriminator tests into sibling files; no behaviour change.

* fix(codegen): drop the now-dead catch-all after the combined accumulator arm

#9303's combined `_ =>` arm made the trailing `_ => false` unreachable, which
is a `-D warnings` failure. Removing it is #9308's fix, which the combined arm
needs to be complete.

* style: rustfmt

* chore: changelog fragment for the train13 follow-up

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
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