C# has no ref-returning switch expression. Skip the transform for StackType.Ref and cover the statement form in RefLocalsAndReturns.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0dd407b6-9410-48df-add5-761ca4a8dec0
A nested designation whose temporary is still read elsewhere is retried with
that variable demoted to a designator leaf. The check that the first tuple
element must be assigned ran before that retry, and every leaf of a wrongly
nested first element precedes the assigned ones, so the pattern looked like it
started mid-way and was rejected before the retry could restore it. The flat
deconstruction was lost for a shape that has one.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Deferring an inner deconstruction to its enclosing one used to be decided by
matching the enclosing pattern in full, once per inner statement of the same
pattern, discarding everything but the end position.
The same decisions are available without it. A nested Deconstruct call can only
be consumed by an enclosing one that is the immediately preceding statement,
looking through the defensive copy of a struct element; anything else in between
is a barrier that stops the enclosing from reaching this position, so it matches
here instead. That leaves the case where the enclosing call is adjacent but
cannot match anyway, which is decided by the constraint MatchDeconstructionCall
already places on its out-parameters.
The tuple-designation branch no longer needs the position the enclosing run
starts at, so the backward walk that searched for it is gone with it. The added
fixtures pin reconstruction across adjacent deconstructions, whose element
stores that walk used to step through.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Only defer to an enclosing designation that can reach this position
The temporaries and element reads of a nested tuple designation are stored back
to back, so a statement of any other kind between the temporary and a read of it
stops the enclosing pattern from consuming that read. Deferring anyway lost the
deconstruction entirely: the enclosing attempt fails and the back-to-front walk
does not return to the position that stepped aside for it, so the reads were left
as the plain element accesses they came from, which master reconstructs.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A nested designation over tuples, var (x, (a, b)) = t;, is lowered to one
temporary per nested designation - parents before children - followed by the
element reads in depth-first leaf order, and decompiled as a flat
deconstruction plus separate element statements.
The temporaries are now consumed into a tree of tuple nodes before the
conversions and assignments are matched, and the leaves get the same flat
depth-first indices the Deconstruct-call chain hands out, so conversion and
assignment matching runs unchanged. Two properties of the lowered IL shape
the matcher to it: earlier transforms rewrite non-escaping element reads
from ldloca to ldloc, and the temporaries are stack slots whose type is
imprecise, so the container's element type is authoritative and the match
variable is retyped to keep the tuple pattern's invariant.
An element that escapes the deconstruction - used after the statement, so
the pattern cannot consume all its reads - demotes back to a designator leaf
and the match is retried, which restores the flat deconstruction the
escaping read needs. The guard against consuming a pattern piecemeal extends
to the new shape: an element read whose container is stored by an earlier
element read defers to the match starting at that store.
Assisted-by: Claude:claude-opus-5:Claude Code
Element index resolution serves both pattern roots: a registered result of a
Deconstruct call, or an element read of a tuple, which it discovers on first
sight and then owns. In an attempt rooted in a Deconstruct call the tuple
branch must not engage - it overwrites the call's result bookkeeping and
rewires the element read to a fresh variable that the pattern never defines.
The shape that reaches it is a tuple whose element is custom-deconstructed
with discarded leaves, followed by an unrelated assignment: the tuple-rooted
attempt fails, the call-rooted one runs at the element's position, and, now
that an unrelated assignment ends a call pattern instead of rejecting it, the
mixed match is no longer rejected on the way out.
Assisted-by: Claude:claude-opus-5:Claude Code
A nested designation rebinds Deconstruct on the element's static type when
the output is recompiled, while the explicit call it replaces is bound at
the call site. Where a derived element type declares a Deconstruct of the
same arity as the called method, and the source deconstructs through a
base-typed view, the two bindings differ, so the sugared output calls the
wrong method - a divergence the runtime fixture demonstrates on optimized
builds, where copy propagation elides the view.
Nesting is therefore only applied when the method the call binds to is the
one a designation would rebind to; otherwise the call stays explicit, where
its receiver cast preserves the binding.
Assisted-by: Claude:claude-opus-5:Claude Code
A nested designation, var (x, (a, b)) = o;, is lowered to a chain of
Deconstruct calls - the inner call taking the outer call's out-argument as
its target, through a defensive copy where the element is a struct - and
decompiled as a flat deconstruction followed by an explicit Deconstruct
call. The IL pattern node, its invariants and the C# builders already
support nested patterns; only the transform never built them.
MatchDeconstruction now consumes the chain into a tree of match patterns.
The leaves get flat indices in depth-first order, which is the order in
which StatementBuilder and ExpressionBuilder pair pattern variables with
assignments, so the conversion and assignment matching runs unchanged on
top of a nested pattern.
Two matching rules follow from the chain being consumed: a call pattern no
longer needs a matched assignment, because single-use leaves are covered by
the forwarding fixup in MatchAssignments; and a pattern is not rooted on an
element of an enclosing deconstruction, because blocks are processed back to
front, so the inner call is visited first and would otherwise consume the
pattern piecemeal, starving the outer call. That guard runs the enclosing
match as a dry run, which is precise: a barrier statement between the calls
or an element with further uses makes it fail, and the inner deconstruction
is then still transformed on its own.
Assisted-by: Claude:claude-opus-5:Claude Code
An assignment whose value is not one of the deconstruction's elements used
to reject the whole match, so a custom deconstruction followed by any
unrelated assignment stayed an explicit Deconstruct call. For a pattern
rooted in a Deconstruct call the element list is fixed by the call's
out-arguments, so such an assignment simply ends the pattern and stays after
the deconstruct instruction.
Tuple-rooted patterns keep rejecting: their element list is discovered from
the assignments, so ending early would misread a suffix of the assignments
as the whole pattern and fabricate discards for the elements before it.
Assisted-by: Claude:claude-opus-5:Claude Code
Deconstruction into a pointer target ((*p, value) = tuple;) stayed an
explicit Deconstruct call: a store through a pointer (or through a target
whose pointer type got erased in a stack slot) does not infer a
ByReferenceType, so IsAssignment reported an unknown expected type and the
transform's conversion check rejected the assignment. The type of the store
itself is just as precise, so use it as the expected type.
Of the three IsAssignment call sites only the transform's MatchAssignment
consumes the expected type; CheckInvariant and GetAssignmentIndex discard
it, so this widens what the transform accepts without weakening the
invariant check.
Assisted-by: Claude:claude-opus-5:Claude Code
C# only accepts System.ValueTuple as a tuple when it is a struct, so a class of
that name is an unrelated type and rendering it with tuple syntax describes it
as something it is not. It also made a tuple appear to contain itself, which no
struct can, and the deconstruction transform then registered the same variable
as a node of its tuple tree twice and threw ArgumentException, failing the whole
method instead of leaving the statements alone.
The check has accepted classes since tuples were added to the type system,
alongside a name comparison against "ValueType" that was corrected later.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
GetTupleElementTypes returns a default ImmutableArray when the type is
not tuple-compatible, so reading Length threw NullReferenceException
instead of taking the documented return-null path.
Assisted-by: Claude:claude-fable-5:Claude Code
The transform is about to be extended substantially; annotating it first
keeps the null contracts of the matcher explicit, where "no match" is
expressed by a null out-argument throughout.
The matching state fields are non-null only while a match is in progress,
which the codebase's null! idiom expresses; MatchConversion additionally
gets the null check its caller's ElementAtOrDefault already implies.
Assisted-by: Claude:claude-opus-5:Claude Code
The bookmark navigation tests asserted the one-shot line highlight by
polling the text view's renderer collection, but the adorner
self-dismisses after an ~800 ms lifetime driven by a DispatcherTimer.
On a loaded CI runner (the desktop job runs the UI and decompiler test
suites concurrently) the dispatcher can stall long enough that the
adorner registers and is dismissed again before the test's next
predicate check, so the wait misses the entire play and burns its full
60 s timeout; raising the timeout cannot help with that. Record the
last played line on DecompilerTextView as persistent evidence of the
one-shot highlight and assert that instead - it also pins the highlight
to the expected line, which the presence check never did.
Assisted-by: Claude:claude-fable-5:Claude Code
The legacy .NET Framework vbc lowers anonymous types differently from
Roslyn: ToString builds its result with a StringBuilder instead of one
String.Format call, no DebuggerBrowsable/DebuggerHidden attributes are
emitted even in debug builds, and in optimized builds the DebuggerDisplay
attribute precedes CompilerGenerated in metadata order. The None/Optimize
test configurations only run on machines where that compiler is
installed, which is why the fixture did not cover them yet.
Assisted-by: Claude:claude-fable-5:Claude Code
A C# anonymous type is immutable and compares every member. VB's are neither
unless every property is declared 'Key': otherwise the properties are settable
and only the 'Key' ones take part in Equals and GetHashCode. Writing such a
type as 'new { ... }' silently gave it value equality and made any assignment
to one of its properties fail to compile, so only an anonymous type with no
settable property is treated as one; the rest keep their own declaration.
Those declarations carry the shape VB gave them, so the round-trip preserves
both mutability and 'Key' equality. Their names are the remaining obstacle,
since the VB compiler separates the parts with '$': the type, its backing
fields and any local named after it are renamed to use '_' instead, and a
comment on the declaration says why the type is spelled out.
Generated variable names are now rejected when they would not be legal C#
identifiers, which also stops a display class from lending its unspeakable
name to a local in the NoLocalFunctions output.
Assisted-by: Claude:claude-fable-5:Claude Code
The VB compiler carries the range variables of a query in $VB$It, $VB$It1,
$VB$It2 and $VB$ItAnonymous, its counterpart to C#'s <>h__TransparentIdentifier.
Unrecognized, they were left in place by CombineQueryExpressions, and since '$'
is not legal in a C# identifier every VB query with more than one range
variable decompiled to code that cannot be recompiled.
Assisted-by: Claude:claude-fable-5:Claude Code
Two predicates disagreed on what a generated name looks like. At the metadata
level a '$' in the name counts, so MemberIsHidden treated VB$AnonymousType_0
as an anonymous type and dropped its definition from the output. At the type
system level only '<' counted, so none of the anonymous-type translations in
CallBuilder and ExpressionBuilder fired. VB assemblies therefore lost the
definitions and kept the raw metadata names at every use site, which is not
valid C#.
Both levels now share one predicate and cannot drift apart again. It keeps the
metadata-level behaviour exactly: counting every name that merely contains
'<' would newly capture explicit implementations of generic interface members.
Assisted-by: Claude:claude-fable-5:Claude Code
VBPretty had no coverage of VB's anonymous types, so nothing caught that their
use sites decompiled to the raw metadata names while their definitions were
hidden from the output. The expected C# is written as it should read once the
generated-name predicates agree with each other; it fails until then.
Roslyn 2.10 targeting .NET Core 2.2 is branched off with #if: there the query
operator calls are not restored to extension-method syntax, so no query
expression is formed and the lowered form survives.
Assisted-by: Claude:claude-fable-5:Claude Code
FractionApprox rejects inputs above 0x7FFFFFFF because they cannot be stored
as a fraction, but the check was one-sided while the sign is stripped right
after it. A large negative value therefore reached the continued-fraction loop
and overflowed the terms it accumulates. ICSharpCode.Decompiler is built with
CheckForOverflowUnderflow, so that aborted decompilation of the whole member
instead of wrapping.
Found by fuzzing nuget.org; reproduces on MathNet.Numerics, whose constants
reach the approximation through their ratio to PI.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
HandleSimpleArrayInitializer multiplies the array dimensions to size the list
it collects elements into. The dimensions come from the input assembly and
need not multiply within int range, and ICSharpCode.Decompiler is built with
CheckForOverflowUnderflow, so an implausible pair of dimensions aborted
decompilation of the whole member. The product is only a capacity hint, so it
can saturate.
Found by fuzzing nuget.org; reproduces on obfuscated assemblies.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
IsCopyConstructor required the copy constructor to be private on a sealed
record and protected otherwise, but IsGeneratedCopyConstructor in the same
class accepts protected regardless of sealedness. Since the former gates the
latter, a sealed record whose copy constructor stayed protected -- what you get
when a record is sealed after it was compiled -- was not recognized as one at
all: it fell through to the general constructor handling, where its base call
made it count as unchained, and the primary-constructor invariant then failed.
Found by fuzzing nuget.org. Beyond silencing the assertion this improves the
output, as the affected types now decompile to positional records.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The accessor forwarding-stub matcher bounded the IL body from above but not
from below. Reference assemblies keep the method RVA while stripping the body
to zero bytes, so an explicit interface accessor there sailed past the size
check and the first opcode read ran off the end of the blob, aborting the
whole type with a BadImageFormatException. The sibling matcher in
TransformDisplayClassUsage already guards its lower bound; this one did not.
Found by fuzzing nuget.org: every package resolving to the
Microsoft.NETFramework.ReferenceAssemblies packs was affected.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Roslyn compiles top-level statements into a synthesized Program class
whose entry point is called '<Main>$', a name that cannot be declared
in C#. Decompiled output kept it verbatim (escaped to
_003CMain_003E_0024 when exporting a project), and since C# accepts
only a method called 'Main' as an entry point, the exported executable
did not compile (CS5001).
Give that method the name 'Main'. Per the decision recorded in #829 we
do not reconstruct top-level statements, so this is the level of
support the output needs to compile.
An async top-level program needs the name in a different place: it
compiles to '<Main>$' holding the statements plus a '<Main>' entry
point that only awaits it. That wrapper carries the .entrypoint marker
but is hidden from the output, so the name goes to the method it
awaits instead - unless AsyncAwait is off, when the wrapper is
emitted and keeps the name itself.
Assisted-by: Claude:claude-fable-5:Claude Code
decimal has no IL literal: legacy csc compiles 0m to a Decimal.Zero
field load, which already decompiled to the literal, but Roslyn
compiles it to a zero-initialization, which decompiled to
default(decimal). The two forms are bit-identical for decimal, so the
literal is no less faithful to the IL and matches what a human writes;
it also removes the per-compiler split in the CompoundAssignmentTest
fixture.
Assisted-by: Claude:claude-fable-5:Claude Code
Operator precedence made the condition read
(setting && Add) || Subtract, so 'x -= 1' was converted to 'x--' even
with IntroduceIncrementAndDecrement disabled. Only observable with the
non-default setting, which no fixture configuration exercises, so no
test accompanies the fix.
Assisted-by: Claude:claude-fable-5:Claude Code
Reviving the 2020 test-cases-fp-types fixtures (compound assignment on
float, double and decimal) exposed an asymmetry: post-increment and
post-decrement on float/double round-tripped as x++/x--, but the pre
forms came back as x += 1f because the increment detection in
PrettifyAssignments only accepted integer constants. C# defines ++/--
on floating-point types as adding or subtracting exactly 1, so the
conversion is exact for a constant 1 operand. Decimal already works
through the op_Increment/op_Decrement path.
Assisted-by: Claude:claude-fable-5:Claude Code
Optimized code stores no temporary for a deconstruction element that is
used only once after the deconstruction. MatchAssignments handled that
for trailing elements, but a nested deconstruction copies the inner
element to a temporary, so the elements preceding it are also left
without an assignment; their external load then violated the
DeconstructInstruction invariant that all pattern variable loads are
descendants of the instruction. The forwarding fixup now covers all
unassigned elements and inserts in pattern order, because the statement
and expression builders pair pattern variables with assignments
positionally. This also fixes the nested tuple deconstruction crash
reported in #3388.
Also unwrap the address of the tested operand in
VisitDeconstructInstruction: deconstructing a struct passes the
receiver by reference, which was emitted as an invalid cast,
'var (x, y) = (S)(ref s);', even without nesting.
Fixes#3388.
Assisted-by: Claude:claude-fable-5:Claude Code
Widening the compiler matrix answers the open review question on the
Issue3230 fixture guard empirically: a class naming its own nested
interface in its base list is a Roslyn-era relaxation. The legacy csc
rejects every such shape with CS0146 (circular base class dependency,
it never reaches the accessibility check), mcs 2.6.4 rejects them with
CS0122/CS0146, and mcs 5.23 accepts them all, rejects naming a base
class's protected nested interface with the same CS0122 as Roslyn, and
emits the same transitive InterfaceImpl metadata. The fixtures are
therefore gated to ROSLYN || MCS5, which exercises the base-list filter
on mcs-generated metadata as well. The pre-existing class C needs
MCS2-specific expected output because mcs 2.6.4 reorders interface-impl
rows and explicit implementations in metadata.
Assisted-by: Claude:claude-fable-5:Claude Code
A class may name its own protected nested interface in its base list
(class F : F.IFoo), but referencing a protected interface nested in a
base class there (class SubF : F, F.IFoo) does not compile, even though
that interface is accessible inside the class body. The interface-impl
metadata still lists such interfaces (they are inherited through other
entries), so emitting every entry produced uncompilable sources. Skip
base types that are neither nested within the current type's nesting
chain nor accessible from the enclosing scope without the
protected-through-inheritance privilege. The check covers every type
the base-list reference names: type arguments, array and tuple
elements, and the declaring chain of the named type.
Assisted-by: Claude:claude-fable-5:Claude Code
TypeDefTableTreeNode and MethodTableTreeNode carried their own copies of
the FieldList/MethodList/ParamList width rule, and ImplMapTableTreeNode
and FieldMarshalTableTreeNode derived heap index widths from the heap
size; both rules diverge from SRM (the *Ptr row count only decides
whether the pointer table itself is large, and heap widths are declared
by the HeapSizes flags, not derived). Rather than fixing the same bugs
in two places, these four nodes now consume the shared readers, which
also verify their computed widths against the SRM row size. The
remaining raw-reading nodes only use plain simple-index widths and are
left for a follow-up.
Assisted-by: Claude:claude-fable-5:Claude Code
The GUI has the metadata-tables view; the CLI had nothing, so checking
e.g. which MethodSemantics rows reference a Property row required a
hand-written System.Reflection.Metadata script. --dump-table <name>
prints every row of a table (RID, token, resolved names, heap offsets,
coded indexes) as an aligned text table, or as JSON with --json, for
the same 39 Cor tables the GUI shows.
Row enumeration for tables without public SRM row access lives in
MetadataExtensions next to the existing GetMethodSemantics helper, so
the GUI's raw-reading table nodes can be folded onto the shared
readers later. Every table's columns are spelled out explicitly in
ECMA-335 declaration order: reflecting over the SRM row structs would
tie the output (and its column order) to runtime internals, and
deterministic output is the point of the feature. JSON uses
System.Text.Json from the shared framework, so no new package
reference is needed.
Assisted-by: Claude:claude-fable-5:Claude Code
The Semantics and Association columns were read from offsets relative
to the metadata root instead of the current row: Semantics always
decoded the first two bytes of the metadata header and Association a
constant offset near it, so only the Method column ever carried real
row data. The Association coded-index width also used the plain-index
threshold (2^16) instead of the coded one (2^15 for one tag bit).
Rewrite the loop with a BlobReader positioned at the table start,
reading each column in sequence, and introduce SimpleIndexSize and
CodedIndexSize helpers encoding the ECMA-335 II.24.2.6 width rules.
The GUI's MethodSemantics metadata table view consumes this helper and
displayed the garbage values.
Assisted-by: Claude:claude-fable-5:Claude Code
The interactive view intentionally shows verbatim metadata names
(EscapeInvalidIdentifiers only runs for save/export), so names compiled
from F# or obfuscators can contain apostrophes. The Char span in the C#
highlighting grammar opened at any apostrophe, coloring everything up to
the next one as a char literal. A negative lookbehind limits the span to
positions where a real char literal can occur.
Assisted-by: Claude:claude-fable-5:Claude Code
The resolver comments cited section numbers from the C# 4.0 spec (and a
few from C# 9.0 drafts), which no longer match the published ECMA-334
standard. Renumber them against dotnet/csharpstandard draft-v11; every
reference was checked against the actual section headings. The old
'better conversion from type' subclause (7.5.3.4) no longer exists as
such and its rules live in 12.6.4.5-12.6.4.7, so that comment now says
so instead of pointing at a dead number.
Assisted-by: Claude:claude-fable-5:Claude Code
CI versions derived from branch names can contain underscores (e.g.
'avalonia12_1_1'), which dpkg rejects in the Version field. Map every
character outside dpkg's allowed set to '.' after the existing '-' to
'~' substitution.
Assisted-by: Claude:claude-fable-5:Claude Code
The tab-opening test only verifies document tab and selection wiring. Using a tiny in-assembly fixture avoids cold framework decompilation work in CI and lowers the chance of unrelated timeout noise.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
Right-clicking inside the details editor inherited the metadata grid's
cell-oriented context menu, whose Copy entries are enabled only for a
hovered DataGridCell -- inside the details area there is none, so the
menu showed permanently disabled entries and selected text could not be
copied by mouse. The editor now carries the decompiler view's editor
menu shape: Copy (rich HTML copy with plain fallback, enabled while a
selection exists) and Select All. Enablement is decided in
ContextMenu.Opening, which only the context-request gesture raises;
the test therefore raises ContextRequested instead of calling Open().
Assisted-by: Claude:claude-fable-5:Claude Code
A one-line payload (short hex dump, tiny source-link document) rendered
as a squeezed strip barely taller than the row itself, which does not
read as an expandable details area. Floor the editor at 100px so the
details region stays visually recognisable regardless of payload size.
Assisted-by: Claude:claude-fable-5:Claude Code
The text-blob editor in the metadata row-details area hardcoded its font
and lacked the text view's flat selection highlight, so embedded source
looked different from the decompiled code right next to it and ignored
the user's font choice in the Options page. The decompiler-view look
(user-selected font applied live while attached, themed background,
square-cornered translucent selection) now lives in DecompilerTextEditor
itself, giving every surface hosting the editor the same appearance by
construction; DecompilerTextView and BuildTextBlob drop their now
redundant per-site styling.
Assisted-by: Claude:claude-fable-5:Claude Code
The CustomDebugInformation details area rendered decoded text payloads
(embedded source, source-link JSON, hex dumps) in a plain TextBox, which
shows code without any highlighting and materializes the whole formatted
text up front, so large embedded-source documents were expensive. Text
payloads now travel as a TextBlobDetail tagged with a file extension --
".json" for source link, the parent document's extension for embedded
source, none for hex -- and render in the theme-aware AvaloniaEdit
editor, which colours them via the existing highlighting registry and
virtualizes long documents. The editor's ThemeChanged subscription moves
from the constructor to OnAttachedToVisualTree so it stays paired with
the detach-time unsubscribe now that editors can leave and re-enter the
visual tree inside recycled row-details containers.
Assisted-by: Claude:claude-fable-5:Claude Code
The details content was pinned left and the text blob capped at 800px,
leaving dead space to the right of embedded-source text and the
flags/typed sub-grids. Let all three detail shapes stretch and give the
last sub-grid column the leftover width so the details area fills its
host row.
Assisted-by: Claude:claude-fable-5:Claude Code
The dotnet-hosted Roslyn 2.10 build cannot start its VBCSCompiler server
under a current dotnet host, so with /shared every test compilation first
waited out the client's full 20-second new-server connection timeout
before falling back to a sub-second in-process compile. Since the 2.10
configurations were enabled on non-Windows (#3914), that added ~29
minutes to the Linux CI job and ~43 minutes on macOS: ~340 affected
tests at ~21s each, versus ~0.2s for the toolsets whose server works.
Assisted-by: Claude:claude-fable-5:Claude Code
Review follow-up. A display-class field initialized from a non-this
parameter is now the only shape where propagation and a later mutation
coexist; it stays sound only because ResolveVariableToPropagate accepts
a parameter with LoadCount == 1, so the mutation can be redirected to
it. Nothing covered that, so Test12 pins it, and Test13 records the
neighbouring shape where the mutation happens inside a lambda - there
capturing the display class keeps it materialized and propagation never
arises. The guard predicate is renamed to say what it matches, since
'ReadOnly' reads like the C# keyword rather than 'a plain read'.
Assisted-by: Claude:claude-fable-5:Claude Code
A field that is propagated to the variable it was initialized from is
replaced by that variable, so re-emitting its initializer assigns the
variable to itself. Where the field was initialized from 'this' the
result does not even compile ('this = this'). The store is dropped
instead, which is what VisitStObj already does for initializer stores
that are not part of an object-initializer block; the insertion position
has to be tracked separately from the loop index, because skipping a
store would otherwise push the following ones past the end of the block.
Assisted-by: Claude:claude-fable-5:Claude Code
Aggressive scalar replacement propagated a display-class field to its
source variable even when the field is mutated after initialization,
aliasing two distinct source-level variables (Test9: thisField and
this). Propagation is now cancelled when the field sees a second store
or its address escapes, but only for propagation targets that cannot
absorb the store: 'this' and variables that are themselves
scalar-replaced display classes. Parameters continue to propagate,
because their remaining uses are already restricted by
ResolveVariableToPropagate and a captured parameter mutated inside a
lambda (DelegateConstruction's Bug951) must keep mapping to the
parameter. Checking CanPropagate first also keeps the guard away from
Mono state-machine fields, whose VariableToDeclare is pre-bound to a
state-machine variable that Propagate(null) would discard.
Re-enables Test9 and adds Test10 covering the escaping-address variant
(Interlocked.Exchange(ref displayClass.thisField, ...)).
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
Assisted-by: Claude:claude-fable-5:Claude Code