Fix super-linear compilation of guarded shared-or active-pattern matches - #20244
Open
T-Gro wants to merge 1 commit into
Open
Fix super-linear compilation of guarded shared-or active-pattern matches#20244T-Gro wants to merge 1 commit into
T-Gro wants to merge 1 commit into
Conversation
Contributor
❗ Release notes requiredYou can open this PR in browser to add release notes: open in github.dev
|
Contributor
|
🔍 Tooling Safety Check — Affects-Compiler-Output
|
…rn matches (dotnet#18425) A single match clause of N disjuncts sharing one `when` guard, whose disjuncts contain partial active patterns, compiled in exponential (2^N) time and assembly size and eventually overflowed the stack at analysis time. Each guarded disjunct contributes both a match-fail edge and a guard-false edge into the same residual decision state, which InvestigateFrontiers re-investigated along all 2^N paths with nothing sharing the identical residuals. Memoize the residual states (Maranget-style join point): each distinct residual state is keyed by structural identity plus captured locals and, once it has been reached more than a fixed threshold (32) of times, compiled once into a let-bound join function that later equal-keyed paths call. Below the threshold the emitted IL is byte-for-byte identical to before, so ordinary code is unchanged; byref-like result types disable memoization for the whole match (a join is an FSharpFunc and the CLR forbids byref-like generic arguments). Active patterns are evaluated the same number of times, in the same order, with the same side effects. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 08c1a339-a621-4b09-8ac5-92f7b6b337b3
T-Gro
force-pushed
the
t-gro-fix-patmatch-frontier-explosion
branch
from
August 13, 2026 15:12
1262508 to
9b684ae
Compare
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.
Fixes #18425.
A single
matchclause of N disjuncts that share onewhenguard, whose disjuncts contain partial active patterns, compiled in exponential (2^N) time and space and eventually overflowed the stack. This happens at analysis time, so besidesfsc/fsiit also hitdotnet buildand the editor language service on file-open.Where it exploded
Pattern-match compilation expands a frontier of active patterns. Each guarded disjunct contributes both a match-fail edge and a guard-false edge into the same residual decision state, so the compiler re-investigated one identical state along all 2^N paths (
InvestigateFrontiersinPatternMatchCompilation.fs). Nothing shared those identical residuals, and nothing bounded the recursion.Fix
Memoize the residual states (a Maranget-style join point). Each distinct residual state is keyed by structural identity of the patterns it still has to test plus the locals it captures; the first time a state is reached it is compiled inline exactly as before, and once a state has been reached often enough it is compiled once into a let-bound join function that every later equal-keyed path calls instead of re-emitting the subtree.
Two guarantees keep the change safe:
FSharpFuncover the captured locals returning the match result, and the CLR forbids a byref-like type (byref/inref/outrefor a ref struct such asSpan) as a generic argument. When the match result type is byref-like, memoization is switched off for the whole match, so such matches compile exactly as before.Active patterns are therefore evaluated the same number of times, in the same order, with the same side effects as the previous compiler.
Numbers
match a, b with (A p, E 1 _) | … | (A p, E N _) when g p -> p | _ -> -1,--optimize+, measured end-to-end (process launch included, so ~2.4 s is fixed startup):Before, both compile time and assembly size grow exponentially until the compiler overflows the stack around N≈24; after, both grow polynomially and the assembly the compiler could not produce at all is emitted in a few seconds.
Runtime impact
None for realistic code: every match below the promotion threshold (i.e. everything a human writes) is byte-identical to the previous compiler, so generated programs are unchanged. A promoted match — only the pathological shapes that previously produced hundreds of KB of IL or failed to compile — calls its active patterns exactly as many times as the old fully-inlined tree would have; sharing removes duplicated code, not evaluations, so it never adds active-pattern calls. The only difference on a promoted match is one extra join-function call per residual on the fall-through path, which is precisely the code that previously did not exist because the compiler crashed.
Tests
GuardedOrPatternComplexity.fs: the 24-disjunct shape now compiles, runs, and returns the exact left-to-right result; a shared guard that binds a variable at a different tuple position per disjunct is not over-fused; a byref-returning guarded shared-or match stays inline and compiles.QuotationRenderingTests.fs: a 6-disjunct control stays inline (no join thunk in the quotation), while 8-disjunct scalar and tuple-with-binding shapes promote and render correctly.