Drop the dead default-initializer on a definitely-assigned local#611
Merged
Conversation
A local whose defining store is not its first entry-block reference declares up front, and the emitter always spelled that `int V_0 = default;`. When the local is in fact assigned on every path before each read — the common shape of a value computed across switch sections or try/catch arms — that initializer is a dead store the original IL never had (locals lean on `.locals init`), so recompiling the body emits an extra `ldc.i4.0; stloc` and diverges from the source opcode stream. The new --compile-back oracle flags exactly this. Declare such a local bare. A new conservative structured definite-assignment walk (ComputeReadBeforeAssign) decides: it threads an assigned set through if/switch/try-catch/try-finally/loops, joining on the paths that reach each merge, and keeps `= default` for any local read while not provably assigned. It under-claims assignment wherever it is unsure and bails outright — keeping every `= default` — on control flow it does not fully model (gotos/labels, leave, lock). So a bare declaration is emitted only when CS0165 cannot arise; the change can only remove a redundant initializer, never a required one. Soundness: faithful. Dropping the initializer leaves the local to `.locals init` zero-initialization, exactly as the original relied on; the value is unchanged and the dead store disappears from the recompiled IL. Corpus: compile-back fixture exact 133 -> 139 (+6), docket 20 -> 14 (-6), recompile-fail unchanged at 498 (no CS0165 introduced). Compile-check arbiter unchanged (semantic 316, Full malformed 1206 — no method-level regression). Tests +2 (428): the three dead-init shapes now declare bare; ParseOrZero (reached via a by-ref out-argument) still keeps `= default`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
richlander
added a commit
that referenced
this pull request
Jun 18, 2026
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
added a commit
that referenced
this pull request
Jun 18, 2026
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
added a commit
that referenced
this pull request
Jun 18, 2026
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
added a commit
that referenced
this pull request
Jun 18, 2026
* Eliminate return-accumulator temps in EH/lock by sinking returns
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>
* Document the #611 division of labor in ReturnSinkingPass header
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>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.
Found by the
--compile-backoracle (#604).A local whose defining store is not its first entry-block reference declares up front, and the emitter always spelled that
int V_0 = default;. When the local is in fact assigned on every path before each read — the common shape of a value computed across switch sections or try/catch arms — that initializer is a dead store the original IL never had (locals lean on.locals init). Recompiling emits an extraldc.i4.0; stlocand diverges from the source opcode stream:This declares such a local bare. A new conservative structured definite-assignment walk (
ComputeReadBeforeAssign) decides: it threads an assigned set throughif/switch/try-catch/try-finally/loops, joining on the paths that reach each merge, and keeps= defaultfor any local read while not provably assigned. It under-claims assignment wherever unsure and bails outright — keeping every= default— on control flow it does not fully model (gotos/labels,leave,lock). So a bare declaration is emitted only when CS0165 cannot arise; the change can only remove a redundant initializer, never a required one.Soundness: faithful. Dropping the initializer leaves the local to
.locals initzero-initialization, exactly as the original relied on; the value is unchanged and the dead store disappears from the recompiled IL.Measurements:
PowerOfTwo/CatchEverything/TryFinallyAddnow declare bare;ParseOrZero(reached via a by-ref out-argument) still keeps= default.