Skip to content

Emit C# 7.1 default literals - #3913

Open
siegfriedpammer wants to merge 7 commits into
masterfrom
default-literal-conversion
Open

Emit C# 7.1 default literals#3913
siegfriedpammer wants to merge 7 commits into
masterfrom
default-literal-conversion

Conversation

@siegfriedpammer

Copy link
Copy Markdown
Member

Implements target-typed default output (C# 7.1), building on the conversion-test groundwork from #3912.

Semantics layer

  • Conversion.DefaultLiteralConversion and DefaultLiteralResolveResult implement the standard's default literal conversions (10.2.16), resolving the long-standing TODO in CSharpConversions.ImplicitConversion. The resolve result is a compile-time constant, like the null literal.
  • Unit tests pin overload resolution, operator resolution and type inference behavior for default-literal arguments against compiled csc probes: better-conversion-target tie-breaks, CS0121-style ambiguity, no participation in type inference (CS0411) but riding along when another argument fixes the type parameter, and binary operators with one default operand. No production changes were needed there — the existing resolution stack already matches csc.

Syntax + emission

  • DefaultValueExpression.Type is now optional; without a type the output visitor prints the bare default literal.
  • New DecompilerSettings.DefaultLiterals (C# 7.1, on by default) and IntroduceDefaultLiterals transform: default(T) becomes default in contexts where the target type is explicit in the surrounding syntax and identical to T — declaration/field initializers, simple assignment right-hand sides, and return statements (including async Task<T>). An identity conversion is required, because e.g. object o = default(SomeStruct); boxes a non-null struct while the bare literal would be null.
  • Call arguments follow the implicitly-typed-out-variable pattern: the overload resolution recheck receives untyped DefaultLiteralResolveResults and falls back to the typed form if the expected member is no longer the unique winner. Validated arguments are annotated rather than rewritten, because later transforms can move an argument into an operator operand where the untyped literal would be invalid; IntroduceDefaultLiterals honors the annotation only if the expression is still in an argument position. Operator method calls never participate.

Tests

  • New DefaultLiteral pretty fixture covering the positive contexts and the must-stay-typed ones (ambiguous overloads, boxing targets, indexer arguments, user-defined operator operands, == default comparisons).
  • New OverloadResolution pretty fixture pinning the disambiguation syntax CallBuilder emits (numeric/object casts, (int?), params expanded vs. array form, explicit type arguments, typed vs. bare default), plus a CS71 section in the Correctness fixture executing the same shapes so the recompiled output must pick the same overloads at run time.
  • Fixture updates across the suite are gated with #if CS71 so pre-7.1 compiler configurations (legacy csc, mcs, Roslyn 1.3.2 — filtered out on non-Windows platforms) keep compiling the typed form.

This pull request was authored by an AI agent (Claude Code) operated by @siegfriedpammer.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds end-to-end support for emitting C# 7.1 target-typed default literals in ILSpy’s decompiler pipeline, including semantic modeling/conversions, AST emission, a new transform, and expanded fixture coverage to validate overload/operator behavior.

Changes:

  • Implement typeless default-literal semantics (DefaultLiteralResolveResult) and default-literal implicit conversions.
  • Emit default (without (T)) when safe via a new IntroduceDefaultLiterals transform and a DecompilerSettings.DefaultLiterals toggle (default-on, requires C# 7.1).
  • Extend pretty/correctness/semantics tests and fixtures to pin behavior (overload resolution, type inference interactions, operators, and “must stay typed” contexts).

Reviewed changes

Copilot reviewed 48 out of 49 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
ILSpy/Properties/Resources.resx Adds UI string for the new DefaultLiterals setting.
ILSpy/Properties/Resources.Designer.cs Exposes the new localized resource string via the generated designer.
ICSharpCode.Decompiler/Semantics/DefaultLiteralResolveResult.cs Introduces a typeless resolve result representing the C# default literal.
ICSharpCode.Decompiler/Semantics/Conversion.cs Adds a dedicated DefaultLiteralConversion and corresponding flags/stringification.
ICSharpCode.Decompiler/DecompilerSettings.cs Adds DefaultLiterals setting, language-version gating, and minimum-version calculation.
ICSharpCode.Decompiler/CSharp/Transforms/IntroduceDefaultLiterals.cs New transform that safely replaces default(T) with default in target-typed contexts.
ICSharpCode.Decompiler/CSharp/Syntax/Expressions/DefaultValueExpression.cs Makes DefaultValueExpression.Type nullable to represent bare default.
ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs Implements implicit conversion classification for DefaultLiteralResolveResult.
ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs Emits default without parentheses/type when Type is null.
ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs Adds IntroduceDefaultLiterals into the transform pipeline.
ICSharpCode.Decompiler/CSharp/CallBuilder.cs Adds overload-resolution “recheck” support for substituting typeless default literals and annotating safe arguments.
ICSharpCode.Decompiler/CSharp/Annotations.cs Adds UseImplicitlyTypedDefaultAnnotation to mark verified argument sites.
ICSharpCode.Decompiler.Tests/TestCases/VBPretty/Issue1906.cs Updates VBPretty fixture to use default (needs CS71 guarding for older compilers).
ICSharpCode.Decompiler.Tests/TestCases/VBPretty/Async.cs Updates VBPretty async fixture to use default in specific branches (needs CS71 guarding for older compilers).
ICSharpCode.Decompiler.Tests/TestCases/Pretty/ValueTypes.cs Uses default under #if CS71 for value-type defaults.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/UsingVariables.cs Updates using-variable fixture to prefer default where applicable.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Using.cs Uses default under #if CS71 in conversion/using patterns.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/UserDefinedConversions.cs Uses default under #if CS71 for user-defined conversion return values.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/UnsafeCode.cs Uses default under #if CS71 in unsafe-related defaults.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Structs.cs Uses default under #if CS71 for struct defaults.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/StringInterpolation.cs Uses default under #if CS71 for generic default return.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/StaticAbstractInterfaceMembers.cs Updates default return to bare default.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefFields.cs Updates Span defaults to bare default.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/QueryExpressions.cs Uses default under #if CS71 for query helper defaults.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/PointerArithmetic.cs Uses default under #if CS71 for pointer assignment defaults.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/OverloadResolution.cs New pretty fixture covering disambiguation patterns and default literal argument selection.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs Updates generic out assignment to default.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.cs Uses default under #if CS71 in struct-returning members.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullableRefTypes.cs Updates FirstOrDefault-style returns to bare default.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/MultidimensionalArray.cs Uses default under #if CS71 for array element assignments.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Loops.cs Uses default under #if CS71 for generic accumulator initialization.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/LocalFunctions.cs Updates local-function/generic defaults to bare default.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3584.cs Uses default under #if CS71 in indexer-return path.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3571_A.cs Updates readonly-struct defaults to bare default.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3571_B.cs Updates readonly-struct defaults to bare default.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3571_C.cs Updates readonly-struct defaults to bare default.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs Updates inline-array test defaults to bare default.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/InitializerTests.cs Uses default under #if CS71 in initializer/indexer/struct-ctor patterns.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExpressionTrees.cs Uses default under #if CS71 for DateTime return default.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateConstruction.cs Uses default under #if CS71 for generic argument defaulting.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/DefaultLiteral.cs New pretty fixture covering safe contexts and “must stay typed” cases.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs Updates defaults in deconstruction helpers to bare default.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/CompoundAssignmentTest.cs Uses default under #if CS71 in multiple struct-operator test blocks.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncUsing.cs Updates async-using struct defaults to bare default.
ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue2260SwitchString.cs Updates ILPretty fixture defaults to bare default.
ICSharpCode.Decompiler.Tests/TestCases/Correctness/OverloadResolution.cs Adds runtime correctness checks for default literal overload/operator selection under #if CS71.
ICSharpCode.Decompiler.Tests/Semantics/OverloadResolutionTests.cs Adds resolver-level tests for overload selection/type inference/operator behavior with default literals.
ICSharpCode.Decompiler.Tests/Semantics/ConversionTests.cs Replaces ignored placeholder with real default-literal conversion assertions.
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs Adds Pretty test runners for the new DefaultLiteral and OverloadResolution fixtures.
Files not reviewed (1)
  • ILSpy/Properties/Resources.Designer.cs: Generated file
Comments suppressed due to low confidence (1)

ICSharpCode.Decompiler.Tests/TestCases/VBPretty/Async.cs:56

  • Same issue as above: this block is exercised for Roslyn 1.3.2 builds where default literals are not supported. Guard with #if CS71 (or keep the typed default(HopToThreadPoolAwaitable) form).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

public void M()
{
Console.WriteLine(Math.Min(Math.Max(long.MinValue, default(long)), long.MaxValue));
Console.WriteLine(Math.Min(Math.Max(long.MinValue, default), long.MaxValue));
{
#if LEGACY_VBC || (OPTIMIZE && !ROSLYN4)
YieldAwaitable yieldAwaitable2 = default(YieldAwaitable);
YieldAwaitable yieldAwaitable2 = default;
Resolves the long-standing TODO in CSharpConversions.ImplicitConversion.
The typeless default literal is represented by DefaultLiteralResolveResult
(same pattern as ThrowResolveResult) and classified by the new
Conversion.DefaultLiteralConversion, valid to any target type. Like the
null literal, the resolve result is a compile-time constant.

Also pins the resolution behavior needed before bare default literals can
be emitted, verified against compiled csc probes: overload resolution
selects by better conversion target (int over long, string over object,
int over int?), no better target means ambiguity, the literal does not
participate in type inference but rides along when another argument fixes
the type parameter, and binary operator resolution works with one default
operand but rejects "default == default". No production changes were
needed for these: OverloadResolution, TypeInference and the operator
resolver already handle the typeless resolve result exactly like csc
handles the default literal.

Assisted-by: Claude:claude-fable-5:Claude Code
A DefaultValueExpression without a type is the C# 7.1 default literal;
the output visitor prints it as a bare "default". The grammar comment on
the node already documented the type as optional. Together with
DefaultLiteralResolveResult this prepares for target-typed default output;
the end-to-end pretty fixture follows once the decompiler emits it.

Assisted-by: Claude:claude-fable-5:Claude Code
Adds the DefaultLiterals setting (the first C# 7.1 setting) and the
IntroduceDefaultLiterals transform, which rewrites default(T) to the
default literal in exactly the contexts where the shorter form cannot
change semantics: initializers of variable and field declarations with
an explicit type, right-hand sides of simple assignments, and return
statements (including async Task<T>). The rewrite requires an identity
conversion between T and the target type, because anything weaker
changes the value ("object o = default(SomeStruct);" boxes a non-null
struct, while "object o = default;" is null).

Arguments and operands are deliberately left typed for now: there the
default literal participates in overload resolution, operator resolution
and type inference. The behavior needed to lift that restriction later
is already pinned by the OverloadResolutionTests default-literal tests;
the DefaultLiteral pretty fixture pins today's conservative choice.

Assisted-by: Claude:claude-fable-5:Claude Code
Like implicitly typed out variables, arguments that are default value
expressions are presented to the overload resolution recheck as untyped
DefaultLiteralResolveResults; if the expected member is no longer the
unique winner, the recheck ladder falls back to the typed form before
attempting more invasive fixes such as casting arguments. This lets
M(default) be emitted whenever csc would resolve it identically,
including through better-conversion-target tie-breaks (e.g. overloads
taking Data and Data?), while genuinely ambiguous overload sets and
non-identity parameter types keep default(T).

Two invariants keep the substitution sound: it only applies to the
resolve results handed to the recheck, never to the final
CSharpInvocationResolveResult (transforms like
ReplaceMethodCallsWithOperators pattern-match on the typed constants);
and the argument expression is only marked with
UseImplicitlyTypedDefaultAnnotation rather than rewritten in place,
because later transforms can move the expression out of the argument
position, e.g. into an operand of a lifted comparison, where the untyped
literal would be invalid. IntroduceDefaultLiterals acts on the
annotation only if the expression is still an argument. Operator method
calls never participate, since operator syntax does not accept untyped
default operands.

Assisted-by: Claude:claude-fable-5:Claude Code
The fixtures updated for default-literal output also run under compiler
configurations without C# 7.1 support (legacy csc, mcs, Roslyn 1.3.2 via
defaultOptions/roslynOnlyOptions), which are filtered out on non-Windows
platforms and therefore never ran during the original sweeps. Those
configurations both compile the fixture source and decompile with
matching language-version settings, so each rewritten line needs the
typed form in the pre-CS71 branch: bare "default" under "#if CS71",
"default(T)" under "#else". Fixtures whose tests only run Roslyn 2.10+
configurations stay ungated, since CS71 is always defined there.

Assisted-by: Claude:claude-fable-5:Claude Code
The new Pretty fixture pins the disambiguation artifacts CallBuilder must
emit for the round trip to re-resolve identically: integer-width and
object/string casts, (int?) on values but plain null where unambiguous,
params expanded vs. array form (with default literals in the expanded
call), explicit type arguments against a non-generic overload, typed
default(T) for ambiguous overloads and equality operands, and the bare
default literal where a better conversion target decides.

The Correctness fixture gains a CS71 section executing the same shapes,
so the recompiled decompilation must not only compile but pick the same
overloads and operators at run time.

Assisted-by: Claude:claude-fable-5:Claude Code
Ported from the issue #829 coverage audit, where the fixture needed
EXPECTED_OUTPUT splits because the decompiler never emitted the bare
literal. Re-baselined on the default-literal feature the fixture now
round-trips as-is and documents both directions of the design: bare
"default" in initializers, assignments, returns and unambiguous call
arguments; the typed or folded spelling for optional-parameter
defaults, ambiguous overloads, operator and conditional operands,
"in" arguments, array initializers, pointers and expression trees.

Assisted-by: Claude:claude-fable-5:Claude Code
@siegfriedpammer
siegfriedpammer force-pushed the default-literal-conversion branch from 49ab918 to 69b11a1 Compare July 28, 2026 19:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants