Commit Graph
8 Commits
Author SHA1 Message Date
Siegfried Pammer a9a43f96a9 Reverse runtime-async multi-handler try-catch dispatched via an if-chain.
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.
2026-05-20 22:30:42 +02:00
Siegfried Pammer 8e2e48f5cc Generalise runtime-async flag-based early-return for nested try-finally.
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.
2026-05-20 22:30:42 +02:00
Siegfried Pammer 8a03ee246f Reverse runtime-async try-finally where the try body always throws.
`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.
2026-05-20 22:30:42 +02:00
Siegfried Pammer 8059fde539 Reverse runtime-async exception lowering for try-catch nested inside try-finally.
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.
2026-05-20 22:30:42 +02:00
Siegfried PammerandClaude Opus 4.7 c107c5a4bc Reverse runtime-async flag-based early-return across a try-finally.
When a return crosses an enclosing try-finally with await, runtime-async lowers it as: capture the return value, set an int flag to a unique non-zero value, leave the try block normally so the finally runs, then post-finally check "if (flag == K) return capture;". Detect that pattern after my outer try-finally rewrite (or, in optimized builds, the compiler-emitted TryFinally directly) and replace each capture-flag-and-leave site with a direct "leave outer (capture)" — the leave still passes through the TryFinally, so the user's finally body executes before the function returns, which matches the source-level semantics.

Handles both the "if (flag == K)" and "if (flag != K)" check forms (the optimizer emits the latter). Closes the last gap in Issue2436 — RuntimeAsync now passes both Optimize and non-Optimize modes; the full RuntimeAsync* sweep is 12/12 green.

Also remap reads of the captured-obj local inside the cleaned filter so optimized builds (where Roslyn inlines the typed-cast directly into the user filter expression instead of stashing it in a local) render against the catch variable rather than against "((T)obj)".

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-20 22:30:42 +02:00
Siegfried PammerandClaude Opus 4.7 7c39e9ea5e Reduce multi-handler runtime-async try-catch back to a multi-handler TryCatch.
When the user writes multiple catch clauses on a single try, runtime-async lowers each catch's body to "[stloc tmp(ex);] [stloc obj(...);] stloc num(K_i); br continuation" with a unique K_i per handler, and the post-catch flow becomes a switch dispatch on `num` that branches to each user-level catch body. Add a TryRewriteMultiHandlerTryCatch driver that mirrors the single-handler match (using NormalizeRuntimeAsyncFilter for filter cleanup), recognizes the post-catch SwitchInstruction, and uses the existing dominator-based block move to relocate each switch case into the corresponding handler body, remapping that handler's per-handler synthesized variables (and the shared filter obj) back to the catch variable.

The shared obj local can no longer be remapped function-wide during filter normalization — that would tag every dispatch idiom with whichever handler ran first — so record the obj per handler in a dictionary and let TryRewriteTryCatch / TryRewriteMultiHandlerTryCatch remap it scoped to each moved catch body. The pre-init "stloc num(0)" is matched by slot index rather than ILVariable identity, since SplitVariables splits the dead pre-init off from the in-handler stores.

Resolves the LoadsToCatch case. Filter normalization extends to the typeless `catch when (filter)` form (isinst Object in the filter), recovered as `catch when` in the AST output. Remaining failures in RuntimeAsync are now multi-await expressions, async-in-struct, and a couple of unrelated decompilation issues.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-20 22:30:41 +02:00
Siegfried PammerandClaude Opus 4.7 58baac0c50 Normalize runtime-async catch filters to recover catch-with-when.
Roslyn lowers `catch (T ex) when (filter)` to `catch object when (BlockContainer { isinst T; obj=ex; <filter> })` even when T is `object` (the source-level `catch when (filter)` form). Run a pre-pass over every catch handler that matches the four-block diamond (entry isinst-gate, trueBody with obj-store + user filter, falseBody constant-false, merge leave-with-result), strip the obj-store machinery, retype the handler variable when T is a more specific type than object, and remap reads of the synthesized obj/tmp/typedEx variables back to the handler variable. After that the catch body is the same simple flag-store shape that TryRewriteTryCatch already handles, so the existing match runs unchanged.

Resolves the RethrowDeclaredWithFilter and ComplexCatchBlockWithFilter cases. Multi-handler catches (LoadsToCatch) still fail because they use a multi-valued discriminator that isn't reduced yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-20 22:30:41 +02:00
Siegfried PammerandClaude Opus 4.7 255e73f3a0 Reverse runtime-async lowering of try-finally and try-catch with await.
Roslyn's runtime-async lowering flattens these into a TryCatch[object] with a captured-rethrow pattern (try-finally) or a TryCatch[T] with a flag-int discriminator and a guarded post-catch body (try-catch). Add a new transform invoked from AsyncAwaitDecompiler when the state-machine matches fail and the method has the runtime-async impl bit; it pattern-matches both shapes and rewrites them back to TryFinally / TryCatch with the original catch body inlined into the handler.

The state-machine and runtime-async lowerings of try-finally use the same catch-handler shape and the same dominator-based finally-body extraction, so promote those to internal static helpers (MatchObjectStoreCatchHandler, MoveDominatedBlocksToContainer) on AwaitInFinallyTransform and call them from the new transform. Filter-bearing catches and multi-handler tries are still left to the standard pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-20 22:30:41 +02:00