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
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
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
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
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
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
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
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
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
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.
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
Generic co-/contravariance had no test coverage at all: no out/in
variance modifier on any interface or delegate declaration and no
variant reference conversion appeared anywhere in the test suite.
The new fixture pins declarations (including a constrained covariant
interface and an explicit implementation of a variant interface) and
conversion shapes that must not produce explicit casts.
Assisted-by: Claude:claude-fable-5:Claude Code
Compilation uses the .NET builds of the Roslyn toolsets (tasks/netcore*,
bincore csc.dll/vbc.dll launched through the dotnet host). ilasm/ildasm
options use the '-' prefix, which all platforms accept. The dotnet-hosted
compilers have no implicit references or SDK path: net40 compiles pass
mscorlib explicitly, and vbc gets -sdkpath, _MYTYPE=Empty and
-vbruntime:Microsoft.VisualBasic.Core.dll (the facade in the ref packs is
not followed for runtime helpers). The TestRunner gets a self-contained
build for the host platform.
Configurations depending on Windows-only tools or runtimes (legacy
csc/vbc, Roslyn 1.x/2.x, mcs, Force32Bit, executing net40 binaries) are
filtered from the matrix off-Windows via Tester.SupportedOnCurrentPlatform
or gated with [Platform("Win")]. PdbGen comparisons normalize document
name separators, and Correctness/Async uses Console.IsInputRedirected
instead of the Windows-only Console.CapsLock.
Assisted-by: Claude:claude-fable-5:Claude Code
* Fix anonymous-type lambda early-return emitting unresolvable cast
When a lambda's inferred return type contains an anonymous type and one
branch returns null, the decompiler emitted an explicit cast such as
`return (IEnumerable<<>f__AnonymousType0<int>>)null;`, which is invalid C#.
Skip the cast in IsPossibleLossOfTypeInformation for null literals whenever
the expected type contains an anonymous type:
null is implicitly convertible to any reference type, so no cast is needed,
and the anonymous type has no nameable form to cast to anyway.
Fixes#3751
Various improvements regarding primary constructor decompilation, including:
- introduce `HasPrimaryConstructor` property in the AST, as there is a difference between no primary constructor and a parameterless primary constructor
- improved support for inherited records and forwarded ctor calls
- exclude non-public fields and properties in IsPrintedMember
- introduce an option to always make the decompiler emit primary constructors, when possible
* Parenthesize interpolations containing global::
* Improvements:
* Cleaner output
* More unit testing
* More efficient tree search
* Implement revisions
* Update Lambda1 to be invariant
* Visit descendents before deciding whether or not to parenthesize an interpolation expression
* Rename local function
* Remove branch for conditional expressions
* Handle Lambda expressions without a block body
* Check for parenthesized expressions
* `NET60` instead of `!NET40`