Skip to content

chore: merge train 241 (v0.5.1620) - #10825

Merged
proggeramlug merged 3 commits into
mainfrom
train241r
Sep 20, 2026
Merged

proggeramlug merged 3 commits into
mainfrom
train241r

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Merge train 241 — ethers compiles, released as v0.5.1620.

Contents

PR Change
#10801 fix(hir): memoize class_mutates_capture recursion to stop exponential HIR lowering — fixes #10757

The diagnosis is the valuable part

Compiling ethers 6.17.0 from real source never finished: HIR lowering of @noble/curves' weierstrass.js spun 8+ minutes on one core before being killed. A probe during this session's audit measured 19 min 22 s at 99.1% on one core, still not past module collection, where node loads the same entry in 0.19 s.

It is a bounded superlinear blowup, not a hang — and establishing that is what made it fixable. Instrumented call counts to class_mutates_capture grow 252 → 35,770 → 688,870 as the Point class gains one arithmetic method at a time, fitting (Bⁿ-1)/(B-1) for n=9 (the hardcoded MAX_NESTED_CLASS_DEPTH) at branching factors B=1, 2, 3. It terminates; just not in any practical time for a realistic method count.

Root cause. for_each_nested_capture was written to find a class genuinely nested inside another class's method body — class Outer { make() { return class Inner {} } } — by scanning member bodies for capture-forwarding constructions. A self-referential new Self(...) matches that same scan, so Point is misread as nested inside Point, and the recursion re-enters the class it started from once per self-constructing method, at every depth up to the cap, redoing the identical (class, id) subproblem each time. Point's double, add, fromAffine, multiplyUnsafe all construct new Point(...) forwarding the outer context — completely ordinary self-referential-class code.

Replacing a cap with an invariant is worth more than the speedup

The fix memoizes by (class_name, id) with an in-progress set to break cycles, replacing MAX_NESTED_CLASS_DEPTH = 9. The new termination argument is "finitely many distinct (class, id) pairs" — a property of the algorithm — where the old one was a bound that happened to hold, and which as the PR notes could also have under-covered a legitimately deep but acyclic chain.

Measured, --no-auto-optimize --no-link to isolate lowering:

fixture pristine fixed
8 self-constructing methods 2.34 B 2.21 B
12 self-constructing methods 26.2 B (11.2× its own 8-method run) 2.26 B (flat, +2%)

With the fix, the whole ethers@6.17.0 tree — ethers + @noble/curves + @noble/hashes + @adraffy/ens-normalize + aes-js, 153 modules — lowers and codegens natively with 0 JS fallback in about a minute.

What this does NOT do, stated plainly

ethers compiles; it does not yet link. The final link fails on undefined references from ethers/src.ts/crypto/crypto.ts's export { createHash, createHmac, pbkdf2Sync, randomBytes } from "crypto" re-export shape. That is tracked as #10802, and it is the same defect as #10432, which has had a six-line reproducer since 2026-09-06:

export { createHash } from "crypto";   // dep.ts — undefined perry_fn_dep_ts__createHash at link

Confirmed during this session's audit with a two-file, no-ethers reproduction. So #10757 stays open: "compiles" is not "links", and closing it would sever the thread from the original never-finishes report to the remaining blocker.

An incidental gap closed

The parity harness gains PERRY_COMPILE_TIMEOUT (default 300 s). It previously timed only the executed binary (PERRY_RUN_TIMEOUT), never the compile — so a compiler hang on any one fixture wedged the whole harness instead of failing that fixture. That is the same shape as three other things fixed on main today: a check that cannot distinguish "still working" from "stuck". It deserves more than the parenthetical the PR gave it.

Validation

Assembled on c1d9f73e58; source head asserted fresh; PR fully represented — zero missing insertions; no attribution trailers. Ten cheap gates, cargo check --workspace --all-targets under -D warnings, all five pinned artifacts byte-identical before and after, six unit suites with an empty failing set, and lint complete at 6-of-6 with nothing outside the known-red public-baseline step.

Both compiler-output suites at failed_workloads=[] and repsel_census rc=0 wasted_promotion=False.

The PR verifies HIR output byte-identical via --print-hir only at fixture sizes the unfixed pass could finish — the honest limit, and why the sweep is weighted to this change's blast radius. Gap sweep at PERRY_RUN_TIMEOUT=30, seven areas, 212 fixtures, every area asserted live, zero unexplained regressions:

class 84   gc_ 54   import 20   property 18   closure 14   module 14   extends 8

perry-bot and others added 3 commits September 20, 2026 15:54
… HIR lowering (#10757)

A class whose own methods construct fresh instances of itself while the
class also captures an outer local (an everyday shape for arithmetic/
builder classes, e.g. @noble/curves' Point.double()/add() each returning
new Point(...)) made for_each_nested_capture misread the class as nested
inside itself. class_mutates_capture then recursed back into the same
class on every self-constructing method, at every depth up to the
hardcoded MAX_NESTED_CLASS_DEPTH cap, recomputing the identical (class,
id) subproblem from scratch each time -- exponential in the class's
method count, bounded only by that depth cap, so a single ordinary
elliptic-curve arithmetic class (weierstrass.js in ethers' @noble/curves
dependency) never finished lowering within any practical wait.

Memoize class_mutates_capture by (class_name, id), shared across every
id detect_shared_in_body asks about, and use an in-progress set to break
cycles instead of the depth cap (which could also, in principle, have
under-covered a legitimately deep but acyclic chain). Bisected on
weierstrass.js confirmed this is a bounded blowup, not a true hang:
instrumented call counts fit (B^9-1)/(B-1) almost exactly for branching
factors 1, 2, 3 as the class grows by one method at a time. HIR output
is byte-identical before/after on every fixture size small enough for
the unfixed pass to complete.

Also give the parity harness's compile step a timeout
(PERRY_COMPILE_TIMEOUT, default 300s) -- it previously had none (only
the executed-binary run did), so a compiler hang on any one fixture
wedged the whole harness instead of failing that fixture.

Add test-files/test_gap_10757_self_referential_class_capture.ts: fails
via the harness's compile timeout on unfixed main, passes byte-identical
to node with the fix.
@proggeramlug
proggeramlug merged commit 80434ce into main Sep 20, 2026
21 of 22 checks passed
@proggeramlug
proggeramlug deleted the train241r branch September 20, 2026 15:23
@coderabbitai

coderabbitai Bot commented Sep 20, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

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: 05294a99-3945-43e7-a19f-31471d435c70

📥 Commits

Reviewing files that changed from the base of the PR and between c1d9f73 and 3bd2796.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • CLAUDE.md
  • Cargo.toml
  • changelog.d/10801-exponential-class-capture-lowering.md
  • crates/perry-hir/src/lower/shared_mutable_capture.rs
  • run_parity_tests.sh
  • test-files/test_gap_10757_self_referential_class_capture.ts
 _____________________________________________
< My `#FF570A` highlighter marks all the TODOs. >
 ---------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ 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.

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.

Compiling ethers from source never finishes: HIR lowering of @noble/curves' weierstrass.js spins indefinitely on one core

2 participants