Skip to content

Lift constructor field initializers ahead of the base call to the field declaration#614

Merged
richlander merged 1 commit into
mainfrom
feature/field-initializers
Jun 18, 2026
Merged

Lift constructor field initializers ahead of the base call to the field declaration#614
richlander merged 1 commit into
mainfrom
feature/field-initializers

Conversation

@richlander

Copy link
Copy Markdown
Owner

What

C# emits field initializers before the base constructor call, so a this.field = value store the IL places before the chain call is a field initializer, not a body assignment. The printer rendered it as a body statement, which recompiles to after the base call — an opcode-order diff. These were the two remaining .ctor compile-back docket items:

  • CfgSampleClass::.ctor (int _shadowed = 1;)
  • LockFixtureSamples::.ctor (readonly object _root = new();)

How

Generalize the constructor-prologue detection in CSharpPrinter: scan the entry block for the base/this .ctor chain call; if every statement before it is a qualifying field-initializer store, lift each to the field declaration (new DecompilerResult.FieldInitializers) and let the existing chain lift fire on the call. The compile-back harness threads the initializers through to the target type's field declarations.

Soundness

A store only qualifies when its receiver is the under-construction this (ldarg.0) and its value references no place — no this, parameter, local, or stack-slot load (ReferencesPlace). That is exactly C#'s field-initializer legality rule (initializers cannot read the instance or constructor parameters), so a lifted store is always legal where it lands. Detection requires the whole pre-chain prologue to be such stores, matching the compiler's prologue shape exactly; anything else bails and keeps the current body rendering. Constants, new T(), and static reads qualify; a field init that reads a parameter does not and stays in the body.

Numbers

  • Compile-back fixture exact 151 to 153 (+2), docket 13 to 11 (-2), recompile-fail unchanged (527; System.Linq unchanged 395).
  • Decompiler tests 432 to 434.
  • Pinned CfgSampleClass::.ctor opcode-exact in the compile-back gate.

Found via the #604 --compile-back oracle. Companion to #612.

…ld declaration

C# emits field initializers before the base constructor call, so a
`this.field = value` store the IL places before the chain call is a field
initializer — not a body assignment. The printer rendered it as a body
statement, which recompiles to AFTER the base call: an opcode-order diff
(the two remaining `.ctor` compile-back docket items, `CfgSampleClass::.ctor`
spelling `_shadowed = 1;` and `LockFixtureSamples::.ctor` spelling
`_root = new object();`).

Generalize the constructor-prologue detection: scan the entry block for the
base/this `.ctor` chain call; if every statement before it is a qualifying
field-initializer store, lift each to the field declaration (new
`DecompilerResult.FieldInitializers`) and let the existing chain lift fire on
the call. The compile-back harness threads the initializers through to the
target type's field declarations.

Soundness: a store only qualifies when its receiver is the under-construction
`this` (ldarg.0) AND its value references no place — no `this`, parameter,
local, or stack-slot load (`ReferencesPlace`). That is exactly C#'s
field-initializer legality rule (initializers cannot read the instance or
constructor parameters), so a lifted store is always legal where it lands.
Detection requires the whole pre-chain prologue to be such stores, matching
the compiler's prologue shape exactly; anything else bails and keeps the
current body rendering. Constants, `new T()`, and static reads qualify; a
field init that reads a parameter does not and stays in the body.

Compile-back fixture exact 151 -> 153 (+2), docket 13 -> 11 (-2),
recompile-fail unchanged (527; System.Linq unchanged 395). Decompiler tests
432 -> 434. Pinned `CfgSampleClass::.ctor` exact in the gate. Found via #604.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@richlander richlander merged commit c4b3c6d into main Jun 18, 2026
10 checks passed
@richlander richlander deleted the feature/field-initializers branch June 18, 2026 13:30
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