Skip to content

Eliminate return-accumulator temps in EH/lock by sinking returns#613

Merged
richlander merged 2 commits into
mainfrom
fix/decompiler-return-accumulator
Jun 18, 2026
Merged

Eliminate return-accumulator temps in EH/lock by sinking returns#613
richlander merged 2 commits into
mainfrom
fix/decompiler-return-accumulator

Conversation

@richlander

Copy link
Copy Markdown
Owner

Summary

When a method's result is computed inside a try, catch, finally-guarded, or lock body and the ret sits after the region closes, the importer reconstructs a synthetic accumulator local:

try { V = x + 1; } finally { ... } return V;   // reconstructed
try { return x + 1; } finally { ... }          // original source

#611 (the definite-assignment dead-init drop) keeps that temp but declares it bare, which recompiles opcode-exact for the simple shapes its DA walk can prove. It deliberately bails on leave and lock, so the two-return try/finally (TryFinallyTwoReturns) and the lock body (ClassicLock) keep diverging — and even the shapes it fixes still render the unnatural temp form rather than the source.

ReturnSinkingPass rewrites the accumulator back to source form: it sinks each feeding store into a return in place — adjacent StoreLocal V; return V; pairs, and the terminal fall-through stores of a try body, catch arm, lock body, or if/else arm that a trailing return V follows.

Soundness

All-or-nothing by construction: the pass acts only on a local that is never address-taken and whose every load is a Return operand, and applies a candidate only when every store and every load is consumed by the plan. switch is excluded — a switch expression is lowered through its own result accumulator, so per-arm returns would diverge.

Results

  • CfgSampleClass compile-back: exact 93 -> 95 (TryFinallyTwoReturns, ClassicLock now exact); all EH/lock return-accumulators render the natural try { return e; }.
  • Compile-check arbiter unchanged across 35K+ BCL methods (System.Private.CoreLib / System.Linq / System.Collections) — no new malformed or binding errors (measured with the pass toggled on/off).
  • Gate updated: the five EH/lock shapes pinned exact; KnownDiffs trimmed to the residual docket.
  • All 433 decompiler tests + 18 analysis tests green.

Relationship to #611

Complementary, not a replacement. #611 is the conservative dead-init drop; this restructures the temp away entirely, producing more natural C# and closing the leave/lock cases #611 leaves behind.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

@richlander richlander force-pushed the fix/decompiler-return-accumulator branch from fdadf17 to 4ecb574 Compare June 18, 2026 13:30
richlander and others added 2 commits June 18, 2026 06:40
When a method's result is computed inside a try, catch, finally-guarded, or
lock body and the ret sits after the region closes, the importer reconstructs
a synthetic accumulator local: try { V = x + 1; } finally { ... } return V;
The original source was try { return x + 1; } finally { ... }.

bare, which recompiles opcode-exact for the simple shapes its DA walk can
prove. It deliberately bails on leave and lock, so the two-return try/finally
(TryFinallyTwoReturns) and the lock body (ClassicLock) keep diverging, and even
the shapes it fixes still render the unnatural temp form rather than the source.

ReturnSinkingPass rewrites the accumulator back to source form: it sinks each
feeding store into a return in place — adjacent StoreLocal V; return V; pairs,
and the terminal fall-through stores of a try-body, catch arm, lock body, or
if/else arm a trailing return V follows. It is sound by construction: it acts
only on a local that is never address-taken and whose every load is a return's
operand, and applies a candidate only when every store and every load is
consumed by the plan (all-or-nothing). Switch is excluded: a switch expression
is lowered through its own result accumulator, so per-arm returns would diverge.

CfgSampleClass compile-back: exact 93 -> 95 (TryFinallyTwoReturns, ClassicLock
now exact); the EH/lock return-accumulators render try { return e; }. Compile-
check arbiter unchanged across 35k+ BCL methods (no new malformed or binding
errors). Gate updated: the five EH/lock shapes pinned exact; KnownDiffs trimmed
to the residual docket.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cross-reference the printer's definite-assignment dead-init drop (#611) as the
conservative fallback for accumulators this pass leaves standing, so the
two-layer division of labor is discoverable from the code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@richlander richlander force-pushed the fix/decompiler-return-accumulator branch from 4ecb574 to b25f9b0 Compare June 18, 2026 13:41
@richlander richlander merged commit 817acab into main Jun 18, 2026
10 checks passed
@richlander richlander deleted the fix/decompiler-return-accumulator branch June 18, 2026 13:55
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.

1 participant