Two paths generate a local named "field" inside an accessor - a local
typed after a class named Field, and one named after the GetField method
it is assigned from - and C# 14 rejects that identifier there (CS9273).
Worse than the compile error, the local shadows the keyword, so a
backing-field read silently becomes a read of the local; both paths now
have a fixture, verified to regress without the name reservation.
A static member named "field" cannot be disambiguated with "this.", so
the accessor has to name its declaring type; that path had no coverage
either.
Assisted-by: Claude:claude-opus-5:Claude Code
Backing-field references inside a property's own get/set/init accessors
are emitted as the `field` keyword at IL-to-AST translation time
(ExpressionBuilder.ConvertField), so arbitrary accessor bodies become
expressible and no separate rewrite pass is needed. Compiler-generated
trivial accessors then collapse individually to `get;`/`set;`, which
also handles mixed shapes like `{ get; set { ... field ... } }`; the
backing-field declaration is removed with its remaining attributes
re-hosted as `field:` sections, and constructor stores become property
initializers (or property assignments, for setter-less properties).
Implicit zero-stores that auto-default struct constructors emit for
unassigned backing fields are dropped rather than lifted.
Recognition stays AST/metadata-based rather than mirroring the ILAst
analysis used for automatic events: events must prove compiler-generated
bodies before discarding them, while the field keyword discards nothing,
so name association plus the accessor context is sufficient.
Below C# 14 (or with the new FieldKeyword setting off), the field
declaration survives under its metadata name, so the UI keeps showing
the truth; EscapeInvalidIdentifiers - the transform the compilable-output
flows (project export, VS, tests) already add - now maps
`<P>k__BackingField` to the readable `P__BackingField` instead of the
generic character escape. A genuine field literally named "field" is
qualified as `this.field` inside accessors, and locals are not named
"field" there, since C# 14 rebinds the bare identifier. Bodiless
accessors mixed into multi-line properties get their own line in the
output.
The fixture covering the feature surface lands with the implementation rather
than as a separate xfailed commit. It is excluded from the test-assembly
compilation because its nullable annotations would trip warnings-as-errors
there.
Assisted-by: Claude:claude-fable-5:Claude Code
The parameter-list-less anonymous method form is compatible with any
delegate signature, and C# code must rely on exactly that when a
delegate's parameter types cannot be named at the use site: IL, unlike
C#, permits a delegate signature to reference less accessible types.
Expanding such an anonymous method into a lambda would force the
unnameable type into a parameter list. Keep the delegate form, with its
parameter list dropped, when the parameters are unused and one of their
types is not accessible from the current context.
Assisted-by: Claude:claude-fable-5:Claude Code
Under UseLambdaSyntax, anonymous functions became lambdas only when an
expression body was possible; statement-bodied ones kept C# 2 delegate
syntax. Now every anonymous function whose parameter shape a lambda can
express uses lambda syntax; delegate syntax remains for ref/out/in and
params parameters and for pre-C# 3 language profiles.
Two latent issues surfaced by the wider lambda coverage: DeclareVariables
assumed an insertion point directly under a LambdaExpression is an
expression body it must convert to a block, which block-bodied lambdas
now violate; and anonymous methods declared without a parameter list
carry compiler-generated parameter names like '<p0>' that are not valid
identifiers, so the lambda's mandatory parameter list regenerates such
names from the parameter type: (object obj, EventArgs e) => ...
A side effect visible in fixtures: an explicit parameter list can make
a delegate-creation cast redundant that bare 'delegate' syntax needed
for overload resolution, e.g. new Thread((ThreadStart)delegate { })
becomes new Thread(() => { }).
Assisted-by: Claude:claude-fable-5:Claude Code
Element 8+ of a long tuple is read through the Rest field, which Roslyn
loads by value; ILSpy turned that into an addressof over the loaded
copy, hiding the tuple field chain from every downstream matcher. Elide
the copy when the enclosing expression only reads through it - the read
then goes directly through the original address and folds into the
usual Item_N chain. The deconstruction transform's use-shape guard also
learns to walk that chain; whether each read really is a consumable
element access remains the job of MatchTupleElementRead and the escape
check.
Assisted-by: Claude:claude-fable-5:Claude Code
Deconstruction assignment copies the right-hand side into a temporary
before calling Deconstruct. When the RHS is a call, inlining folds that
temporary away, but for a local or parameter it survived into the
output as a separate assignment statement. Consume the copy into the
deconstruction pattern; rendering the copied value as the RHS
recompiles to the identical temporary. Because blocks are processed
back to front, the call-position match defers to the attempt starting
at the copy, mirroring the existing nested-deconstruction defer guard.
The new fixture also covers deconstruction assignment to locals
captured by a lambda in an async method (issue #3037's crash shape,
already fixed earlier).
Assisted-by: Claude:claude-fable-5:Claude Code
Issue #3275 reported an ArgumentOutOfRangeException in
ExpressionBuilder.ConstructTuple for exactly this shape; the crash was
fixed by the nested-deconstruction rework, but no fixture pinned the
record-struct variant, whose Deconstruct methods are compiler-generated.
Assisted-by: Claude:claude-fable-5:Claude Code
The guard added here reads the target of a member access to decide whether an
assignment may move into a field initializer, and it recognises the current
instance as a ThisResolveResult. Only one of the two spellings produces that.
An unqualified `A` is resolved through CSharpResolver.LookInCurrentType, which
synthesizes the target as a this-reference; an explicit `this.A` is built by
ExpressionBuilder, whose TranslateTarget hands back whatever ConvertVariable
produced - and `this` is a parameter like any other there, so the target is an
ILVariableResolveResult. The guard saw the first and missed the second.
Which spelling appears is decided by RequiresQualifier, for reasons unrelated
to the question being asked: a constructor parameter that shadows the field
forces the qualifier, and AlwaysQualifyMemberReferences forces it everywhere.
So the transform hoisted `b = this.value + 1` into a field initializer, where
naming the instance is CS0027 and the output does not compile.
TranslateTarget already builds a ThisResolveResult for `base`, one branch
above. Doing the same for `this` leaves the guard untouched and makes it see
both spellings, and spares every future consumer the same trap. The type is
carried over from the previous resolve result, so nothing downstream observes
a different one - the this/base keyword links read exactly this node.
Fixes#3984.
Assisted-by: Claude:claude-opus-5:Claude Code
A moved field initializer cannot read another instance member. Reject primary-constructor conversion for that case and preserve the original constructor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0dd407b6-9410-48df-add5-761ca4a8dec0
Tuple element names and nullability are not part of a type's identity, so an
interface resolved through one of its members carries neither. Naming an
explicit implementation from that type produced `void I<(int, int)>.M()` on a
type declared as `I<(int A, int B)>`, which the C# compiler rejects outright
with CS0540 - the decompiled source did not build. The nullable case was
already recorded as a TODO in the NullableRefTypes fixture, where the mismatch
costs a CS8643 warning rather than an error.
The implementing type's base-type list is the only place those annotations are
recorded, so the qualifier is looked up there. Three call sites derived it
independently - the AST builder for all five member kinds, the ambience used
for tooltips and tree labels, and the forwarders synthesized for MethodImpls -
so they now share one helper rather than repeating the rule twice more.
Matching while ignoring tuple names and nullability cannot be ambiguous:
implementing two interfaces that differ only in those is itself an error
(CS8140, CS8645).
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A query source can be reached through an indexer as well as through a member
access or a call: `holder?[0].Where(...).Select(...)` puts an IndexerExpression
between the LINQ call and the `?.`. The receiver walk stopped there, so query
syntax was still introduced over a source the conditional access had lifted to
a nullable value type, and the output failed to compile with CS1936 - the same
way as the case that was reported, one node kind further along.
IndexerExpression.Target is nullable where MemberReferenceExpression's and
InvocationExpression's are not, so only that arm needs to match on the target.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Query syntax cannot preserve a null-conditional receiver that lifts a value type. Detect null conditionals through the LINQ receiver chain before introducing query syntax.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0dd407b6-9410-48df-add5-761ca4a8dec0
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
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
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
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
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
Covers ref structs implementing interfaces (implicit, explicit, and
default-interface-method reimplementation, which CS9245 forces on every
ref struct implementer), the allows ref struct anti-constraint on
methods, classes, interfaces, delegates, local functions, iterators,
async methods, and capturing local functions, constraint combinations
(interface, IDisposable with using, unmanaged, struct, new()),
interface members invoked through a constrained T (instance, static
abstract factory, and default interface methods, which are callable
through T because implementers must always override them), scoped/ref/
in/out parameters of T, and call sites instantiating with Span/
ReadOnlySpan type arguments.
The test is green in all four Roslyn 4.14/latest debug/opt configs: the
decompiler already round-trips the gpAcceptByRefLike flag into
"allows ref struct" clauses and re-emits ref struct interface
implementations correctly. One cosmetic quirk is pinned as-is: calls
whose type arguments are ref structs are printed with explicit type
arguments and a declaring-type qualifier even within the declaring
class, because type-argument inference validation does not accept ref
struct type arguments; the output remains compilable and semantically
identical.
Assisted-by: Claude:claude-fable-5:Claude Code
The chain cases in the fixture route T : TOuter through a class-level
type parameter; the variant where the dependency target is a sibling
method type parameter (M<T, U> with T : U) was uncovered. It pins the
same alignment from a different angle: csc rejects 'class' with CS8665
when U is merely class-constrained and requires 'default', but accepts
'class' when U carries a class-type constraint, matching what the
tri-state IsReferenceType derives. Both directions were verified
against csc before adding the expected output.
Assisted-by: Claude:claude-fable-5:Claude Code
The foreach pattern was only matched against using instructions, but
the compiler emits no using/try-finally at all when the enumerator's
static type can never require disposal: a struct or a sealed class
that does not implement IDisposable (SerializationInfoEnumerator in
the issue's example). Such loops stayed while loops.
Recognize the bare 'enumerator = x.GetEnumerator(); while
(enumerator.MoveNext())' shape during statement building and reuse the
existing foreach transformation core for it. The transformation is
restricted to exactly the cases where recompilation would produce the
same IL: the enumerator type rules above (ref structs are excluded
because of pattern-based disposal), a single-store enumerator variable
unused outside the loop, and synchronous enumeration only, since async
enumerators are always IAsyncDisposable.
Assisted-by: Claude:claude-fable-5:Claude Code
allows ref struct is inherited implicitly, so restating it on an override is
CS0460 even alongside a legal disambiguator. Roslyn still re-emits the byreflike
flag on the override's own type parameter, and the general constraint printer
turns that flag back into source, so the disambiguator stays legal only as long
as it is built separately. Cover a C# 13 base whose annotated and plain methods
both allow ref structs.
Assisted-by: Copilot:claude-opus-5:GitHub Copilot CLI
A class-type constraint such as Stream or Delegate sets no
ReferenceTypeConstraint flag, so keying the disambiguator off that flag gave
those overrides the default constraint, which is CS8822, and the output still
did not recompile. The restated disambiguator leaves no metadata trace of its
own, so the choice has to follow from whether the inherited constraints make
the type parameter a reference type, a value type, or neither.
Matching the annotated type parameters by identity rather than by owner kind
and index also keeps a specialized signature from contributing a foreign type
parameter that happens to share an index.
Assisted-by: Copilot:claude-opus-5:GitHub Copilot CLI
Override constraints are normally inherited and omitted, but nullable type
parameters still require class or default to distinguish annotations from
Nullable<T>. Derive that legal discriminator from the method metadata.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
Yield translation derived its target type only from synchronous enumerable
interfaces, leaving async iterator yields untyped and preserving compiler
boxing casts. Use the element type already recovered by the async decompiler.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
ExpressionBuilder printed bitwise & / | on booleans as && / || whenever
the right-hand side was pure, but Roslyn lowers && / || to & / | only
when the right operand is a bare local or parameter read
(LocalRewriter.MakeBinaryOperator, unchanged since 2014). Shapes like
(c == 'a') | (c == 'b') can therefore only originate from a bitwise
source operator, yet were shown as short-circuiting. Per the discussion
in #1545, show the operator the IL actually uses instead of guessing the
source form: the reversal is dropped entirely, so Roslyn-compiled
"a && b" now decompiles to "a & b", which recompiles to the same IL.
Assisted-by: Claude:claude-fable-5:Claude Code
Enum members whose value duplicates an earlier member now reference it
(Item2B = Item2A), [Flags] members combine earlier single-bit members
(All = Item1 | Item2 | Item3) or their complement (NotItem1 = ~Item1)
instead of showing a bare number.
Several guardrails keep the output faithful to how such enums are
written by hand: only previously declared members are referenced (field
row order); a multi-bit value lying entirely within a larger, earlier
member is a field encoding inside that mask, not a flag union, and
stays numeric, as do zero-valued members of [Flags] enums, which
routinely have several unrelated zero members. The ~X form is
suppressed in byte/ushort enum declarations, where the initializer
constant folds in int and would not compile. Enums with unusual
underlying types (bool, native int) keep the plain constant conversion.
With these rules, decompiling System.Private.CoreLib reproduces the
hand-written declarations of TypeAttributes, MethodAttributes,
AttributeTargets and FileAttributes almost verbatim.
Assisted-by: Claude:claude-fable-5:Claude Code
Object creation can require an unsafe context solely because the selected
constructor has a pointer parameter. Apply the existing unsafe-signature
check to object creation nodes so the emitted declaration remains compilable.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
PatternStatementTransform renamed backing-field identifiers to the event
after the fact, keyed on the metadata name association alone: references
belonging to an event that is not actually automatic were still renamed,
binding them to a custom event that is unusable as a value (the same
defect class as #3858), and the rename bypassed the resolver checks, so
qualifiers were computed for the hidden field instead of the printed
event. ExpressionBuilder.ConvertField now performs the substitution,
keyed on the AutoEventDecompiler verdict whose memo moves into
DecompileRun so that member hiding, the event declaration, and reference
translation all decide from one analysis. Checking the verdict's field
identity also keeps same-typed sibling events apart (#3575), and the
qualifier logic running against the event drops spurious this./type
qualifiers from raise sites.
mcs 2.x accesses a sibling automatic event's backing field directly
inside custom accessors instead of calling the accessor, so the fixture
expects the resulting Delegate.Combine form there.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
MemberIsHidden decides field hiding from the metadata name association
alone, which over-approximates: an event whose accessors fail the ILAst
validation is decompiled with explicit accessors, and while a referenced
backing field is re-added through the work list, an unreferenced one was
silently dropped from the output. The type-definition member loop now
consults the same memoized verdict as the event declaration, so the two
decisions agree by construction.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
The syntactic accessor-body patterns in PatternStatementTransform sit
downstream of every settings-dependent transform, so each new compiler
shape or settings combination silently broke recognition: with
AggressiveInlining enabled, static events inline the Delegate.Combine
call into CompareExchange positionally, which none of the four patterns
matched, while call sites were still rewritten to the event name from
metadata alone - producing uncompilable output (CS0079).
Recognition now happens in DoDecompile(IEvent) by structurally matching
the ILAst of the accessors, decompiled with a fixed set of settings the
same way RecordDecompiler analyzes method bodies. This makes detection
independent of the user-visible settings by construction. Events that
are not recognized fall back to the classic path unchanged, including
the existing AST patterns.
mcs 2.x compiles the accessors as a compound assignment, evaluating
'this' once via IL 'dup'; the simple-combine matcher accepts that
stack-slot alias.
Assisted-by: Claude:claude-fable-5:Claude Code
Roslyn caches a ReadOnlySpan<T> created from an array literal in a
<PrivateImplementationDetails> field on target frameworks without
RuntimeHelpers.CreateSpan (e.g. .NET Framework / netstandard2.0 + System.Memory):
object obj = <PrivateImplementationDetails>.cache;
if (obj == null) {
obj = new char[] { '\r', '\n' };
<PrivateImplementationDetails>.cache = (char[])obj;
}
... new ReadOnlySpan<char>((char[])obj) ...
The decompiled output referenced the compiler-synthesized
<PrivateImplementationDetails> type, whose escaped name is not expressible in C#
and is never declared, so the output failed to recompile (CS0400).
The modern RuntimeHelpers.CreateSpan form was already handled
(TransformRuntimeHelpersCreateSpanInitialization); this adds the analogous
handling for the legacy lazy-cache form, mirroring CachedDelegateInitialization
(which collapses the same lazy-static-field cache for anonymous-method delegates).
Once the cache is collapsed, the existing array-initializer transforms recover
the array literal, so the <PrivateImplementationDetails> reference disappears.
Test: ILPretty/CachedReadOnlySpanInitialization.
Added Issue3877 test to PrettyTestRunner and new test case source to verify dictionary initialization with negative capacity. Updated SwitchOnStringTransform to skip processing when a negative dictionary capacity is detected.
When a null literal is the only argument a generic type argument could
be inferred from, and that type argument is an anonymous type, ILSpy
used to drop the type arguments entirely (they cannot be written
explicitly), producing 'Test(null)', which no longer compiles
(CS0411). A conditional expression whose never-taken branch creates an
instance of the anonymous type is the minimal C# expression that gives
a null value that type, so the argument now carries enough information
for type inference.
The instance expression is obtained by translating a synthesized
'newobj' with 'default.value' arguments through the existing pipeline
rather than assembling syntax by hand; the property values are typed
defaults, since the IL only contains ldnull. Anonymous types occurring
inside other constructed types (e.g. arrays) stay unexpressible and
keep the previous output.
Assisted-by: Claude:claude-fable-5:Claude Code
CS8196 also fires when the second reference sits inside a nested call
in another argument of the declaring call, e.g.
OutAndValue(out var a, UseAndReturn(out a)). The existing check already
covers this because it walks all descendants of the sibling arguments;
this fixture pins that behavior.
Assisted-by: Claude:claude-fable-5:Claude Code
Passing the same local as multiple out arguments of one call made
DeclareVariables turn the first use into an implicitly-typed declaration,
producing 'f(out var x, out x)'. Referencing an implicitly-typed out
variable in another argument of the declaring call is rejected by the
compiler (CS8196), because its type is only inferred once overload
resolution of that call has completed. The explicitly-typed form
'f(out int x, out x)' is valid, so fall back to the explicit type in
that case.
Assisted-by: Claude:claude-fable-5:Claude Code
Awaiting a dynamic value lowers GetAwaiter/IsCompleted/GetResult to
dynamic callsites, which async decompilation could not recognize:
await detection ran before DynamicCallSiteTransform, so the await was
emitted as a raw state machine (or crashed in AnalyzeAwaitBlock, #1388).
AnalyzeStateMachine now collapses the awaiter callsites per block, folds
the runtime ICriticalNotifyCompletion branch the compiler emits for an
awaiter not statically known to implement it into the canonical single
call, and re-joins the branch chains the collapse leaves so each dynamic
await sits in one block. DetectAwaitPattern matches the dynamic
GetAwaiter/IsCompleted/GetResult shape and emits `await expr`; a
synthesized dynamic GetResult method gives the await and its local the
dynamic type. DynamicCallSiteTransform also follows callsite targets
spilled into state-machine locals, so an awaited value flowing into a
dynamic callsite (e.g. d.Result = await ..., #1928) decompiles too.
Assisted-by: Claude:claude-fable-5:Claude Code
DynamicIsEventInstruction has no ExpressionBuilder visitor, so any is-event
diamond that survives the transform pipeline leaks as an "OpCode not supported"
comment. The collapse only fired for the plain statement form; every other
lowering the compiler emits for "d.Event += b" was missed:
- a copy-of-value temporary between the getmember cache and the diamond (modern
Roslyn emits it for the statement form),
- the result-used shape, where the diamond is a value-if nested inside the
consuming expression (a call argument, or a leave's value when returned),
- the result-returned shape as two leaves, "if (isevent) leave(add)" plus a
fall-through "leave(compound)" rather than an if/else, and
- the same two-leaves shape when the optimizer drops the getmember cache (legacy
csc and Roslyn <= 2.x): the is-event flag then feeds a single branch, inlining
folds it into the condition, and the compound leave is already collapsed, so
the cache-based entry never matched it and the branch leaked. These compilers
only run on Windows CI, so the gap was invisible on Linux.
The descendants walk handles the nested value-if once an over-strict
copy-of-value bounds guard (which never held for the compact return block) is
dropped. The two two-leaves shapes share a skeleton matcher and the add/remove
accessor-name check, and both validate the accessor name and arguments. Re-running
from the collapsed statement instead of the next one avoids indexing past the end
of a block that has shrunk to a single leave.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A second store to an already-written array element must prevent
HandleSimpleArrayInitializer from folding the stores into a collection
initializer: the earlier store would be dropped (a visible change when
its value has side effects), and without the nextMinimumIndex guard the
transform crashes on the colliding index. The nextMinimumIndex check
already enforces this; pin it with a Pretty fixture so the boundary
between a foldable initializer and a duplicate-write sequence stays
covered.
Assisted-by: Claude:claude-fable-5:Claude Code
The C# compiler lowers a foreach over an inline array into a for loop whose
body reads each element through <PrivateImplementationDetails>.InlineArrayElementRef(ref
buffer, i). That helper cannot be named in C#, so the decompiled for loop did
not compile.
Reconstruct the foreach at the AST level instead. Rewriting the unchecked
InlineArrayElementRef helper to the bounds-checked indexer buffer[i] would be
unsound for an out-of-bounds index, so the transform fires only when the loop
bound equals the inline array's length: that proves 0 <= i < length, matching
the exact shape the compiler emits and nothing else. A loop that does not match
keeps the faithful (if unnameable) helper call rather than silently gaining a
bounds check.
Assisted-by: Claude:claude-fable-5:Claude Code