Copying 'params' from every target delegate's Invoke onto the lambda broke
the Newtonsoft.Json round-trip: each plain '(object[] args) => ...' bound to
a 'params' delegate came back as '(params object[] args)', and below C# 12
the fallback rendered that as '([ParamArray] object[] args)', which no C#
version compiles (CS0674, plus CS8400 for lambda attributes before C# 10).
The lambda's own metadata is the faithful record for named delegate types:
current Roslyn emits ParamArrayAttribute and the default value on the
closure method exactly when the source spelled them, and where Roslyn 4.14
omits the attribute the plain parameter list is equivalent anyway. Only a
compiler-synthesized delegate type has to be spelled through the lambda's
declaration, so that is the one place Invoke is still consulted. Without a
legal pre-C# 12 spelling, the modifiers are now dropped instead of being
turned into attributes.
Assisted-by: Claude:claude-fable-5:Claude Code
C# 10 converts a lambda's function type to System.MulticastDelegate, its base
classes and its interfaces, and an expression tree's to Expression and
LambdaExpression, so 'Delegate d = (Func<int, int>)((int x) => x);' names a
type the language re-infers. The natural-type annotation now also marks
anonymous functions, and the declaration site drops the redundant cast.
The 'var' shortcut stays method-group-only on purpose: 'var' over an
expression tree infers the delegate that the Expression<> wraps, which would
silently turn a tree into a delegate. Discards are unaffected, since only a
declaration ever unwraps - a discard offers no target type, and function
types are not used in assignments to discards.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
C# 10 converts a function type to System.MulticastDelegate, to its base
classes and to its interfaces, but only the Delegate and object targets
dropped the explicit delegate construction. A local declared as
MulticastDelegate, ICloneable or ISerializable kept 'new Func<int, int>(M)'
even though a bare method group binds there just as well. Deriving the set
from MulticastDelegate's base types states the language rule directly instead
of listing two of its five members.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
An anonymous function's own method does not reliably carry ParamArrayAttribute
- Roslyn 4.14 omits it - while the delegate's Invoke method always describes
the full signature, and it is Invoke that call sites bind against. Reading the
modifiers from the anonymous function alone therefore dropped 'params' from
the parameter list while the call was still decompiled in expanded form,
which does not compile: the natural type of the re-emitted lambda is a plain
Func<int[], int>, taking exactly one argument.
Sourcing both modifiers from Invoke also makes the pre-C# 12 downgrade to
[ParamArray]/[Optional] fire on every compiler rather than only the newest.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Pretty tests pin the syntax of a natural-typed lambda, but not what the
syntax then binds to. Where the natural type is a synthesized delegate the
decompiler must reproduce its whole signature - ref parameters, default
values, params, and a ref readonly return - or the output picks a different
overload, or stops compiling. The ref readonly return leaves no trace in
anything an invocation can observe, so the fixture reads the modreq back off
the delegate type.
The params case is compiled only under the current compiler: Roslyn 4.14
emits no ParamArrayAttribute on a lambda's method, so the modifier cannot be
recovered while the call is still decompiled in expanded form.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Ref-readonly-ness is not part of IType; it is a separate flag on the delegate's
Invoke method. The explicit-return-type gate compared types only, so a lambda
whose synthesized delegate returns 'ref readonly' matched its inferred natural
type and the modifier was dropped without a trace. Recompiling that output
synthesizes a plain ref-returning delegate, so the type identity silently
differs from the original.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A method group whose signature fits no Func/Action overload (ref, out,
in, params or default-value parameters) still has a natural type since
C# 10/12: the compiler synthesizes an anonymous delegate type, whose
name is unspeakable. The fixture pins that such values are declared with
var and initialized from the method group, which round-trips to an
equivalent synthesized delegate type, and covers each parameter kind
that forces the synthesis, plus returning the value as object.
Assisted-by: Claude:claude-fable-5:Claude Code
A lambda was wrapped in a cast to its delegate type and a method group in a
construction of it, both of which later collapse away wherever the target type
is that same delegate. Where the target is a base type instead, such as a field
or return type of object or Delegate, nothing collapsed them and the wrapper
spelled out a type that has no name in C# and whose definition is hidden, so
the output did not recompile.
Neither wrapper can ever be written for an anonymous delegate type, so neither
is built now; the conversion rides on the anonymous function itself. That also
keeps the site typed as the delegate rather than as an anonymous function,
which is what lets the conversion to a base type proceed without a cast: the
standard permits it (C# 12 draft, 10.2.21) but the resolver does not model it.
Assisted-by: Claude:claude-fable-5:Claude Code
The compiler names a synthesized delegate type <>A when it returns void and
<>F otherwise, and appends a bit pattern of the by-reference parameters only
when the signature has any. A signature that needs a synthesized type solely
because it has more than 16 parameters is passed entirely by value, so it is
named plain <>A or <>F, which the prefix tests requiring a brace missed. Such
a method group was left as an ordinary delegate construction and the type,
whose definition is hidden as compiler-generated, was spelled out by its
unspeakable metadata name, so the output did not recompile.
The two predicates that decide this, one on the type system and one on
metadata, no longer carry separate copies of the name rule: they cannot drift
apart the way the generated-name predicates did in #3952.
Assisted-by: Claude:claude-fable-5:Claude Code
With MethodGroupNaturalTypeImprovements off, every candidate in every
scope still takes part in the natural type determination, so the
scope-by-scope cases fall back to an explicitly typed local, while a
unique extension method, a unique member, and the Delegate/object
conversions keep their natural-typed form (those are C# 10 features).
Assisted-by: Claude:claude-fable-5:Claude Code
The pretty fixture states the desired round-trip: method groups whose
C# natural type equals the delegate type constructed in the IL come
back as 'var result = M;' (or keep a Delegate/object local's declared
type while dropping the explicit construction). The C# 13 cases cover
scope-by-scope candidate pruning: instance scope before extension
scopes, and arity, constraint and receiver-form pruning within a
scope. Under /o the compiler erases a Delegate/object local's declared
type, so those two cases pin the natural-typed 'var' form instead.
The correctness companion executes both compilations and verifies every
method group still binds to the same target method.
Assisted-by: Claude:claude-fable-5:Claude Code
Pins the natural-typed (var) emission for method groups converted to
synthesized anonymous delegate types, across all three Roslyn name
families (<>A{...}, <>F{...} including ref returns, and
<>f__AnonymousDelegateN for params/default-value parameters), with
implicit-this and expression receivers, invocation through the
natural-typed local, and capture into a lambda. All signatures involve
ref parameters or params/defaults so that no framework delegate type
matches and var output is required rather than stylistic.
The ExplicitTypeArguments case runs red by design: the decompiler
still omits the generic type arguments, which a natural-typed method
group cannot re-infer without a target type (CS8917).
Assisted-by: Claude:claude-fable-5:Claude Code
A delegate construction site can drop the explicit 'new DelegateType(...)'
only when the natural type C# assigns to the emitted method group is
exactly the delegate type the IL constructs; otherwise 'var' (or a
Delegate/object local's initializer) would re-infer a different type or
none at all. MethodGroupNaturalType re-resolves the emitted form and
decides this, mirroring the version-specific rules: C# 13 walks scopes
one at a time (instance members before each extension scope) and prunes
candidates with mismatched arity, violated constraints or the wrong
static/instance form; C# 10 lets every candidate in every scope take
part. Only System.Action/Func (or anonymous delegate) types qualify -
Roslyn never infers a signature-compatible custom delegate type.
CallBuilder annotates qualifying method groups; DeclareVariables uses
the annotation to emit 'var' when the natural type equals the local's
type, and to drop the construction (but keep the declared type) for
Delegate- and object-typed locals. Sites in any other context keep the
explicit construction. Generic groups retry with spelled type
arguments, since a natural type requires them.
Assisted-by: Claude:claude-fable-5:Claude Code
When LambdaOptionalAndParamsParameters is disabled, no anonymous
function syntax can declare 'params' or a parameter default value, so
these were silently dropped. Emit the underlying metadata attributes
([ParamArray], [Optional] plus [DefaultParameterValue]) on the parameter
instead, so the information stays visible. This has to happen in
TranslateFunction: from metadata alone the type system cannot tell a
lambda's method apart from a local function's, where the sugar stays
legal in older language versions, so neither a TypeSystemOptions flag
nor IsDefaultValueAssignmentAllowed can make this distinction.
RequiredNamespaceCollector adds System.Runtime.InteropServices for
optional parameters of method parts, since the downgrade decision is
made only later, during translation.
Assisted-by: Claude:claude-fable-5:Claude Code
A lambda body consisting of one ExpressionStatement was rendered as a
block, because only a single 'return' qualified for the expression form.
Any single statement-expression is a legal expression body - its value,
if there is one, is discarded and the lambda stays void-returning - so
braces are only needed for genuinely multi-statement bodies.
Assisted-by: Claude:claude-fable-5:Claude Code
Anonymous methods can never declare 'params' (CS1670) or parameter
default values (CS1065); since C# 12 lambdas can. TranslateFunction
printed both modifiers on whichever syntax it had chosen anyway, so
closure methods carrying ParamArrayAttribute or a default value
decompiled to uncompilable anonymous methods.
Force lambda syntax when a parameter carries one of these shapes, gated
by a new LambdaOptionalAndParamsParameters setting (C# 12). Below that
version the modifiers are dropped instead: the delegate type still
provides both, so they are purely decorative on the anonymous function.
The fixture branches per compiler because Roslyn 4.14 does not emit
ParamArrayAttribute on the synthesized lambda method (the modifier is
then unrecoverable), while current Roslyn does.
Assisted-by: Claude:claude-fable-5:Claude Code
Method groups and lambdas whose shape Action/Func cannot express (ref
parameter or return kinds, and with pointers, params, or default values
the non-generic fallback) get compiler-synthesized delegate types:
<>A{flags} (void-returning), <>F{flags} (value-returning), and
<>f__AnonymousDelegateN. Their names are unspeakable, so declared
variables printed the escaped type name and did not compile.
Detect them (generated name in one of the three families, delegate
kind, CompilerGenerated, no namespace), declare locals of such types as
'var', hide the synthesized type definitions, name the locals 'anon',
and let DelegateConstruction accept the synthesized methods. Since the
site is then typed solely by the anonymous function's natural type,
lambda syntax is mandatory: 'delegate {}' without a parameter list has
no natural type, and the 'delegate' form cannot declare a return type.
When the return type C# would re-infer from the emitted body differs
from the delegate's (a widened return, or a discarded value in an
expression body), the lambda declares the delegate's return type
explicitly (C# 10).
The LambdaReturnTypes fixture pins that last part: in a delegate-typed
context an explicit return type leaves no trace in metadata, so the
syntax only matters for natural-typed lambdas. Every case has a ref
parameter, which makes 'var' the only legal declaration, and the return
type appears exactly where it is load-bearing - widening the body's type
to object, or forcing void over an inferable int - and is omitted where
inference recovers it.
Gated by a new NaturalTypeForLambdaAndMethodGroup setting (C# 10).
Assisted-by: Claude:claude-fable-5:Claude Code
The C# 10 grammar only allows attributes on a lambda or its parameters
when the parameter list is parenthesized, but LambdaNeedsParenthesis
predates attribute support and only considered the single parameter's
type and modifiers. An attributed lambda whose parameter type is erased
for being anonymous therefore printed as '[My] a => a.X', which does not
parse. Latent since attributed-lambda decompilation was added: every
other attributed lambda has explicitly typed parameters, which already
force the parenthesized form.
Assisted-by: Claude:claude-fable-5:Claude Code
Structs whose assembly is missing decompile through the unresolved-type
path, which is not covered anywhere: the fixture pins the constructor
shapes that path has to recognize, and the reference-type cases that must
keep falling through to a plain call.
Assisted-by: Claude:claude-opus-5:Claude Code
Comments used to be child nodes flushed by InsertSpecialsDecorator when the
next node started printing, which put the marker of an init-only setter right
after the keyword. In the slot AST comments are leading/trailing trivia, so the
accessor's trailing trivia moved the marker behind the accessor body. The
placement cannot go back to trivia on the body either: the auto-property
transform drops the body, and the marker with it. The accessor now carries the
init-only fact itself and the printer writes the marker next to the keyword.
Assisted-by: Claude:claude-opus-5:Claude Code
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
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
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
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