Emit C# 7.1 default literals - #3913
Open
siegfriedpammer wants to merge 7 commits into
Open
Conversation
siegfriedpammer
force-pushed
the
default-literal-conversion
branch
from
July 26, 2026 16:50
a621962 to
49ab918
Compare
Contributor
There was a problem hiding this comment.
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 newIntroduceDefaultLiteralstransform and aDecompilerSettings.DefaultLiteralstoggle (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
defaultliterals are not supported. Guard with#if CS71(or keep the typeddefault(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
force-pushed
the
default-literal-conversion
branch
from
July 28, 2026 19:22
49ab918 to
69b11a1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements target-typed
defaultoutput (C# 7.1), building on the conversion-test groundwork from #3912.Semantics layer
Conversion.DefaultLiteralConversionandDefaultLiteralResolveResultimplement the standard's default literal conversions (10.2.16), resolving the long-standing TODO inCSharpConversions.ImplicitConversion. The resolve result is a compile-time constant, like the null literal.Syntax + emission
DefaultValueExpression.Typeis now optional; without a type the output visitor prints the baredefaultliteral.DecompilerSettings.DefaultLiterals(C# 7.1, on by default) andIntroduceDefaultLiteralstransform:default(T)becomesdefaultin 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 asyncTask<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.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;IntroduceDefaultLiteralshonors the annotation only if the expression is still in an argument position. Operator method calls never participate.Tests
DefaultLiteralpretty fixture covering the positive contexts and the must-stay-typed ones (ambiguous overloads, boxing targets, indexer arguments, user-defined operator operands,== defaultcomparisons).OverloadResolutionpretty 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.#if CS71so 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