-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Check GetConstantValue and GetConversion on target-typed default #17205
Conversation
@AlekseyTs Small semantic model change for "default" literal. |
@@ -439,6 +439,7 @@ public object Value | |||
{ | |||
case ConstantValueTypeDiscriminator.Bad: return null; | |||
case ConstantValueTypeDiscriminator.Null: return null; | |||
case ConstantValueTypeDiscriminator.DefaultLiteral: return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
case ConstantValueTypeDiscriminator.DefaultLiteral: return null; [](start = 20, length = 64)
case ConstantValueTypeDiscriminator.DefaultLiteral: return null; [](start = 20, length = 64)
It feels unexpected that we ended up with a special discriminator for this feature. This is pretty much the same as Nothing literal in VB and we managed to support it without special discriminator. #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. The value should be what ConstantValue.Default
returns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm having some more trouble with constant values. The trouble is that expressions like default(int?)
and default(int*)
don't have constant values (see GetDefaultValue).
That means that default
in int? x = default;
shouldn't have a constant value. Does that seem right?
Also, I explored binding default
as a BoundConversion with a null literal conversion (instead of creating a new conversion kind) and a BoundLiteralExpression as operand. But I think that literals should have a constant value. Is that correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm having some more trouble with constant values.
I don't think of default
as a literal. It represents a default value of a type. Each type has a default value, but not all default values are constants from the language point of view. Let's talk offline in more details. #Closed
@@ -980,6 +980,11 @@ internal override Optional<object> GetConstantValueWorker(CSharpSyntaxNode node, | |||
|
|||
if (boundExpr == null) return default(Optional<object>); | |||
|
|||
if (boundExpr.Kind == BoundKind.DefaultLiteral) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (boundExpr.Kind == BoundKind.DefaultLiteral) [](start = 12, length = 47)
It is not clear why can't we make this decision based on the syntax.
var model = comp.GetSemanticModel(tree); | ||
var def = tree.GetCompilationUnitRoot().DescendantNodes().OfType<DefaultLiteralSyntax>().Single(); | ||
|
||
Assert.Equal(0, model.GetConstantValue(def).Value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
model.GetConstantValue(def).Value [](start = 28, length = 33)
model.GetConstantValue(def).Value [](start = 28, length = 33)
What type is returned by SemanticModel for the "def" node here? That type and the type of the constant value should agree. #Closed
@@ -325,6 +354,10 @@ static void Main() | |||
Assert.Null(model.GetTypeInfo(def).Type); | |||
Assert.Equal("System.Int32", model.GetTypeInfo(def).ConvertedType.ToTestDisplayString()); | |||
Assert.Null(model.GetSymbolInfo(def).Symbol); | |||
Assert.Equal(0, model.GetConstantValue(def).Value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assert.Equal(0, model.GetConstantValue(def).Value); [](start = 12, length = 51)
Assert.Equal(0, model.GetConstantValue(def).Value); [](start = 12, length = 51)
What type is returned by SemanticModel for the "def" node here? That type and the type of the constant value should agree. #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -427,6 +460,7 @@ static void Main() | |||
Assert.Null(model.GetTypeInfo(def).Type); | |||
Assert.Null(model.GetSymbolInfo(def).Symbol); | |||
Assert.Null(model.GetDeclaredSymbol(def)); | |||
Assert.Equal(0, model.GetConstantValue(def).Value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assert.Equal(0, model.GetConstantValue(def).Value); [](start = 12, length = 51)
Assert.Equal(0, model.GetConstantValue(def).Value); [](start = 12, length = 51)
What type is returned by SemanticModel for the "def" node here? That type and the type of the constant value should agree. #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think I fully agree with that. The type of the constant value should agree with the converted type of def
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For comparison, for null
, the Type
is null
, the ConvertedType
is the inferred type and the constant is null
(which agrees with ConvertedType
):
public void AssignNullToString()
{
string source = @"
class C
{
static void Main()
{
string x = null;
System.Console.Write(x);
}
}
";
var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.ExperimentalParseOptions);
comp.VerifyDiagnostics();
var tree = comp.SyntaxTrees.First();
var model = comp.GetSemanticModel(tree);
var def = tree.GetCompilationUnitRoot().DescendantNodes().OfType<LiteralExpressionSyntax>().Single();
Assert.Null(model.GetConstantValue(def).Value);
Assert.Equal("System.String", model.GetTypeInfo(def).ConvertedType.ToTestDisplayString());
Assert.Null(model.GetTypeInfo(def).Type);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The null
literal is special, it will only "match" reference types and will match any reference type. So, its constant value is null regardless of the target type. As in these scenarios, for example:
[Fact]
public void AssignNullToInt()
{
string source = @"
class C
{
static void Main()
{
int x = null;
System.Console.Write(x);
}
}
";
var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.ExperimentalParseOptions);
var tree = comp.SyntaxTrees.First();
var model = comp.GetSemanticModel(tree);
var def = tree.GetCompilationUnitRoot().DescendantNodes().OfType<LiteralExpressionSyntax>().Single();
Assert.Equal("null", def.ToString());
Assert.True(model.GetConstantValue(def).HasValue);
Assert.Null(model.GetConstantValue(def).Value);
Assert.Equal("System.Int32", model.GetTypeInfo(def).ConvertedType.ToTestDisplayString());
Assert.Null(model.GetTypeInfo(def).Type);
}
[Fact]
public void AssignNullToNullableInt()
{
string source = @"
class C
{
static void Main()
{
int? x = null;
System.Console.Write(x);
}
}
";
var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.ExperimentalParseOptions);
var tree = comp.SyntaxTrees.First();
var model = comp.GetSemanticModel(tree);
var def = tree.GetCompilationUnitRoot().DescendantNodes().OfType<LiteralExpressionSyntax>().Single();
Assert.Equal("null", def.ToString());
Assert.True(model.GetConstantValue(def).HasValue);
Assert.Null(model.GetConstantValue(def).Value);
Assert.Equal("System.Int32?", model.GetTypeInfo(def).ConvertedType.ToTestDisplayString());
Assert.Null(model.GetTypeInfo(def).Type);
}
Clearly null value doesn't match the ConvertedType in cases like this, its the value of the null literal regardless of the target type.
However, if we look at literals of value types:
[Fact]
public void AssignIntToLong()
{
string source = @"
class C
{
static void Main()
{
long x = 1;
System.Console.Write(x);
}
}
";
var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.ExperimentalParseOptions);
comp.VerifyDiagnostics();
var tree = comp.SyntaxTrees.First();
var model = comp.GetSemanticModel(tree);
var def = tree.GetCompilationUnitRoot().DescendantNodes().OfType<LiteralExpressionSyntax>().Single();
Assert.Equal(1, model.GetConstantValue(def).Value);
Assert.Equal(typeof(int), model.GetConstantValue(def).Value.GetType());
Assert.Equal("System.Int64", model.GetTypeInfo(def).ConvertedType.ToTestDisplayString());
Assert.Equal("System.Int32", model.GetTypeInfo(def).Type.ToTestDisplayString());
}
We can observe that the constant value matches the Type, not the ConvertedType.
I understand that default
is special in the way that it doesn't have natural type (similar to null
). At the same time it doesn't have a "natural" constant value that is going to be valid for any target type in an non-error scenario. In fact, for some target types there could be no valid constant value. Given that, it would feel right to me If the Type (not Converted type) and the ConstantValue of the node were both based on the target type. But I could be convinced that the Type should always bee null. I would still like to avoid any special cases in SemanticModel to get this behavior. #Closed
It is hard to say at the moment. It feels like it would be ideal to avoid adding special handling into SemanticModel because it is very likely that we will have to do that for different APIs so that they work in concert. #Closed |
This is analogous to the way
|
Could you please elaborate what source location or message this is related to, if any. |
This is related to the suggestion that |
a0d7113
to
ff13f93
Compare
9856c50
to
9ba6ed9
Compare
9ba6ed9
to
6f6d28d
Compare
@AlekseyTs @gafter I've re-purposed and renamed the "null literal" conversion as we discussed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, presuming this is the right approach to take.
@@ -643,7 +643,8 @@ private BoundExpression VisitConversion(BoundConversion node) | |||
var e1 = Convert(Visit(node.Operand), node.Operand.Type, intermediate, node.Checked, false); | |||
return Convert(e1, intermediate, node.Type, node.Checked, false); | |||
} | |||
case ConversionKind.NullLiteral: | |||
case ConversionKind.DefaultOrNullLiteral: | |||
// TODO REVIEW |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// PROTOTYPE(default): REVIEW
var nodes = tree.GetCompilationUnitRoot().DescendantNodes(); | ||
|
||
var def = nodes.OfType<DefaultLiteralSyntax>().Single(); | ||
Assert.Equal("System.Int32", model.GetTypeInfo(def).Type.ToTestDisplayString()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this similar to how we handle the null
literal?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not exactly. See discussion above.
When we create a null literal conversion, we create a BoundConversion which wraps the BoundLiteralExpression, but that literal node isn't changed. It will have ConstantValue.Null and no Type. The Type is stored in the conversion node, but not in the literal node.
For default
, when we create the conversion, we update the operand to have a ConstantValue (if one exists for that type) and a Type.
Here's what we get for null
:
public void Null()
{
string source = @"
class C
{
static void Main()
{
C x = null;
System.Console.Write(x);
}
}
";
var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.ExperimentalParseOptions, options: TestOptions.DebugExe);
comp.VerifyDiagnostics();
var tree = comp.SyntaxTrees.First();
var model = comp.GetSemanticModel(tree);
var nodes = tree.GetCompilationUnitRoot().DescendantNodes();
var def = nodes.OfType<LiteralExpressionSyntax>().Single();
Assert.Null(model.GetTypeInfo(def).Type);
Assert.Equal("C", model.GetTypeInfo(def).ConvertedType.ToTestDisplayString());
Assert.Null(model.GetSymbolInfo(def).Symbol);
Assert.Null(model.GetConstantValue(def).Value);
}
{ | ||
if (conversion.Kind == ConversionKind.DefaultOrNullLiteral && source.Kind == BoundKind.DefaultLiteral) | ||
{ | ||
return destination.GetDefaultValue(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GetDefaultValue [](start = 39, length = 15)
Is this method new to this branch? Could you provide a link to its implementation? #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the existing method used for making a ConstantValue for default()
operator.
Code is here http://source.roslyn.io/#Microsoft.CodeAnalysis.CSharp/Symbols/TypeSymbolExtensions.cs,264ab804a03fe275
@@ -216,7 +216,7 @@ internal BoundExpression ConvertPatternExpression(TypeSymbol inputType, CSharpSy | |||
case ConversionKind.Identity: | |||
case ConversionKind.ImplicitReference: | |||
case ConversionKind.Unboxing: | |||
case ConversionKind.NullLiteral: | |||
case ConversionKind.DefaultOrNullLiteral: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
case ConversionKind.DefaultOrNullLiteral: [](start = 20, length = 41)
Do we have a test that would go through here with default
? #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apparently this line affects no tests whatsoever. I'll see if I can hit it, or I'll file an issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method called only from BindDeclarationPattern
(and more specifically when there is a problem with the type in the declaration pattern), so I don't think this line is needed for either null
or default
.
@@ -2991,7 +2991,7 @@ private TypeSymbol StackMergeType(BoundExpression expr) | |||
var conversionKind = conversion.ConversionKind; | |||
if (conversionKind.IsImplicitConversion() && | |||
conversionKind != ConversionKind.MethodGroup && | |||
conversionKind != ConversionKind.NullLiteral) | |||
conversionKind != ConversionKind.DefaultOrNullLiteral) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
conversionKind != ConversionKind.DefaultOrNullLiteral) [](start = 24, length = 54)
Do we have a test that covers this path for default
? #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't manage to reach this with null
or default
either. The logic above in StackMergeType
exits early except for array, interface or delegate types. But the local rewriter strips the bound conversion on an array, interface and delegate types (because they have a constant value).
@@ -456,7 +456,7 @@ private static BoundExpression DemoteEnumOperand(BoundExpression operand) | |||
var conversion = (BoundConversion)operand; | |||
if (!conversion.ConversionKind.IsUserDefinedConversion() && | |||
conversion.ConversionKind.IsImplicitConversion() && | |||
conversion.ConversionKind != ConversionKind.NullLiteral && | |||
conversion.ConversionKind != ConversionKind.DefaultOrNullLiteral && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
conversion.ConversionKind != ConversionKind.DefaultOrNullLiteral && [](start = 20, length = 67)
Do we have a test that covers this code path with default
? #Resolved
Assert.Equal("System.Int32", model.GetTypeInfo(def).Type.ToTestDisplayString()); | ||
Assert.Equal("System.Int32", model.GetTypeInfo(def).ConvertedType.ToTestDisplayString()); | ||
Assert.Null(model.GetSymbolInfo(def).Symbol); | ||
Assert.Equal("0", model.GetConstantValue(def).Value.ToString()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assert.Equal("0", model.GetConstantValue(def).Value.ToString()); [](start = 12, length = 64)
It would be good to test conversion as well. #Closed
Assert.Equal("S", model.GetTypeInfo(def).Type.ToTestDisplayString()); | ||
Assert.Equal("S", model.GetTypeInfo(def).ConvertedType.ToTestDisplayString()); | ||
Assert.Null(model.GetSymbolInfo(def).Symbol); | ||
Assert.False(model.GetConstantValue(def).HasValue); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assert.False(model.GetConstantValue(def).HasValue); [](start = 3, length = 60)
It would be good to test conversion as well. #Closed
Assert.Equal("T", model.GetTypeInfo(def).Type.ToTestDisplayString()); | ||
Assert.Equal("T", model.GetTypeInfo(def).ConvertedType.ToTestDisplayString()); | ||
Assert.Null(model.GetSymbolInfo(def).Symbol); | ||
Assert.False(model.GetConstantValue(def).HasValue); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assert.False(model.GetConstantValue(def).HasValue); [](start = 12, length = 51)
It would be good to test conversion as well. #Closed
@@ -787,7 +859,7 @@ static void Main() | |||
var tree = comp.SyntaxTrees.First(); | |||
var model = comp.GetSemanticModel(tree); | |||
var def = tree.GetCompilationUnitRoot().DescendantNodes().OfType<DefaultLiteralSyntax>().Single(); | |||
Assert.Null(model.GetTypeInfo(def).Type); | |||
Assert.Equal("System.Int32?", model.GetTypeInfo(def).Type.ToTestDisplayString()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assert.Equal("System.Int32?", model.GetTypeInfo(def).Type.ToTestDisplayString()); [](start = 12, length = 81)
It would be good to test conversion and constant value as well. #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -988,7 +1064,7 @@ static void Main() | |||
var nodes = tree.GetCompilationUnitRoot().DescendantNodes(); | |||
|
|||
var def = nodes.OfType<DefaultLiteralSyntax>().Single(); | |||
Assert.Null(model.GetTypeInfo(def).Type); | |||
Assert.Equal("System.Int16", model.GetTypeInfo(def).Type.ToTestDisplayString()); | |||
Assert.Null(model.GetSymbolInfo(def).Symbol); | |||
Assert.Null(model.GetDeclaredSymbol(def)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assert.Null(model.GetDeclaredSymbol(def)); [](start = 12, length = 42)
It would be good to test conversion and constant value for this scenario because it is using explicit cast. #Closed
Assert.Equal("System.Int16", model.GetTypeInfo(def).ConvertedType.ToTestDisplayString()); | ||
Assert.Null(model.GetSymbolInfo(def).Symbol); | ||
Assert.Equal((short)0, model.GetConstantValue(def).Value); | ||
Assert.True(model.GetConversion(def).IsIdentity); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AlekseyTs This one surprised me. This behavior comes from GetTypeInfoForNode when type and converted type match.
2ec1199
to
7bbaf29
Compare
Assert.Equal("System.Int16", model.GetTypeInfo(def).ConvertedType.ToTestDisplayString()); | ||
Assert.Null(model.GetSymbolInfo(def).Symbol); | ||
Assert.Equal((short)0, model.GetConstantValue(def).Value); | ||
Assert.True(model.GetConversion(def).IsIdentity); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assert.True(model.GetConversion(def).IsIdentity); [](start = 12, length = 49)
Is this consistent with behavior when explicit cast is applied to a null
literal? #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in terms of conversions, the behavior is similar with null
(there is an identity conversion on the literal).
The main difference with null
as we already discussed is that we give default
a type.
[Fact]
public void ExplicitCastOnNull()
{
string source = @"
class Base { }
class Derived : Base
{
static void Main()
{
Base x = (Derived)null;
System.Console.Write(x);
}
}
";
var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.ExperimentalParseOptions, options: TestOptions.DebugExe);
comp.VerifyDiagnostics();
var tree = comp.SyntaxTrees.First();
var model = comp.GetSemanticModel(tree);
var nodes = tree.GetCompilationUnitRoot().DescendantNodes();
var def = nodes.OfType<LiteralExpressionSyntax>().Single();
Assert.Equal(null, model.GetTypeInfo(def).Type);
Assert.Null(model.GetSymbolInfo(def).Symbol);
Assert.Null(model.GetDeclaredSymbol(def));
Assert.Equal(null, model.GetTypeInfo(def).ConvertedType);
Assert.Null(model.GetSymbolInfo(def).Symbol);
Assert.True(model.GetConstantValue(def).HasValue);
Assert.Equal(null, model.GetConstantValue(def).Value);
Assert.True(model.GetConversion(def).IsIdentity);
var conversionSyntax = nodes.OfType<CastExpressionSyntax>().Single();
var conversionTypeInfo = model.GetTypeInfo(conversionSyntax);
Assert.Equal("Derived", conversionTypeInfo.Type.ToTestDisplayString());
Assert.Equal("Base", conversionTypeInfo.ConvertedType.ToTestDisplayString());
Assert.True(model.GetConstantValue(conversionSyntax).HasValue);
Assert.Equal(null, model.GetConstantValue(conversionSyntax).Value);
Conversion conversion = model.GetConversion(conversionSyntax);
Assert.False(conversion.IsNumeric);
Assert.True(conversion.IsImplicit);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for confirming this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
@gafter Could you take another look and approve the change if looks good? |
@gafter Still good to go? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
…net#17205) Conflicts: src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionKind.cs src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionKindExtensions.cs src/Compilers/CSharp/Portable/CodeGen/EmitExpression.cs
I noticed two more semantic model APIs that need coverage.
GetConstantValue
should behave as it does ondefault(...)
.In the current implementation, when the type of the
default
literal is inferred, the bound node remains the same (typeless and without a constant value) and it is wrapped with a bound conversion. I'd like to get your feedback on that.Relates to umbrella item for "default" (#13602)
@dotnet/roslyn-compiler for review.