Lift a spilled base/this constructor argument into the chain initializer#612
Merged
Conversation
A base(...)/this(...) argument that carries control flow — the ubiquitous base(message ?? "default") / base(code > 0 ? "x" : null) exception shape — gets spilled to a temporary the general inliner declines to fold: the chain receiver is evaluated first, and TypeRef cannot yet prove it is a class rather than a mutable byref struct, so it reads as an impure leaf the stored value may not be reordered past. The printer's lift only fires when the call is the body's first statement, so the spilled-argument call stayed an invalid `base(temp);` body statement (CS0175) that dropped its argument on recompile. But the receiver of a constructor's own base/this call is the object under construction — an immutable reference with no observable evaluation effect — so moving an argument's computation past it never reorders anything. New ConstructorChainArgumentPass (after structuring folds the ??/?:) inlines each single-use argument spill into the chain call, so the call lands first and the existing lift renders it as a `: base(args)` / `: this(args)` initializer. The compile-back harness ignored the lifted initializer and emitted class skeletons with no base type, so lifted chains could not recompile. It now renders the initializer and emits a `: Base` clause for non-generic in-assembly class bases (object/value-type/generic/out-of-assembly bases stay unspelled, costing at most a base-call diff, never a miscompile). Soundness: the pass reuses single-store/single-load/address-not-taken safety and only inlines temps stored in the run immediately preceding the chain call, where the sole prior evaluation is the effect-free under-construction `this`. The base clause is conservative (TypeDefinition, non-generic, non-object only). On a broad System.Linq corpus the base clause left recompile-fail unchanged (424). Compile-back fixture corpus: exact 147 -> 151 (+4), docket 14 -> 13, recompile-fail 528 -> 525 (no new failures). CtorChainSamples base(message), base(message ?? "default"), and this(value.ToString()) now opcode-exact; the remaining ctor#2 diff is the orthogonal ternary-sense equivalence. Decompiler tests 432 pass; main suite 1140 pass (9 skipped). Found via #604. 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.
What
A
base(...)/this(...)argument that carries control flow — the ubiquitousbase(message ?? "default")/base(code > 0 ? "x" : null)exception shape — gets spilled to a temporary. The printer's chain lift only fires when the call is the body's first statement, so the spilled-argument call stayed an invalidbase(temp);body statement (CS0175) that dropped its argument on recompile (ldarg ldarg call→ldarg call).The receiver of a constructor's own base/this call is the object under construction — an immutable reference with no observable evaluation effect — so moving an argument's computation past it never reorders anything. The general inliner declines this (it can't yet prove the
thisreceiver isn't a mutable byref struct), exactly asConstructorChainPassalready noted for the receiver itself.How
ConstructorChainArgumentPass(new, runs after structuring folds the??/?:): inlines each single-use argument spill into the chain call, so the call lands as the body's first statement and the existing lift renders: base(args)/: this(args).CompileBack): the oracle ignored the lifted initializer and emitted class skeletons with no base type, so lifted chains couldn't recompile. It now renders the initializer and emits a: Baseclause for non-generic in-assembly class bases.Soundness
this.objectonly); generic / out-of-assembly bases stay unspelled, costing at most a base-call diff, never a miscompile. On a broad System.Linq corpus the base clause left recompile-fail unchanged (424 → 424).Numbers
Compile-back fixture corpus:
CtorChainSamplesbase(message),base(message ?? "default"), andthis(value.ToString())are now opcode-exact; the remaining ctor#2 diff is the orthogonal ternary-sense equivalence. Decompiler tests 432 pass; main suite 1140 pass (9 skipped).Found via the
--compile-backoracle (#604).