The multi-handler matcher only recognized a switch-instruction dispatch — but
when a try-catch has just two handlers (or a handful with non-consecutive K
values), Roslyn emits an if-chain instead:
if (num == K_1) br case_K_1; br nextBlock
; nextBlock { if (num == K_2) br case_K_2; <leave outer | br end> }
Add a parallel matcher that walks the if-chain and collects (K, case-block)
pairs the same way MatchSwitchDispatch does, plus the terminating leave/branch
as the default exit. Call it as a fallback when the switch matcher rejects.
Also clone the default-exit before re-adding it to the continuation block —
in the if-chain shape it's a child of a *different* block (a later step in
the chain), not the now-cleared switch instruction, so the in-place re-add
relied on the switch's release cascade and didn't generalize.
Closes Cluster 2 from #3745.
The flag-based early-return rewriter was tied to one specific lowered shape:
the try body's flag-setter had to be exactly `stloc flag(K); leave try`, the
post-try check had to be a `br checkBlock` (not an inline `IfInstruction`), and
the early path had to be a direct Leave or a forward to a one-instruction
leave-block whose target was the function body. None of those hold for
`try { try { return X; } finally { await ... } } finally { await ... }`:
- The inner flag-setter has a leading capture-forwarding store
(`stloc capture(X); stloc innerFlag(K); leave inner-try`).
- The inner check-block's early path branches to a multi-instruction helper
that sets the *outer* flag and leaves the outer try, instead of being a
direct return.
- SplitVariables hands out a separate ILVariable for the pre-init flag store
when the in-handler store is in a disjoint dataflow region.
Rebuild the matcher around the idea of a "template" — the chain of stores
the early path performs before its terminating Leave. Each flag-setter then
becomes its own prefix stores + a clone of the template, which collapses the
inner-then-outer flag chain in two passes (inner first, outer second, because
descendant order visits the inner TryFinally first). Also extend the
flag-setter scan to walk the whole try-block's descendants — after the inner
rewrite, the inner's spliced flag-setter lives inside the inner-try container
but still leaves outwards to the outer try, so it's an outer flag-setter from
the outer's perspective.
Add a `RUNTIMEASYNC` preprocessor symbol (defined when `EnableRuntimeAsync`
is set) and gate the new return-from-try-finally fixtures on it — the
state-machine async pipeline doesn't recover this shape, so it would expand
the same source into the `int result; try { ...; result = X; } finally { ... }
return result;` verbose form and the Async (state-machine) pretty test would
regress.
Closes Cluster 1 (1.1, 1.3) from #3745. Cluster 1.2 (void `return;` at the
end of a try-finally body) and 1.4 (break/continue across a try-finally) are
left for a follow-up: both round-trip semantically equivalently but the AST
emitter drops a trailing void `return;` and the break/continue lowering uses
a switch dispatch that the current single-K matcher can't recognize.
`try { throw new ...(); } finally { await ... }` lowers to a try whose only
exit is the throw (handled by the synthetic catch). The existing matcher
required at least one outward Branch to the continuation, which is too strict
— a throw-only try body produces zero outward branches but is still a valid
lowered shape. Two follow-on fixes were also needed:
- The pre-init's ILVariable diverges from the in-handler store after
SplitVariables when the try body has no path that reaches the dispatch's
load without going through the catch; match the flag init by slot/kind/type
instead of identity (same workaround the multi-handler matcher uses).
- With a throw-only try body the new TryFinally has unreachable endpoint,
so appending the no-exception successor after it would put a non-final
unreachable-endpoint instruction in the parent block. Skip the append in
that case — the parent block's endpoint is already correctly unreachable.
Closes Cluster 4 from #3745.
The single-handler try-catch matcher was tied to the top-level shape: it
required the try-catch be the last instruction in its parent block, that the
post-catch "no exception" path be a direct Leave that exits the function, and
that the flag-init's ILVariable be identical to the in-handler flag store.
None of those hold for an inner try-catch sitting inside an outer try-finally
where both await — the inner is followed by a `br continuation`, the no-exception
path leaves the outer try-block (not the function), and SplitVariables hands
out a separate ILVariable for the pre-init store.
Drop the "must be last instruction" gate, accept Leave-to-any-ancestor and
cross-container Branch as the no-exception exit (extracted into a new
`IsContainerExit` helper), and match the flag-init by slot/kind/type the same
way the multi-handler matcher already does.
Closes Cluster 3 from #3745.
Two methods exercise `Task<int>.ConfigureAwait(bool)`: a single false-flag form
and a mixed false/true form that combines two awaits in a return expression.
Both cases run through the regular state-machine and runtime-async pipelines
(RuntimeAsync reuses Async.cs as its source).
Gated by `#if ROSLYN2` because Roslyn 2+ preserves named-argument metadata at
the call site, so the decompiler renders `continueOnCapturedContext: false`
when the binary was compiled by Roslyn 2+ and positional `false` for default
csc / Roslyn 1.3.2.
Also adds NoInliningTaskMethod — an async method carrying
[MethodImpl(MethodImplOptions.NoInlining)] — so the runtime-async path
exercises the impl-attribute masking added in the scaffolding commit:
MetadataMethod strips the synthesized MethodImplOptions.Async (0x2000) bit
from the decompiled output, and unrelated impl bits like NoInlining (0x0008)
must still render in the surfaced [MethodImpl(...)] attribute.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>