chore: merge train 241 (v0.5.1620) - #10825
Merged
Merged
Conversation
… 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.
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (6)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merge train 241 — ethers compiles, released as v0.5.1620.
Contents
fix(hir): memoizeclass_mutates_capturerecursion to stop exponential HIR lowering — fixes #10757The diagnosis is the valuable part
Compiling
ethers6.17.0 from real source never finished: HIR lowering of@noble/curves'weierstrass.jsspun 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_capturegrow 252 → 35,770 → 688,870 as thePointclass gains one arithmetic method at a time, fitting(Bⁿ-1)/(B-1)forn=9(the hardcodedMAX_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_capturewas 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-referentialnew Self(...)matches that same scan, soPointis misread as nested insidePoint, 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'sdouble,add,fromAffine,multiplyUnsafeall constructnew 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, replacingMAX_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-linkto isolate lowering:With the fix, the whole
ethers@6.17.0tree —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'sexport { 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: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 onmaintoday: 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-targetsunder-D warnings, all five pinned artifacts byte-identical before and after, six unit suites with an empty failing set, andlintcomplete at 6-of-6 with nothing outside the known-red public-baseline step.Both compiler-output suites at
failed_workloads=[]andrepsel_census rc=0 wasted_promotion=False.The PR verifies HIR output byte-identical via
--print-hironly 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 atPERRY_RUN_TIMEOUT=30, seven areas, 212 fixtures, every area asserted live, zero unexplained regressions: