Skip to content

Emit C# 7.1 default literals where the context supplies the type - #4014

Open
christophwille wants to merge 3 commits into
masterfrom
default-literals-convertto
Open

Emit C# 7.1 default literals where the context supplies the type#4014
christophwille wants to merge 3 commits into
masterfrom
default-literals-convertto

Conversation

@christophwille

Copy link
Copy Markdown
Member

Alternative implementation of #3913: same feature, same tests, different mechanism.

Approach

Shortening default(T) is the same problem as removing the redundant cast around a lambda whose delegate type the context already fixes, so it uses the same mechanism instead of a dedicated AST transform: default(T) is emitted as before, and TranslatedExpression.ConvertTo(targetType, allowImplicitConversion: true) makes the explicit type implicit when the conversion is an identity conversion - a few lines above the existing lambda/ConversionResolveResult case in the very same block.

  • Shorten: the DefaultValueExpression loses its type and is annotated with DefaultLiteralResolveResult, which remembers the type it was shortened from.
  • Restore: any later ConvertTo that is not implicit, or that supplies a different type, spells default(T) out again and converts that. This is what keeps object o = default(SomeStruct) boxing (the bare literal would be null), and what CastArguments uses when an overload resolution recheck rejects the untyped literal.
  • Because the shortened literal resolves to DefaultLiteralResolveResult, CallBuilder's existing IsUnambiguousCall recheck sees a real default literal and rejects ambiguous calls on its own - no bookkeeping about which arguments may stay untyped, no argument annotations.

Only the contexts that supply no target type at all restore the explicit form via RestoreDefaultLiteralType: an awaited expression (await default does not compile) and arguments of operator methods (ReplaceMethodCallsWithOperators turns those into operator or cast syntax). Two further spots opt in to the shortening because their declaration always spells out the type: DeclareVariables' synthesized initializer and using resource declarations.

Compared to #3913 this drops the IntroduceDefaultLiterals transform, UseImplicitlyTypedDefaultAnnotation and the ArgumentList.UseImplicitlyTypedDefault plumbing in CallBuilder. Kept from it unchanged: optional DefaultValueExpression.Type plus output visitor, DefaultLiteralResolveResult / Conversion.DefaultLiteralConversion / the CSharpConversions hook, and the DefaultLiterals setting (C# 7.1, on by default).

Tests

The tests are the ones from #3913. Three expectations differ, because ConvertTo covers different ground than the transform did - each fixture is also the compiled input, so csc validates every one of them:

  • this[default] - indexer arguments shorten (Emit C# 7.1 default literals #3913 pinned them typed); the fixture method is renamed IndexerArgumentStaysTyped -> IndexerArgument.
  • () => default in GuidExpressionTree - expression-tree lambda bodies shorten.
  • Params(default(int), default(int), default(int)) - params-array elements synthesized by the params expansion never pass through ConvertTo, so they keep their type.

These fixtures needed updating on top of #3913's set, because ConvertTo reaches contexts the transform did not: RefFields, RefStructInterfaces, FirstClassSpanTypes, FirstClassSpanConversions, QueryExpressions, DeconstructionTests, SpanConversionOperatorMismatch. In Issue2260SwitchString the decimal x = default; lines from #3913 are back to 0m, which is what master emits there today.

Full ICSharpCode.Decompiler.Tests suite passes (the Windows-only test projects were not run - this was developed on macOS).

Shortening default(T) is the same problem as removing the redundant cast
around a lambda whose delegate type the context already fixes, so it uses
the same mechanism: ConvertTo makes the explicit type implicit when the
conversion is an identity conversion and the caller allows an implicit
one. The literal keeps the type it was shortened from, so any later
conversion to a different type - or any context that requires an explicit
type, such as an overload resolution recheck falling back to CastArguments
- can spell default(T) out again. That keeps the value intact where the
bare literal would change it, e.g. "object o = default(SomeStruct)", which
boxes a non-null struct while "default" would be null.

Because the shortened literal resolves to DefaultLiteralResolveResult,
CallBuilder's existing overload resolution recheck sees a real default
literal and rejects ambiguous calls on its own; no separate bookkeeping
about which arguments may stay untyped is needed. Only the contexts that
supply no target type at all restore the explicit form: an awaited
expression, and arguments of operator methods, which later become operator
or cast syntax rather than calls.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
{
// The target type is supplied by the context, so "default(T)" can be
// shortened to the C# 7.1 default literal.
var shortened = new DefaultValueExpression();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Cloning the node is unnecessary, this could have been an in-place modification.

Reviewed with Stampeded!

{
value = value.ConvertTo(expectedType, this, allowImplicitConversion: true);
// The awaited expression is not target-typed: "await default" does not compile.
value = value.RestoreDefaultLiteralType(this);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a workaround for a bug we'll fix separately on another branch -- really await shouldn't be using allowImplicitConversion
.

Mutating the DefaultValueExpression is enough here; ConvertTo already hands
out mutated input nodes elsewhere (UnwrapChild), so building a replacement
node and copying the annotations over bought nothing.

The operator special case is easy to mistake for a cosmetic preference,
because the null literal is accepted in the same position: it converts only
to reference and nullable types, so it still narrows operator overload
resolution, whereas the default literal converts to everything and C#
rejects it outright for every binary operator except == and !=.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Making a conversion implicit by unwrapping it hands the operand to a
different target type, and a default literal takes its value from that
type: "S? x = new S?(default)" holds a value, while "S? x = default" is
null. Unwrapping the nullable constructor around a shortened literal
therefore turned "S? x = default(S)" into a null nullable. The literal is
spelled out again whenever unwrapping moves it to a type other than the one
it was shortened from.

Converting a using resource to the declared variable type is unconditional
now (except when the declaration says "var", which supplies no type): the
declaration always spells the type out, so any conversion to it may stay
implicit, which is also what shortens default(T) there.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
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