diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs index 60cc8c7973e0..c5cea61f8d02 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs @@ -487,6 +487,14 @@ private BoundExpression BindSimpleBinaryOperator(BinaryExpressionSyntax node, Di return new BoundLiteral(node, ConstantValue.Create(kind == BinaryOperatorKind.Equal), GetSpecialType(SpecialType.System_Boolean, diagnostics, node)); } + if (GetTupleCardinality(left) > 1 && + GetTupleCardinality(right) > 1 && + (kind == BinaryOperatorKind.Equal || kind == BinaryOperatorKind.NotEqual)) + { + CheckFeatureAvailability(node, MessageID.IDS_FeatureTupleEquality, diagnostics); + return BindTupleBinaryOperator(node, kind, left, right, diagnostics); + } + // SPEC: For an operation of one of the forms x == null, null == x, x != null, null != x, // SPEC: where x is an expression of nullable type, if operator overload resolution // SPEC: fails to find an applicable operator, the result is instead computed from @@ -504,64 +512,18 @@ private BoundExpression BindSimpleBinaryOperator(BinaryExpressionSyntax node, Di LookupResultKind resultKind; ImmutableArray originalUserDefinedOperators; - var best = this.BinaryOperatorOverloadResolution(kind, left, right, node, diagnostics, out resultKind, out originalUserDefinedOperators); - - // However, as an implementation detail, we never "fail to find an applicable - // operator" during overload resolution if we have x == null, etc. We always - // find at least the reference conversion object == object; the overload resolution - // code does not reject that. Therefore what we should do is only bind - // "x == null" as a nullable-to-null comparison if overload resolution chooses - // the reference conversion. - - BoundExpression resultLeft = left; - BoundExpression resultRight = right; - MethodSymbol resultMethod = null; - ConstantValue resultConstant = null; - BinaryOperatorKind resultOperatorKind; - TypeSymbol resultType; - bool hasErrors; - - if (!best.HasValue) - { - resultOperatorKind = kind; - resultType = CreateErrorType(); - hasErrors = true; - } - else - { - var signature = best.Signature; - - bool isObjectEquality = signature.Kind == BinaryOperatorKind.ObjectEqual || signature.Kind == BinaryOperatorKind.ObjectNotEqual; - - bool isNullableEquality = (object)signature.Method == null && - (signature.Kind.Operator() == BinaryOperatorKind.Equal || signature.Kind.Operator() == BinaryOperatorKind.NotEqual) && - (leftNull && (object)rightType != null && rightType.IsNullableType() || - rightNull && (object)leftType != null && leftType.IsNullableType()); - - if (isNullableEquality) - { - resultOperatorKind = kind | BinaryOperatorKind.NullableNull; - resultType = GetSpecialType(SpecialType.System_Boolean, diagnostics, node); - hasErrors = false; - } - else - { - resultOperatorKind = signature.Kind; - resultType = signature.ReturnType; - resultMethod = signature.Method; - resultLeft = CreateConversion(left, best.LeftConversion, signature.LeftType, diagnostics); - resultRight = CreateConversion(right, best.RightConversion, signature.RightType, diagnostics); - resultConstant = FoldBinaryOperator(node, resultOperatorKind, resultLeft, resultRight, resultType.SpecialType, diagnostics, ref compoundStringLength); - HashSet useSiteDiagnostics = null; - hasErrors = isObjectEquality && !BuiltInOperators.IsValidObjectEquality(Conversions, leftType, leftNull, rightType, rightNull, ref useSiteDiagnostics); - diagnostics.Add(node, useSiteDiagnostics); - } - } + BinaryOperatorSignature signature; + BinaryOperatorAnalysisResult best; + bool foundOperator = BindSimpleBinaryOperatorParts(node, diagnostics, left, right, kind, + out resultKind, out originalUserDefinedOperators, out signature, out best); - if (hasErrors) + BinaryOperatorKind resultOperatorKind = signature.Kind; + bool hasErrors = false; + if (!foundOperator) { ReportBinaryOperatorError(node, diagnostics, node.OperatorToken, left, right, resultKind); resultOperatorKind &= ~BinaryOperatorKind.TypeMask; + hasErrors = true; } switch (node.Kind()) @@ -583,6 +545,21 @@ private BoundExpression BindSimpleBinaryOperator(BinaryExpressionSyntax node, Di break; } + TypeSymbol resultType = signature.ReturnType; + BoundExpression resultLeft = left; + BoundExpression resultRight = right; + ConstantValue resultConstant = null; + + if (foundOperator && (resultOperatorKind.OperandTypes() != BinaryOperatorKind.NullableNull)) + { + Debug.Assert((object)signature.LeftType != null); + Debug.Assert((object)signature.RightType != null); + + resultLeft = CreateConversion(left, best.LeftConversion, signature.LeftType, diagnostics); + resultRight = CreateConversion(right, best.RightConversion, signature.RightType, diagnostics); + resultConstant = FoldBinaryOperator(node, resultOperatorKind, resultLeft, resultRight, resultType.SpecialType, diagnostics, ref compoundStringLength); + } + hasErrors = hasErrors || resultConstant != null && resultConstant.IsBad; return new BoundBinaryOperator( @@ -591,13 +568,67 @@ private BoundExpression BindSimpleBinaryOperator(BinaryExpressionSyntax node, Di resultLeft, resultRight, resultConstant, - resultMethod, + signature.Method, resultKind, originalUserDefinedOperators, resultType, hasErrors); } + private bool BindSimpleBinaryOperatorParts(BinaryExpressionSyntax node, DiagnosticBag diagnostics, BoundExpression left, BoundExpression right, BinaryOperatorKind kind, + out LookupResultKind resultKind, out ImmutableArray originalUserDefinedOperators, + out BinaryOperatorSignature resultSignature, out BinaryOperatorAnalysisResult best) + { + bool foundOperator; + best = this.BinaryOperatorOverloadResolution(kind, left, right, node, diagnostics, out resultKind, out originalUserDefinedOperators); + + // However, as an implementation detail, we never "fail to find an applicable + // operator" during overload resolution if we have x == null, etc. We always + // find at least the reference conversion object == object; the overload resolution + // code does not reject that. Therefore what we should do is only bind + // "x == null" as a nullable-to-null comparison if overload resolution chooses + // the reference conversion. + + if (!best.HasValue) + { + resultSignature = new BinaryOperatorSignature(kind, leftType: null, rightType: null, CreateErrorType()); + foundOperator = false; + } + else + { + var signature = best.Signature; + + bool isObjectEquality = signature.Kind == BinaryOperatorKind.ObjectEqual || signature.Kind == BinaryOperatorKind.ObjectNotEqual; + + bool leftNull = left.IsLiteralNull(); + bool rightNull = right.IsLiteralNull(); + + TypeSymbol leftType = left.Type; + TypeSymbol rightType = right.Type; + + bool isNullableEquality = (object)signature.Method == null && + (signature.Kind.Operator() == BinaryOperatorKind.Equal || signature.Kind.Operator() == BinaryOperatorKind.NotEqual) && + (leftNull && (object)rightType != null && rightType.IsNullableType() || + rightNull && (object)leftType != null && leftType.IsNullableType()); + + if (isNullableEquality) + { + resultSignature = new BinaryOperatorSignature(kind | BinaryOperatorKind.NullableNull, leftType: null, rightType: null, + GetSpecialType(SpecialType.System_Boolean, diagnostics, node)); + + foundOperator = true; + } + else + { + resultSignature = signature; + HashSet useSiteDiagnostics = null; + foundOperator = !isObjectEquality || BuiltInOperators.IsValidObjectEquality(Conversions, leftType, leftNull, rightType, rightNull, ref useSiteDiagnostics); + diagnostics.Add(node, useSiteDiagnostics); + } + } + return foundOperator; + } + private static void ReportUnaryOperatorError(CSharpSyntaxNode node, DiagnosticBag diagnostics, string operatorName, BoundExpression operand, LookupResultKind resultKind) { ErrorCode errorCode = resultKind == LookupResultKind.Ambiguous ? diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_TupleOperators.cs b/src/Compilers/CSharp/Portable/Binder/Binder_TupleOperators.cs new file mode 100644 index 000000000000..a4d1481408f7 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Binder/Binder_TupleOperators.cs @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using Microsoft.CodeAnalysis.CSharp.Symbols; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.PooledObjects; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp +{ + internal partial class Binder + { + /// + /// If the left and right are tuples of matching cardinality, we'll try to bind the operator element-wise. + /// When that succeeds, the element-wise conversions are collected. We keep them for semantic model. + /// The element-wise binary operators are collected and stored as a tree for lowering. + /// + private BoundTupleBinaryOperator BindTupleBinaryOperator(BinaryExpressionSyntax node, BinaryOperatorKind kind, + BoundExpression left, BoundExpression right, DiagnosticBag diagnostics) + { + // PROTOTYPE(tuple-equality) Block in expression tree + + TupleBinaryOperatorInfo.Multiple operators = BindTupleBinaryOperatorNestedInfo(node, kind, left, right, diagnostics); + + // PROTOTYPE(tuple-equality) We'll save the converted nodes separately, for the semantic model + //BoundExpression convertedLeft = GenerateConversionForAssignment(operators.LeftConvertedType, left, diagnostics); + //BoundExpression convertedRight = GenerateConversionForAssignment(operators.RightConvertedType, right, diagnostics); + + TypeSymbol resultType = GetSpecialType(SpecialType.System_Boolean, diagnostics, node); + return new BoundTupleBinaryOperator(node, left, right, kind, operators, resultType); + } + + /// + /// Binds: + /// 1. dynamically, if either side is dynamic + /// 2. as tuple binary operator, if both sides are tuples of matching cardinalities + /// 3. as regular binary operator otherwise + /// + private TupleBinaryOperatorInfo BindTupleBinaryOperatorInfo(BinaryExpressionSyntax node, BinaryOperatorKind kind, + BoundExpression left, BoundExpression right, DiagnosticBag diagnostics) + { + TypeSymbol leftType = left.Type; + TypeSymbol rightType = right.Type; + + if ((object)leftType != null && leftType.IsDynamic() || (object)rightType != null && rightType.IsDynamic()) + { + return BindTupleDynamicBinaryOperatorSingleInfo(node, kind, left, right, diagnostics); + } + + if (GetTupleCardinality(left) > 1 && GetTupleCardinality(right) > 1) + { + return BindTupleBinaryOperatorNestedInfo(node, kind, left, right, diagnostics); + } + + LookupResultKind resultKind; + BinaryOperatorSignature signature; + BinaryOperatorAnalysisResult analysisResult; + + bool foundOperator = BindSimpleBinaryOperatorParts(node, diagnostics, left, right, kind, + out resultKind, originalUserDefinedOperators: out _, out signature, out analysisResult); + + if (!foundOperator) + { + ReportBinaryOperatorError(node, diagnostics, node.OperatorToken, left, right, resultKind); + } + + PrepareBoolConversionAndTruthOperator(signature.ReturnType, node, kind, diagnostics, out Conversion conversionIntoBoolOperator, out UnaryOperatorSignature boolOperator); + + return new TupleBinaryOperatorInfo.Single(signature.LeftType, signature.RightType, signature.Kind, + analysisResult.LeftConversion, analysisResult.RightConversion, signature.Method, conversionIntoBoolOperator, boolOperator); + } + + /// + /// If an element-wise binary operator returns a non-bool type, we will either: + /// - prepare a conversion to bool if one exists + /// - prepare a truth operator: op_false in the case of an equality (`a == b` will be lowered to `!((a == b).op_false)) or op_true in the case of inequality, + /// with the conversion being used for its input. + /// + private void PrepareBoolConversionAndTruthOperator(TypeSymbol type, BinaryExpressionSyntax node, BinaryOperatorKind binaryOperator, DiagnosticBag diagnostics, + out Conversion conversionForBool, out UnaryOperatorSignature boolOperator) + { + // Is the operand implicitly convertible to bool? + + HashSet useSiteDiagnostics = null; + TypeSymbol boolean = GetSpecialType(SpecialType.System_Boolean, diagnostics, node); + Conversion conversion = this.Conversions.ClassifyImplicitConversionFromType(type, boolean, ref useSiteDiagnostics); + diagnostics.Add(node, useSiteDiagnostics); + + if (conversion.IsImplicit) + { + conversionForBool = conversion; + boolOperator = default; + return; + } + + // It was not. Does it implement operator true (or false)? + + UnaryOperatorKind boolOpKind; + switch (binaryOperator) + { + case BinaryOperatorKind.Equal: + boolOpKind = UnaryOperatorKind.False; + break; + case BinaryOperatorKind.NotEqual: + boolOpKind = UnaryOperatorKind.True; + break; + default: + throw ExceptionUtilities.UnexpectedValue(binaryOperator); + } + + LookupResultKind resultKind; + ImmutableArray originalUserDefinedOperators; + BoundExpression comparisonResult = new BoundTupleOperandPlaceholder(node, type); + UnaryOperatorAnalysisResult best = this.UnaryOperatorOverloadResolution(boolOpKind, comparisonResult, node, diagnostics, out resultKind, out originalUserDefinedOperators); + if (best.HasValue) + { + conversionForBool = best.Conversion; + boolOperator = best.Signature; + return; + } + + // It did not. Give a "not convertible to bool" error. + + GenerateImplicitConversionError(diagnostics, node, conversion, comparisonResult, boolean); + conversionForBool = Conversion.NoConversion; + boolOperator = default; + return; + } + + private TupleBinaryOperatorInfo BindTupleDynamicBinaryOperatorSingleInfo(BinaryExpressionSyntax node, BinaryOperatorKind kind, + BoundExpression left, BoundExpression right, DiagnosticBag diagnostics) + { + // This method binds binary == and != operators where one or both of the operands are dynamic. + Debug.Assert((object)left.Type != null && left.Type.IsDynamic() || (object)right.Type != null && right.Type.IsDynamic()); + + bool hasError = false; + if (!IsLegalDynamicOperand(left) || !IsLegalDynamicOperand(right)) + { + // Operator '{0}' cannot be applied to operands of type '{1}' and '{2}' + Error(diagnostics, ErrorCode.ERR_BadBinaryOps, node, node.OperatorToken.Text, left.Display, right.Display); + hasError = true; + } + + BinaryOperatorKind elementOperatorKind = hasError ? kind : kind.WithType(BinaryOperatorKind.Dynamic); + TypeSymbol dynamicType = Compilation.DynamicType; + + // We'll want to dynamically invoke operators op_true (/op_false) for equality (/inequality) comparison, but we don't need + // to prepare either a conversion or a truth operator. Those can just be synthesized during lowering. + return new TupleBinaryOperatorInfo.Single(dynamicType, dynamicType, elementOperatorKind, + leftConversion: Conversion.NoConversion, rightConversion: Conversion.NoConversion, + methodSymbolOpt: null, conversionForBool: Conversion.Identity, boolOperator: default); + } + + private TupleBinaryOperatorInfo.Multiple BindTupleBinaryOperatorNestedInfo(BinaryExpressionSyntax node, BinaryOperatorKind kind, + BoundExpression left, BoundExpression right, DiagnosticBag diagnostics) + { + TypeSymbol leftType = left.Type; + TypeSymbol rightType = right.Type; + + int leftCardinality = GetTupleCardinality(left); + int rightCardinality = GetTupleCardinality(right); + + if (leftCardinality != rightCardinality) + { + Error(diagnostics, ErrorCode.ERR_TupleSizesMismatchForBinOps, node, leftCardinality, rightCardinality); + + return new TupleBinaryOperatorInfo.Multiple(ImmutableArray.Empty, leftType ?? CreateErrorType(), rightType ?? CreateErrorType()); + } + + // typeless tuple literals are not nullable + bool leftNullable = leftType?.IsNullableType() == true; + bool rightNullable = rightType?.IsNullableType() == true; + + ImmutableArray leftParts = GetTupleArgumentsOrPlaceholders(left); + ImmutableArray rightParts = GetTupleArgumentsOrPlaceholders(right); + + int length = leftParts.Length; + Debug.Assert(length == rightParts.Length); + + var operatorsBuilder = ArrayBuilder.GetInstance(length); + + for (int i = 0; i < length; i++) + { + operatorsBuilder.Add(BindTupleBinaryOperatorInfo(node, kind, leftParts[i], rightParts[i], diagnostics)); + } + + var compilation = this.Compilation; + var operators = operatorsBuilder.ToImmutableAndFree(); + bool isNullable = leftNullable || rightNullable; + TypeSymbol leftTupleType = MakeConvertedType(operators.SelectAsArray(o => o.LeftConvertedTypeOpt), node.Left, leftParts, isNullable, compilation, diagnostics); + TypeSymbol rightTupleType = MakeConvertedType(operators.SelectAsArray(o => o.RightConvertedTypeOpt), node.Right, rightParts, isNullable, compilation, diagnostics); + + return new TupleBinaryOperatorInfo.Multiple(operators, leftTupleType, rightTupleType); + } + + private static int GetTupleCardinality(BoundExpression expr) + { + if (expr.Kind == BoundKind.TupleLiteral) + { + var tuple = (BoundTupleLiteral)expr; + return tuple.Arguments.Length; + } + + TypeSymbol type = expr.Type; + if (type is null) + { + return -1; + } + + type = type.StrippedType(); + + if (type.IsTupleType) + { + return type.TupleElementTypes.Length; + } + + return -1; + } + + private static ImmutableArray GetTupleArgumentsOrPlaceholders(BoundExpression expr) + { + if (expr.Kind == BoundKind.TupleLiteral) + { + return ((BoundTupleLiteral)expr).Arguments; + } + + // placeholder bound nodes with the proper types are sufficient to bind the element-wise binary operators + return expr.Type.StrippedType().TupleElementTypes + .SelectAsArray((t, s) => (BoundExpression)new BoundTupleOperandPlaceholder(s, t), expr.Syntax); + } + + /// + /// Make a tuple type (with appropriate nesting) from the types (on the left or on the right) collected + /// from binding element-wise binary operators. + /// If any of the elements is typeless, then the tuple is typeless too. + /// + private TypeSymbol MakeConvertedType(ImmutableArray convertedTypes, CSharpSyntaxNode syntax, + ImmutableArray elements, bool isNullable, CSharpCompilation compilation, DiagnosticBag diagnostics) + { + foreach (var convertedType in convertedTypes) + { + if (convertedType is null) + { + return null; + } + } + + ImmutableArray elementLocations = elements.SelectAsArray(e => e.Syntax.Location); + + // PROTOTYPE(tuple-equality) Add test for violated tuple constraint + var tuple = TupleTypeSymbol.Create(locationOpt: null, elementTypes: convertedTypes, + elementLocations, elementNames: default, compilation, + shouldCheckConstraints: true, errorPositions: default, syntax, diagnostics); + + if (!isNullable) + { + return tuple; + } + + // PROTOTYPE(tuple-equality) check constraints + NamedTypeSymbol nullableT = GetSpecialType(SpecialType.System_Nullable_T, diagnostics, syntax); + return nullableT.Construct(tuple); + } + } +} diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml b/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml index 560e88a72f77..82fb2575177c 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml @@ -90,6 +90,15 @@ + + + + + @@ -302,6 +311,16 @@ + + + + + + + + + + diff --git a/src/Compilers/CSharp/Portable/BoundTree/TupleBinaryOperatorInfo.cs b/src/Compilers/CSharp/Portable/BoundTree/TupleBinaryOperatorInfo.cs new file mode 100644 index 000000000000..4111ad11c72b --- /dev/null +++ b/src/Compilers/CSharp/Portable/BoundTree/TupleBinaryOperatorInfo.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using Microsoft.CodeAnalysis.CSharp.Symbols; + +namespace Microsoft.CodeAnalysis.CSharp +{ + /// + /// A tree of binary operators for tuple comparisons. + /// + /// For `(a, (b, c)) == (d, (e, f))` we'll hold a Multiple with two elements. + /// The first element is a Single (describing the binary operator and conversions that are involved in `a == d`). + /// The second element is a Multiple containing two Singles (one for the `b == e` comparison and the other for `c == f`). + /// + internal abstract class TupleBinaryOperatorInfo + { + internal abstract bool IsSingle(); + internal readonly TypeSymbol LeftConvertedTypeOpt; + internal readonly TypeSymbol RightConvertedTypeOpt; +#if DEBUG + internal abstract TreeDumperNode DumpCore(); + internal string Dump() => TreeDumper.DumpCompact(DumpCore()); +#endif + + private TupleBinaryOperatorInfo(TypeSymbol leftConvertedTypeOpt, TypeSymbol rightConvertedTypeOpt) + { + LeftConvertedTypeOpt = leftConvertedTypeOpt; + RightConvertedTypeOpt = rightConvertedTypeOpt; + } + + /// + /// Holds the information for an element-wise comparison (like `a == b`) + /// + internal class Single : TupleBinaryOperatorInfo + { + internal readonly BinaryOperatorKind Kind; + internal readonly Conversion LeftConversion; + internal readonly Conversion RightConversion; + internal readonly MethodSymbol MethodSymbolOpt; // User-defined comparison operator, if applicable + + internal readonly Conversion ConversionForBool; // If a conversion to bool exists, then no operator needed. If an operator is needed, this holds the conversion for input to that operator. + internal readonly UnaryOperatorSignature BoolOperator; // Information for op_true or op_false + + internal Single(TypeSymbol leftConvertedTypeOpt, TypeSymbol rightConvertedTypeOpt, BinaryOperatorKind kind, + Conversion leftConversion, Conversion rightConversion, MethodSymbol methodSymbolOpt, + Conversion conversionForBool, UnaryOperatorSignature boolOperator) : base(leftConvertedTypeOpt, rightConvertedTypeOpt) + { + Kind = kind; + LeftConversion = leftConversion; + RightConversion = rightConversion; + MethodSymbolOpt = methodSymbolOpt; + ConversionForBool = conversionForBool; + BoolOperator = boolOperator; + + Debug.Assert(Kind.IsUserDefined() == ((object)MethodSymbolOpt != null)); + } + + internal override bool IsSingle() + => true; + + public override string ToString() + => $"binaryOperatorKind: {Kind}"; + +#if DEBUG + internal override TreeDumperNode DumpCore() + { + var sub = new List(); + if ((object)MethodSymbolOpt != null) + { + sub.Add(new TreeDumperNode("methodSymbolOpt", MethodSymbolOpt.ToDisplayString(), null)); + } + sub.Add(new TreeDumperNode("leftConversion", LeftConvertedTypeOpt.ToDisplayString(), null)); + sub.Add(new TreeDumperNode("rightConversion", RightConvertedTypeOpt.ToDisplayString(), null)); + + return new TreeDumperNode("nested", Kind, sub); + } +#endif + } + + /// + /// Holds the information for a tuple comparison, either at the top-level (like `(a, b) == ...`) or nested (like `(..., (a, b)) == (..., ...)`). + /// + internal class Multiple : TupleBinaryOperatorInfo + { + internal readonly ImmutableArray Operators; + + internal Multiple(ImmutableArray operators, TypeSymbol leftConvertedTypeOpt, TypeSymbol rightConvertedTypeOpt) + : base(leftConvertedTypeOpt, rightConvertedTypeOpt) + { + Debug.Assert(leftConvertedTypeOpt is null || leftConvertedTypeOpt.StrippedType().IsTupleType); + Debug.Assert(rightConvertedTypeOpt is null || rightConvertedTypeOpt.StrippedType().IsTupleType); + Debug.Assert(!operators.IsDefault); + Debug.Assert(operators.IsEmpty || operators.Length > 1); // an empty array is used for error cases, otherwise tuples must have cardinality > 1 + + Operators = operators; + } + + internal override bool IsSingle() + => false; + +#if DEBUG + internal override TreeDumperNode DumpCore() + { + var sub = new List(); + sub.Add(new TreeDumperNode($"nestedOperators[{Operators.Length}]", null, + Operators.SelectAsArray(c => c.DumpCore()))); + + return new TreeDumperNode("nested", null, sub); + } +#endif + } + } +} diff --git a/src/Compilers/CSharp/Portable/CSharpResources.Designer.cs b/src/Compilers/CSharp/Portable/CSharpResources.Designer.cs index 0001b08c9def..5285b86317ac 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.Designer.cs +++ b/src/Compilers/CSharp/Portable/CSharpResources.Designer.cs @@ -5588,7 +5588,7 @@ internal static string ERR_InterfaceImplementedByConditional { } /// - /// Looks up a localized string similar to '{0}' cannot implement interface member '{1}' in type '{2}' because it has an __arglist parameter.. + /// Looks up a localized string similar to '{0}' cannot implement interface member '{1}' in type '{2}' because it has an __arglist parameter. /// internal static string ERR_InterfaceImplementedImplicitlyByVariadic { get { @@ -9439,6 +9439,15 @@ internal static string ERR_TupleReservedElementNameAnyPosition { } } + /// + /// Looks up a localized string similar to Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right.. + /// + internal static string ERR_TupleSizesMismatchForBinOps { + get { + return ResourceManager.GetString("ERR_TupleSizesMismatchForBinOps", resourceCulture); + } + } + /// /// Looks up a localized string similar to Tuple must contain at least two elements.. /// @@ -10781,6 +10790,15 @@ internal static string IDS_FeatureThrowExpression { } } + /// + /// Looks up a localized string similar to tuple equality. + /// + internal static string IDS_FeatureTupleEquality { + get { + return ResourceManager.GetString("IDS_FeatureTupleEquality", resourceCulture); + } + } + /// /// Looks up a localized string similar to tuples. /// diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index e76f2e8b7de6..3b0abf961c10 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -216,6 +216,9 @@ private protected + + tuple equality + nullable types @@ -5256,4 +5259,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ Arguments with 'in' modifier cannot be used in dynamically dispatched expessions. + + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs index 096c6b6ae18d..87045949c5ac 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -1554,10 +1554,14 @@ internal enum ErrorCode ERR_DefaultInPattern = 8363, ERR_InDynamicMethodArg = 8364, + #region diagnostics introduced for C# 7.3 ERR_FeatureNotAvailableInVersion7_3 = 8370, WRN_AttributesOnBackingFieldsNotAvailable = 8371, ERR_DoNotUseFixedBufferAttrOnProperty = 8372, + ERR_TupleSizesMismatchForBinOps = 8373, + #endregion diagnostics introduced for C# 7.3 + // Note: you will need to re-generate compiler code after adding warnings (build\scripts\generate-compiler-code.cmd) } } diff --git a/src/Compilers/CSharp/Portable/Errors/MessageID.cs b/src/Compilers/CSharp/Portable/Errors/MessageID.cs index 3cc8de8a2d21..c21f746459aa 100644 --- a/src/Compilers/CSharp/Portable/Errors/MessageID.cs +++ b/src/Compilers/CSharp/Portable/Errors/MessageID.cs @@ -148,6 +148,7 @@ internal enum MessageID IDS_FeatureRefConditional = MessageBase + 12731, IDS_FeatureAttributesOnBackingFields = MessageBase + 12732, + IDS_FeatureTupleEquality = MessageBase + 12733, } // Message IDs may refer to strings that need to be localized. @@ -189,6 +190,7 @@ internal static LanguageVersion RequiredVersion(this MessageID feature) { // C# 7.3 features. case MessageID.IDS_FeatureAttributesOnBackingFields: // semantic check + case MessageID.IDS_FeatureTupleEquality: // semantic check return LanguageVersion.CSharp7_3; // C# 7.2 features. diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/PreciseAbstractFlowPass.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/PreciseAbstractFlowPass.cs index e6f14fe27664..63b35556179c 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/PreciseAbstractFlowPass.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/PreciseAbstractFlowPass.cs @@ -1025,6 +1025,13 @@ private BoundNode VisitTupleExpression(BoundTupleExpression node) return null; } + public override BoundNode VisitTupleBinaryOperator(BoundTupleBinaryOperator node) + { + Visit(node.Left); + Visit(node.Right); + return null; + } + public override BoundNode VisitDynamicObjectCreationExpression(BoundDynamicObjectCreationExpression node) { VisitArguments(node.Arguments, node.ArgumentRefKindsOpt, null); diff --git a/src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs b/src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs index e3dad0c56ab4..80f5ebb542b8 100644 --- a/src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs +++ b/src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs @@ -26,6 +26,7 @@ internal enum BoundKind: byte ParameterEqualsValue, GlobalStatementInitializer, DeconstructValuePlaceholder, + TupleOperandPlaceholder, Dup, BadExpression, BadStatement, @@ -41,6 +42,7 @@ internal enum BoundKind: byte MakeRefOperator, RefValueOperator, BinaryOperator, + TupleBinaryOperator, UserDefinedConditionalLogicalOperator, CompoundAssignmentOperator, AssignmentOperator, @@ -450,6 +452,42 @@ public BoundDeconstructValuePlaceholder Update(uint valEscape, TypeSymbol type) } } + internal sealed partial class BoundTupleOperandPlaceholder : BoundValuePlaceholderBase + { + public BoundTupleOperandPlaceholder(SyntaxNode syntax, TypeSymbol type, bool hasErrors) + : base(BoundKind.TupleOperandPlaceholder, syntax, type, hasErrors) + { + + Debug.Assert(type != null, "Field 'type' cannot be null (use Null=\"allow\" in BoundNodes.xml to remove this check)"); + + } + + public BoundTupleOperandPlaceholder(SyntaxNode syntax, TypeSymbol type) + : base(BoundKind.TupleOperandPlaceholder, syntax, type) + { + + Debug.Assert(type != null, "Field 'type' cannot be null (use Null=\"allow\" in BoundNodes.xml to remove this check)"); + + } + + + public override BoundNode Accept(BoundTreeVisitor visitor) + { + return visitor.VisitTupleOperandPlaceholder(this); + } + + public BoundTupleOperandPlaceholder Update(TypeSymbol type) + { + if (type != this.Type) + { + var result = new BoundTupleOperandPlaceholder(this.Syntax, type, this.HasErrors); + result.WasCompilerGenerated = this.WasCompilerGenerated; + return result; + } + return this; + } + } + internal sealed partial class BoundDup : BoundExpression { public BoundDup(SyntaxNode syntax, RefKind refKind, TypeSymbol type, bool hasErrors) @@ -1019,6 +1057,49 @@ public BoundBinaryOperator Update(BinaryOperatorKind operatorKind, BoundExpressi } } + internal sealed partial class BoundTupleBinaryOperator : BoundExpression + { + public BoundTupleBinaryOperator(SyntaxNode syntax, BoundExpression left, BoundExpression right, BinaryOperatorKind operatorKind, TupleBinaryOperatorInfo.Multiple operators, TypeSymbol type, bool hasErrors = false) + : base(BoundKind.TupleBinaryOperator, syntax, type, hasErrors || left.HasErrors() || right.HasErrors()) + { + + Debug.Assert(left != null, "Field 'left' cannot be null (use Null=\"allow\" in BoundNodes.xml to remove this check)"); + Debug.Assert(right != null, "Field 'right' cannot be null (use Null=\"allow\" in BoundNodes.xml to remove this check)"); + Debug.Assert(operators != null, "Field 'operators' cannot be null (use Null=\"allow\" in BoundNodes.xml to remove this check)"); + Debug.Assert(type != null, "Field 'type' cannot be null (use Null=\"allow\" in BoundNodes.xml to remove this check)"); + + this.Left = left; + this.Right = right; + this.OperatorKind = operatorKind; + this.Operators = operators; + } + + + public BoundExpression Left { get; } + + public BoundExpression Right { get; } + + public BinaryOperatorKind OperatorKind { get; } + + public TupleBinaryOperatorInfo.Multiple Operators { get; } + + public override BoundNode Accept(BoundTreeVisitor visitor) + { + return visitor.VisitTupleBinaryOperator(this); + } + + public BoundTupleBinaryOperator Update(BoundExpression left, BoundExpression right, BinaryOperatorKind operatorKind, TupleBinaryOperatorInfo.Multiple operators, TypeSymbol type) + { + if (left != this.Left || right != this.Right || operatorKind != this.OperatorKind || operators != this.Operators || type != this.Type) + { + var result = new BoundTupleBinaryOperator(this.Syntax, left, right, operatorKind, operators, type, this.HasErrors); + result.WasCompilerGenerated = this.WasCompilerGenerated; + return result; + } + return this; + } + } + internal sealed partial class BoundUserDefinedConditionalLogicalOperator : BoundExpression { public BoundUserDefinedConditionalLogicalOperator(SyntaxNode syntax, BinaryOperatorKind operatorKind, BoundExpression left, BoundExpression right, MethodSymbol logicalOperator, MethodSymbol trueOperator, MethodSymbol falseOperator, LookupResultKind resultKind, TypeSymbol type, bool hasErrors = false) @@ -6168,6 +6249,8 @@ internal R VisitInternal(BoundNode node, A arg) return VisitGlobalStatementInitializer(node as BoundGlobalStatementInitializer, arg); case BoundKind.DeconstructValuePlaceholder: return VisitDeconstructValuePlaceholder(node as BoundDeconstructValuePlaceholder, arg); + case BoundKind.TupleOperandPlaceholder: + return VisitTupleOperandPlaceholder(node as BoundTupleOperandPlaceholder, arg); case BoundKind.Dup: return VisitDup(node as BoundDup, arg); case BoundKind.BadExpression: @@ -6198,6 +6281,8 @@ internal R VisitInternal(BoundNode node, A arg) return VisitRefValueOperator(node as BoundRefValueOperator, arg); case BoundKind.BinaryOperator: return VisitBinaryOperator(node as BoundBinaryOperator, arg); + case BoundKind.TupleBinaryOperator: + return VisitTupleBinaryOperator(node as BoundTupleBinaryOperator, arg); case BoundKind.UserDefinedConditionalLogicalOperator: return VisitUserDefinedConditionalLogicalOperator(node as BoundUserDefinedConditionalLogicalOperator, arg); case BoundKind.CompoundAssignmentOperator: @@ -6488,6 +6573,10 @@ public virtual R VisitDeconstructValuePlaceholder(BoundDeconstructValuePlacehold { return this.DefaultVisit(node, arg); } + public virtual R VisitTupleOperandPlaceholder(BoundTupleOperandPlaceholder node, A arg) + { + return this.DefaultVisit(node, arg); + } public virtual R VisitDup(BoundDup node, A arg) { return this.DefaultVisit(node, arg); @@ -6548,6 +6637,10 @@ public virtual R VisitBinaryOperator(BoundBinaryOperator node, A arg) { return this.DefaultVisit(node, arg); } + public virtual R VisitTupleBinaryOperator(BoundTupleBinaryOperator node, A arg) + { + return this.DefaultVisit(node, arg); + } public virtual R VisitUserDefinedConditionalLogicalOperator(BoundUserDefinedConditionalLogicalOperator node, A arg) { return this.DefaultVisit(node, arg); @@ -7092,6 +7185,10 @@ public virtual BoundNode VisitDeconstructValuePlaceholder(BoundDeconstructValueP { return this.DefaultVisit(node); } + public virtual BoundNode VisitTupleOperandPlaceholder(BoundTupleOperandPlaceholder node) + { + return this.DefaultVisit(node); + } public virtual BoundNode VisitDup(BoundDup node) { return this.DefaultVisit(node); @@ -7152,6 +7249,10 @@ public virtual BoundNode VisitBinaryOperator(BoundBinaryOperator node) { return this.DefaultVisit(node); } + public virtual BoundNode VisitTupleBinaryOperator(BoundTupleBinaryOperator node) + { + return this.DefaultVisit(node); + } public virtual BoundNode VisitUserDefinedConditionalLogicalOperator(BoundUserDefinedConditionalLogicalOperator node) { return this.DefaultVisit(node); @@ -7701,6 +7802,10 @@ public override BoundNode VisitDeconstructValuePlaceholder(BoundDeconstructValue { return null; } + public override BoundNode VisitTupleOperandPlaceholder(BoundTupleOperandPlaceholder node) + { + return null; + } public override BoundNode VisitDup(BoundDup node) { return null; @@ -7775,6 +7880,12 @@ public override BoundNode VisitBinaryOperator(BoundBinaryOperator node) this.Visit(node.Right); return null; } + public override BoundNode VisitTupleBinaryOperator(BoundTupleBinaryOperator node) + { + this.Visit(node.Left); + this.Visit(node.Right); + return null; + } public override BoundNode VisitUserDefinedConditionalLogicalOperator(BoundUserDefinedConditionalLogicalOperator node) { this.Visit(node.Left); @@ -8479,6 +8590,11 @@ public override BoundNode VisitDeconstructValuePlaceholder(BoundDeconstructValue TypeSymbol type = this.VisitType(node.Type); return node.Update(node.ValEscape, type); } + public override BoundNode VisitTupleOperandPlaceholder(BoundTupleOperandPlaceholder node) + { + TypeSymbol type = this.VisitType(node.Type); + return node.Update(type); + } public override BoundNode VisitDup(BoundDup node) { TypeSymbol type = this.VisitType(node.Type); @@ -8567,6 +8683,13 @@ public override BoundNode VisitBinaryOperator(BoundBinaryOperator node) TypeSymbol type = this.VisitType(node.Type); return node.Update(node.OperatorKind, left, right, node.ConstantValueOpt, node.MethodOpt, node.ResultKind, type); } + public override BoundNode VisitTupleBinaryOperator(BoundTupleBinaryOperator node) + { + BoundExpression left = (BoundExpression)this.Visit(node.Left); + BoundExpression right = (BoundExpression)this.Visit(node.Right); + TypeSymbol type = this.VisitType(node.Type); + return node.Update(left, right, node.OperatorKind, node.Operators, type); + } public override BoundNode VisitUserDefinedConditionalLogicalOperator(BoundUserDefinedConditionalLogicalOperator node) { BoundExpression left = (BoundExpression)this.Visit(node.Left); @@ -9394,6 +9517,14 @@ public override TreeDumperNode VisitDeconstructValuePlaceholder(BoundDeconstruct } ); } + public override TreeDumperNode VisitTupleOperandPlaceholder(BoundTupleOperandPlaceholder node, object arg) + { + return new TreeDumperNode("tupleOperandPlaceholder", null, new TreeDumperNode[] + { + new TreeDumperNode("type", node.Type, null) + } + ); + } public override TreeDumperNode VisitDup(BoundDup node, object arg) { return new TreeDumperNode("dup", null, new TreeDumperNode[] @@ -9550,6 +9681,18 @@ public override TreeDumperNode VisitBinaryOperator(BoundBinaryOperator node, obj } ); } + public override TreeDumperNode VisitTupleBinaryOperator(BoundTupleBinaryOperator node, object arg) + { + return new TreeDumperNode("tupleBinaryOperator", null, new TreeDumperNode[] + { + new TreeDumperNode("left", null, new TreeDumperNode[] { Visit(node.Left, null) }), + new TreeDumperNode("right", null, new TreeDumperNode[] { Visit(node.Right, null) }), + new TreeDumperNode("operatorKind", node.OperatorKind, null), + new TreeDumperNode("operators", node.Operators, null), + new TreeDumperNode("type", node.Type, null) + } + ); + } public override TreeDumperNode VisitUserDefinedConditionalLogicalOperator(BoundUserDefinedConditionalLogicalOperator node, object arg) { return new TreeDumperNode("userDefinedConditionalLogicalOperator", null, new TreeDumperNode[] diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_DeconstructionAssignmentOperator.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_DeconstructionAssignmentOperator.cs index 5718661b3114..c5b564ee4986 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_DeconstructionAssignmentOperator.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_DeconstructionAssignmentOperator.cs @@ -268,7 +268,7 @@ private ImmutableArray InvokeDeconstructMethod(DeconstructMetho return outLocals.ToImmutableAndFree(); } - BoundExpression EvaluateSideEffectingArgumentToTemp(BoundExpression arg, ArrayBuilder effects, + private BoundExpression EvaluateSideEffectingArgumentToTemp(BoundExpression arg, ArrayBuilder effects, ref ArrayBuilder temps) { if (CanChangeValueBetweenReads(arg, localsMayBeAssignedOrCaptured: true)) @@ -326,7 +326,7 @@ BoundExpression EvaluateSideEffectingArgumentToTemp(BoundExpression arg, ArrayBu return assignmentTargets; } - internal class DeconstructionSideEffects + private class DeconstructionSideEffects { internal ArrayBuilder init; internal ArrayBuilder deconstructions; diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_TupleBinaryOperator.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_TupleBinaryOperator.cs new file mode 100644 index 000000000000..f42ad50e4741 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_TupleBinaryOperator.cs @@ -0,0 +1,342 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Diagnostics; +using Microsoft.CodeAnalysis.CSharp.Symbols; +using Microsoft.CodeAnalysis.PooledObjects; + +namespace Microsoft.CodeAnalysis.CSharp +{ + internal sealed partial class LocalRewriter + { + /// + /// Rewrite `GetTuple() == (1, 2)` to `tuple.Item1 == 1 && tuple.Item2 == 2`. + /// Also supports the != operator, nullable and nested tuples. + /// + /// Note that all the side-effects for visible expressions are evaluated first and from left to right. The initialization phase + /// contains side-effects for: + /// - single elements in tuple literals, like `a` in `(a, ...) == (...)` for example + /// - nested expressions that aren't tuple literals, like `GetTuple()` in `(..., GetTuple()) == (..., (..., ...))` + /// On the other hand, `Item1` and `Item2` of `GetTuple()` are not saved as part of the initialization phase of `GetTuple() == (..., ...)` + /// + /// Element-wise conversions occur late, together with the element-wise comparisons. They might not be evaluated. + /// + public override BoundNode VisitTupleBinaryOperator(BoundTupleBinaryOperator node) + { + var boolType = node.Type; // we can re-use the bool type + var initEffects = ArrayBuilder.GetInstance(); + var temps = ArrayBuilder.GetInstance(); + + BoundExpression newLeft = ReplaceTerminalElementsWithTemps(node.Left, node.Operators, initEffects, temps); + BoundExpression newRight = ReplaceTerminalElementsWithTemps(node.Right, node.Operators, initEffects, temps); + + var returnValue = RewriteTupleNestedOperators(node.Operators, newLeft, newRight, boolType, temps, node.OperatorKind); + BoundExpression result = MakeSequenceOrResultValue(temps.ToImmutableAndFree(), initEffects.ToImmutableAndFree(), returnValue); + return result; + } + + private BoundExpression MakeSequenceOrResultValue(ImmutableArray locals, ImmutableArray effects, BoundExpression returnValue) + { + if (locals.IsEmpty && effects.IsEmpty) + { + return returnValue; + } + + return _factory.Sequence(locals, effects, returnValue); + } + + /// + /// Walk down tuple literals and replace all the side-effecting elements that need saving with temps. + /// Expressions that are not tuple literals need saving, and tuple literals that are involved in a simple comparison rather than a tuple comparison. + /// + private BoundExpression ReplaceTerminalElementsWithTemps(BoundExpression expr, TupleBinaryOperatorInfo operators, ArrayBuilder initEffects, ArrayBuilder temps) + { + if (!operators.IsSingle()) + { + // Example: + // in `(expr1, expr2) == (..., ...)` we need to save `expr1` and `expr2` + if (expr.Kind == BoundKind.TupleLiteral) + { + var tuple = (BoundTupleLiteral)expr; + var multiple = (TupleBinaryOperatorInfo.Multiple)operators; + var builder = ArrayBuilder.GetInstance(tuple.Arguments.Length); + for (int i = 0; i < tuple.Arguments.Length; i++) + { + var argument = tuple.Arguments[i]; + var newArgument = ReplaceTerminalElementsWithTemps(argument, multiple.Operators[i], initEffects, temps); + builder.Add(newArgument); + } + return new BoundTupleLiteral(tuple.Syntax, tuple.ArgumentNamesOpt, tuple.InferredNamesOpt, builder.ToImmutableAndFree(), tuple.Type, tuple.HasErrors); + } + } + + // Examples: + // in `expr == (..., ...)` we need to save `expr` because it's not a tuple literal + // in `(..., expr) == (..., (..., ...))` we need to save `expr` because it is used in a simple comparison + return EvaluateSideEffectingArgumentToTemp(VisitExpression(expr), initEffects, ref temps); + } + + private BoundExpression RewriteTupleOperator(TupleBinaryOperatorInfo @operator, + BoundExpression left, BoundExpression right, TypeSymbol boolType, + ArrayBuilder temps, BinaryOperatorKind operatorKind) + { + if (@operator.IsSingle()) + { + return RewriteTupleSingleOperator((TupleBinaryOperatorInfo.Single)@operator, left, right, boolType, operatorKind); + } + else + { + return RewriteTupleNestedOperators((TupleBinaryOperatorInfo.Multiple)@operator, left, right, boolType, temps, operatorKind); + } + } + + private BoundExpression RewriteTupleNestedOperators(TupleBinaryOperatorInfo.Multiple operators, BoundExpression left, BoundExpression right, + TypeSymbol boolType, ArrayBuilder temps, BinaryOperatorKind operatorKind) + { + // If either left or right is nullable, produce: + // + // // outer sequence + // leftHasValue = left.HasValue; (or true if !leftNullable) + // leftHasValue = right.HasValue (or true if !rightNullable) + // ? leftHasValue ? ... inner sequence ... : true/false + // : false/true + // + // where inner sequence is: + // leftValue = left.GetValueOrDefault(); (or left if !leftNullable) + // rightValue = right.GetValueOrDefault(); (or right if !rightNullable) + // ... logical expression using leftValue and rightValue ... + // + // and true/false and false/true depend on operatorKind (== vs. !=) + // + // But if neither is nullable, then just produce the inner sequence. + // + // Note: all the temps are created in a single bucket (rather than different scopes of applicability) for simplicity + + // PROTOTYPE(tuple-equality) Consider if optimizations from TrivialLiftedComparisonOperatorOptimizations can be applied + + var outerEffects = ArrayBuilder.GetInstance(); + var innerEffects = ArrayBuilder.GetInstance(); + + BoundExpression leftHasValue; + BoundExpression leftValue; + + // Note: left and right are either temps or `null`, so we don't have detailed information to tell us a nullable always has a value + // PROTOTYPE(tuple-equality) We could save this information when the temps are created + var isLeftNullable = left.Kind != BoundKind.TupleLiteral && left.Type.IsNullableType(); + if (isLeftNullable) + { + leftHasValue = MakeHasValueTemp(left, temps, outerEffects); + leftValue = MakeValueOrDefaultTemp(left, temps, innerEffects); + } + else + { + leftHasValue = MakeBooleanConstant(left.Syntax, true); + leftValue = left; + } + + BoundExpression rightHasValue; + BoundExpression rightValue; + + var isRightNullable = right.Kind != BoundKind.TupleLiteral && right.Type.IsNullableType(); + if (isRightNullable) + { + rightHasValue = MakeNullableHasValue(right.Syntax, right); // no need for local for right.HasValue since used once + rightValue = MakeValueOrDefaultTemp(right, temps, innerEffects); + } + else + { + rightHasValue = MakeBooleanConstant(right.Syntax, true); + rightValue = right; + } + + // Produces: + // ... logical expression using leftValue and rightValue ... + BoundExpression logicalExpression = RewriteNonNullableNestedTupleOperators(operators, leftValue, rightValue, boolType, temps, innerEffects, operatorKind); + + // Produces: + // leftValue = left.GetValueOrDefault(); (or left if !leftNullable) + // rightValue = right.GetValueOrDefault(); (or right if !rightNullable) + // ... logical expression using leftValue and rightValue ... + BoundExpression innerSequence = MakeSequenceOrResultValue(locals: ImmutableArray.Empty, innerEffects.ToImmutableAndFree(), logicalExpression); + + if (!isLeftNullable && !isRightNullable) + { + // The outer sequence degenerates when we know that both `leftHasValue` and `rightHasValue` are true + return innerSequence; + } + + // outer sequence: + // leftHasValue == rightHasValue + // ? leftHasValue ? ... inner sequence ... : true/false + // : false/true + bool boolValue = operatorKind == BinaryOperatorKind.Equal; // true/false + BoundExpression outerSequence = + MakeSequenceOrResultValue(ImmutableArray.Empty, outerEffects.ToImmutableAndFree(), + _factory.Conditional( + _factory.Binary(BinaryOperatorKind.Equal, boolType, leftHasValue, rightHasValue), + _factory.Conditional(leftHasValue, innerSequence, MakeBooleanConstant(right.Syntax, boolValue), boolType), + MakeBooleanConstant(right.Syntax, !boolValue), + boolType)); + + return outerSequence; + } + + private BoundLocal MakeTemp(BoundExpression loweredExpression, ArrayBuilder temps, ArrayBuilder effects) + { + BoundLocal temp = _factory.StoreToTemp(loweredExpression, out BoundAssignmentOperator assignmentToTemp); + effects.Add(assignmentToTemp); + temps.Add(temp.LocalSymbol); + return temp; + } + + /// + /// Returns a temp which is initialized with lowered-expression.HasValue + /// + private BoundLocal MakeHasValueTemp(BoundExpression expression, ArrayBuilder temps, ArrayBuilder effects) + { + BoundExpression hasValueCall = MakeNullableHasValue(expression.Syntax, expression); + return MakeTemp(hasValueCall, temps, effects); + } + + /// + /// Returns a temp which is initialized with lowered-expression.GetValueOrDefault() + /// + private BoundLocal MakeValueOrDefaultTemp(BoundExpression expression, + ArrayBuilder temps, ArrayBuilder effects) + { + BoundExpression valueOrDefaultCall = MakeOptimizedGetValueOrDefault(expression.Syntax, expression); + return MakeTemp(valueOrDefaultCall, temps, effects); + } + + /// + /// Produces a chain of equality (or inequality) checks combined logically with AND (or OR) + /// + private BoundExpression RewriteNonNullableNestedTupleOperators(TupleBinaryOperatorInfo.Multiple operators, + BoundExpression left, BoundExpression right, TypeSymbol type, + ArrayBuilder temps, ArrayBuilder effects, BinaryOperatorKind operatorKind) + { + ImmutableArray nestedOperators = operators.Operators; + + BoundExpression currentResult = null; + for (int i = 0; i < nestedOperators.Length; i++) + { + BoundExpression leftElement = GetTuplePart(left, i); + BoundExpression rightElement = GetTuplePart(right, i); + BoundExpression nextLogicalOperand = RewriteTupleOperator(nestedOperators[i], leftElement, rightElement, type, temps, operatorKind); + if (currentResult is null) + { + currentResult = nextLogicalOperand; + } + else + { + var logicalOperator = operatorKind == BinaryOperatorKind.Equal ? BinaryOperatorKind.LogicalBoolAnd : BinaryOperatorKind.LogicalBoolOr; + currentResult = _factory.Binary(logicalOperator, type, currentResult, nextLogicalOperand); + } + } + + return currentResult; + } + + /// + /// For tuple literals, we just return the element. + /// For expressions with tuple type, we access `Item{i}`. + /// + private BoundExpression GetTuplePart(BoundExpression tuple, int i) + { + // Example: + // (1, 2) == (1, 2); + if (tuple.Kind == BoundKind.TupleLiteral) + { + return ((BoundTupleLiteral)tuple).Arguments[i]; + } + + Debug.Assert(tuple.Type.IsTupleType); + + // Example: + // t == GetTuple(); + // t == ((byte, byte)) (1, 2); + // t == ((short, short))((int, int))(1L, 2L); + return MakeTupleFieldAccessAndReportUseSiteDiagnostics(tuple, tuple.Syntax, tuple.Type.TupleElements[i]); + } + + /// + /// Produce an element-wise comparison and logic to ensure the result is a bool type. + /// + /// If an element-wise comparison doesn't return bool, then: + /// - if it is dynamic, we'll do `!(comparisonResult.false)` or `comparisonResult.true` + /// - if it implicitly converts to bool, we'll just do the conversion + /// - otherwise, we'll do `!(comparisonResult.false)` or `comparisonResult.true` (as we'd do for `if` or `while`) + /// + private BoundExpression RewriteTupleSingleOperator(TupleBinaryOperatorInfo.Single single, + BoundExpression left, BoundExpression right, TypeSymbol boolType, BinaryOperatorKind operatorKind) + { + if (single.Kind.IsDynamic()) + { + // Produce + // !((left == right).op_false) + // (left != right).op_true + + BoundExpression dynamicResult = _dynamicFactory.MakeDynamicBinaryOperator(single.Kind, left, right, isCompoundAssignment: false, _compilation.DynamicType).ToExpression(); + if (operatorKind == BinaryOperatorKind.Equal) + { + return _factory.Not(MakeUnaryOperator(UnaryOperatorKind.DynamicFalse, left.Syntax, method: null, dynamicResult, boolType)); + } + else + { + return MakeUnaryOperator(UnaryOperatorKind.DynamicTrue, left.Syntax, method: null, dynamicResult, boolType); + } + } + + if (left.IsLiteralNull() && right.IsLiteralNull()) + { + // For `null == null` this is special-cased during initial binding + return new BoundLiteral(left.Syntax, ConstantValue.Create(operatorKind == BinaryOperatorKind.Equal), boolType); + } + + // PROTOTYPE(tuple-equality) checked + // We leave both operands in nullable-null conversions unconverted, MakeBinaryOperator has special for null-literal + bool isNullableNullConversion = single.Kind.OperandTypes() == BinaryOperatorKind.NullableNull; + BoundExpression convertedLeft = isNullableNullConversion + ? left + : MakeConversionNode(left.Syntax, left, single.LeftConversion, single.LeftConvertedTypeOpt, @checked: false); + + BoundExpression convertedRight = isNullableNullConversion + ? right + : MakeConversionNode(right.Syntax, right, single.RightConversion, single.RightConvertedTypeOpt, @checked: false); + + BoundExpression binary = MakeBinaryOperator(_factory.Syntax, single.Kind, convertedLeft, convertedRight, single.MethodSymbolOpt?.ReturnType ?? boolType, single.MethodSymbolOpt); + UnaryOperatorSignature boolOperator = single.BoolOperator; + Conversion boolConversion = single.ConversionForBool; + + BoundExpression result; + if (boolOperator.Kind != UnaryOperatorKind.Error) + { + // Produce + // !((left == right).op_false) + // (left != right).op_true + BoundExpression convertedBinary = MakeConversionNode(_factory.Syntax, binary, boolConversion, boolOperator.OperandType, @checked: false); + + Debug.Assert(boolOperator.ReturnType.SpecialType == SpecialType.System_Boolean); + result = MakeUnaryOperator(boolOperator.Kind, binary.Syntax, boolOperator.Method, convertedBinary, boolType); + + if (operatorKind == BinaryOperatorKind.Equal) + { + result = _factory.Not(result); + } + } + else if (!boolConversion.IsIdentity) + { + // Produce + // (bool)(left == right) + // (bool)(left != right) + result = MakeConversionNode(_factory.Syntax, binary, boolConversion, boolType, @checked: false); + } + else + { + result = binary; + } + + return result; + } + } +} diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_UnaryOperator.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_UnaryOperator.cs index af740ad177f6..5bef4f73d6ac 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_UnaryOperator.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_UnaryOperator.cs @@ -76,20 +76,20 @@ private BoundExpression MakeUnaryOperator( { if (kind.IsDynamic()) { - Debug.Assert(kind == UnaryOperatorKind.DynamicTrue && type.SpecialType == SpecialType.System_Boolean || type.IsDynamic()); + Debug.Assert((kind == UnaryOperatorKind.DynamicTrue || kind == UnaryOperatorKind.DynamicFalse) && type.SpecialType == SpecialType.System_Boolean + || type.IsDynamic()); Debug.Assert((object)method == null); // Logical operators on boxed Boolean constants: var constant = UnboxConstant(loweredOperand); if (constant == ConstantValue.True || constant == ConstantValue.False) { - if (kind == UnaryOperatorKind.DynamicTrue) + switch (kind) { - return _factory.Literal(constant.BooleanValue); - } - else if (kind == UnaryOperatorKind.DynamicLogicalNegation) - { - return MakeConversionNode(_factory.Literal(!constant.BooleanValue), type, @checked: false); + case UnaryOperatorKind.DynamicTrue: + return _factory.Literal(constant.BooleanValue); + case UnaryOperatorKind.DynamicLogicalNegation: + return MakeConversionNode(_factory.Literal(!constant.BooleanValue), type, @checked: false); } } diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf index be6f21278586..2ededd92a283 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf @@ -8615,6 +8615,16 @@ Pokud chcete odstranit toto varování, můžete místo toho použít /reference '{0}' cannot implement interface member '{1}' in type '{2}' because it has an __arglist parameter + + tuple equality + tuple equality + + + + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf index 822a1079ad79..774c3d79debd 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf @@ -8615,6 +8615,16 @@ Um die Warnung zu beheben, können Sie stattdessen /reference verwenden (Einbett '{0}' cannot implement interface member '{1}' in type '{2}' because it has an __arglist parameter + + tuple equality + tuple equality + + + + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf index 981321df291c..d0fe35c0e9e3 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf @@ -8615,6 +8615,16 @@ Para eliminar la advertencia puede usar /reference (establezca la propiedad Embe '{0}' cannot implement interface member '{1}' in type '{2}' because it has an __arglist parameter + + tuple equality + tuple equality + + + + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf index fffeab124525..de4729aed083 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf @@ -8615,6 +8615,16 @@ Pour supprimer l'avertissement, vous pouvez utiliser la commande /reference (dé '{0}' cannot implement interface member '{1}' in type '{2}' because it has an __arglist parameter + + tuple equality + tuple equality + + + + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf index d7caf7a9c6a7..0ef720cc46fe 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf @@ -8615,6 +8615,16 @@ Per rimuovere l'avviso, è invece possibile usare /reference (impostare la propr '{0}' cannot implement interface member '{1}' in type '{2}' because it has an __arglist parameter + + tuple equality + tuple equality + + + + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf index b8a451fba46d..510ae16baaf1 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf @@ -8615,6 +8615,16 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ '{0}' cannot implement interface member '{1}' in type '{2}' because it has an __arglist parameter + + tuple equality + tuple equality + + + + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf index 3164d095bb84..705f75a9176a 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf @@ -8615,6 +8615,16 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ '{0}' cannot implement interface member '{1}' in type '{2}' because it has an __arglist parameter + + tuple equality + tuple equality + + + + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf index 264c5304efbd..c579c2b69bc6 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf @@ -8615,6 +8615,16 @@ Aby usunąć ostrzeżenie, możesz zamiast tego użyć opcji /reference (ustaw w '{0}' cannot implement interface member '{1}' in type '{2}' because it has an __arglist parameter + + tuple equality + tuple equality + + + + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf index 5545c7001c6f..c44a3876d9b3 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf @@ -8615,6 +8615,16 @@ Para incorporar informações de tipo de interoperabilidade para os dois assembl '{0}' cannot implement interface member '{1}' in type '{2}' because it has an __arglist parameter + + tuple equality + tuple equality + + + + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf index 337513f17d1b..35cb4e891f10 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf @@ -8615,6 +8615,16 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ '{0}' cannot implement interface member '{1}' in type '{2}' because it has an __arglist parameter + + tuple equality + tuple equality + + + + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf index 9d4546b1a2c0..4501751f6e8c 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf @@ -8615,6 +8615,16 @@ Uyarıyı kaldırmak için, /reference kullanabilirsiniz (Birlikte Çalışma T '{0}' cannot implement interface member '{1}' in type '{2}' because it has an __arglist parameter + + tuple equality + tuple equality + + + + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf index 44cfa8ef6911..1909e3b92b27 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf @@ -8615,6 +8615,16 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ '{0}' cannot implement interface member '{1}' in type '{2}' because it has an __arglist parameter + + tuple equality + tuple equality + + + + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf index 26efc0403945..98a40a68e8c5 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf @@ -8615,6 +8615,16 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ '{0}' cannot implement interface member '{1}' in type '{2}' because it has an __arglist parameter + + tuple equality + tuple equality + + + + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + Tuple types used as operands of an == or != operator must have matching cardinalities. But this operator has tuple types of cardinality {0} on the left and {1} on the right. + + \ No newline at end of file diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDeconstructTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDeconstructTests.cs index 8cd16743f7ae..45bda124bcec 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDeconstructTests.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDeconstructTests.cs @@ -393,6 +393,66 @@ class D2 comp.VerifyDiagnostics(); } + [Fact] + public void VerifyExecutionOrder_TupleLiteralAndDeconstruction() + { + string source = @" +using System; +class C +{ + int w { set { Console.WriteLine($""setW""); } } + int x { set { Console.WriteLine($""setX""); } } + int y { set { Console.WriteLine($""setY""); } } + int z { set { Console.WriteLine($""setZ""); } } + + C getHolderForW() { Console.WriteLine(""getHolderforW""); return this; } + C getHolderForX() { Console.WriteLine(""getHolderforX""); return this; } + C getHolderForY() { Console.WriteLine(""getHolderforY""); return this; } + C getHolderForZ() { Console.WriteLine(""getHolderforZ""); return this; } + + static void Main() + { + C c = new C(); + (c.getHolderForW().x, (c.getHolderForY().y, c.getHolderForZ().z), c.getHolderForX().x) = (new D1(), new D2(), new D3()); + } +} +class D1 +{ + public D1() { Console.WriteLine(""Constructor1""); } + public static implicit operator int(D1 d) { Console.WriteLine(""Conversion1""); return 1; } +} +class D2 +{ + public D2() { Console.WriteLine(""Constructor2""); } + public void Deconstruct(out int x, out int y) { x = 2; y = 3; Console.WriteLine(""deconstruct""); } +} +class D3 +{ + public D3() { Console.WriteLine(""Constructor3""); } + public static implicit operator int(D3 d) { Console.WriteLine(""Conversion3""); return 3; } +} +"; + + string expected = +@"getHolderforW +getHolderforY +getHolderforZ +getHolderforX +Constructor1 +Conversion1 +Constructor2 +Constructor3 +Conversion3 +deconstruct +setX +setY +setZ +setX +"; + var comp = CompileAndVerify(source, expectedOutput: expected, additionalRefs: s_valueTupleRefs); + comp.VerifyDiagnostics(); + } + [Fact] public void DifferentVariableKinds() { diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenTupleEqualityTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenTupleEqualityTests.cs new file mode 100644 index 000000000000..604cd9fbd1ca --- /dev/null +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenTupleEqualityTests.cs @@ -0,0 +1,3512 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp.Test.Utilities; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.CSharp.UnitTests.CodeGen +{ + [CompilerTrait(CompilerFeature.TupleEquality)] + public class CodeGenTupleEqualityTests : CSharpTestBase + { + private static readonly MetadataReference[] s_valueTupleRefs = new[] { SystemRuntimeFacadeRef, ValueTupleRef }; + + [Fact] + public void TestCSharp7_2() + { + var source = @" +class C +{ + static void Main() + { + var t = (1, 2); + System.Console.Write(t == (1, 2)); + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, parseOptions: TestOptions.Regular7_2); + comp.VerifyDiagnostics( + // (7,30): error CS8320: Feature 'tuple equality' is not available in C# 7.2. Please use language version 7.3 or greater. + // System.Console.Write(t == (1, 2)); + Diagnostic(ErrorCode.ERR_FeatureNotAvailableInVersion7_2, "t == (1, 2)").WithArguments("tuple equality", "7.3").WithLocation(7, 30) + ); + } + + [Theory] + [InlineData("(1, 2)", "(1L, 2L)", true)] + [InlineData("(1, 2)", "(1, 0)", false)] + [InlineData("(1, 2)", "(0, 2)", false)] + [InlineData("(1, 2)", "((long, long))(1, 2)", true)] + [InlineData("((1, 2L), (3, 4))", "((1L, 2), (3L, 4))", true)] + [InlineData("((1, 2L), (3, 4))", "((0L, 2), (3L, 4))", false)] + [InlineData("((1, 2L), (3, 4))", "((1L, 0), (3L, 4))", false)] + [InlineData("((1, 2L), (3, 4))", "((1L, 0), (0L, 4))", false)] + [InlineData("((1, 2L), (3, 4))", "((1L, 0), (3L, 0))", false)] + void TestSimple(string change1, string change2, bool expectedMatch) + { + var sourceTemplate = @" +class C +{ + static void Main() + { + var t1 = CHANGE1; + var t2 = CHANGE2; + System.Console.Write($""{(t1 == t2) == EXPECTED} {(t1 != t2) != EXPECTED}""); + } +}"; + string source = sourceTemplate + .Replace("CHANGE1", change1) + .Replace("CHANGE2", change2) + .Replace("EXPECTED", expectedMatch ? "true" : "false"); + string name = GetUniqueName(); + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe, assemblyName: name); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "True True"); + } + + [Fact] + public void TestTuplesWithDifferentCardinalities() + { + var source = @" +class C +{ + static bool M() + { + var t1 = (1, 1); + var t2 = (2, 2, 2); + return t1 == t2; + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (8,16): error CS8355: Tuple types used as operands of a binary operator must have matching cardinalities. But this operator has tuple types of cardinality 2 on the left and 3 on the right. + // return t1 == t2; + Diagnostic(ErrorCode.ERR_TupleSizesMismatchForBinOps, "t1 == t2").WithArguments("2", "3").WithLocation(8, 16) + ); + } + + [Fact] + public void TestNestedTuplesWithDifferentCardinalities() + { + var source = @" +class C +{ + static bool M() + { + var t1 = (1, (1, 1)); + var t2 = (2, (2, 2, 2)); + return t1 == t2; + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (8,16): error CS8355: Tuple types used as operands of a binary operator must have matching cardinalities. But this operator has tuple types of cardinality 2 on the left and 3 on the right. + // return t1 == t2; + Diagnostic(ErrorCode.ERR_TupleSizesMismatchForBinOps, "t1 == t2").WithArguments("2", "3").WithLocation(8, 16) + ); + } + + [Fact] + public void TestWithoutValueTuple() + { + var source = @" +class C +{ + static bool M() + { + return (1, 2) == (3, 4); + } +}"; + var comp = CreateStandardCompilation(source); + + // PROTOTYPE(tuple-equality) See if we can relax the restriction on requiring ValueTuple types + + comp.VerifyDiagnostics( + // (6,16): error CS8179: Predefined type 'System.ValueTuple`2' is not defined or imported + // return (1, 2) == (3, 4); + Diagnostic(ErrorCode.ERR_PredefinedValueTupleTypeNotFound, "(1, 2)").WithArguments("System.ValueTuple`2").WithLocation(6, 16), + // (6,26): error CS8179: Predefined type 'System.ValueTuple`2' is not defined or imported + // return (1, 2) == (3, 4); + Diagnostic(ErrorCode.ERR_PredefinedValueTupleTypeNotFound, "(3, 4)").WithArguments("System.ValueTuple`2").WithLocation(6, 26), + // (6,16): error CS8179: Predefined type 'System.ValueTuple`2' is not defined or imported + // return (1, 2) == (3, 4); + Diagnostic(ErrorCode.ERR_PredefinedValueTupleTypeNotFound, "(1, 2)").WithArguments("System.ValueTuple`2").WithLocation(6, 16), + // (6,26): error CS8179: Predefined type 'System.ValueTuple`2' is not defined or imported + // return (1, 2) == (3, 4); + Diagnostic(ErrorCode.ERR_PredefinedValueTupleTypeNotFound, "(3, 4)").WithArguments("System.ValueTuple`2").WithLocation(6, 26) + ); + } + + [Fact] + public void TestNestedNullableTuplesWithDifferentCardinalities() + { + var source = @" +class C +{ + static bool M() + { + (int, int)? nt = (1, 1); + var t1 = (1, nt); + var t2 = (2, (2, 2, 2)); + return t1 == t2; + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (9,16): error CS8373: Tuple types used as operands of a binary operator must have matching cardinalities. But this operator has tuple types of cardinality 2 on the left and 3 on the right. + // return t1 == t2; + Diagnostic(ErrorCode.ERR_TupleSizesMismatchForBinOps, "t1 == t2").WithArguments("2", "3").WithLocation(9, 16) + ); + } + + [Fact] + public void TestILForSimpleEqual() + { + var source = @" +class C +{ + static bool M() + { + var t1 = (1, 1); + var t2 = (2, 2); + return t1 == t2; + } +}"; + var comp = CompileAndVerify(source, additionalRefs: s_valueTupleRefs); + comp.VerifyDiagnostics(); + + comp.VerifyIL("C.M", @"{ + // Code size 50 (0x32) + .maxstack 3 + .locals init (System.ValueTuple V_0, //t1 + System.ValueTuple V_1, + System.ValueTuple V_2) + IL_0000: ldloca.s V_0 + IL_0002: ldc.i4.1 + IL_0003: ldc.i4.1 + IL_0004: call ""System.ValueTuple..ctor(int, int)"" + IL_0009: ldc.i4.2 + IL_000a: ldc.i4.2 + IL_000b: newobj ""System.ValueTuple..ctor(int, int)"" + IL_0010: ldloc.0 + IL_0011: stloc.1 + IL_0012: stloc.2 + IL_0013: ldloc.1 + IL_0014: ldfld ""int System.ValueTuple.Item1"" + IL_0019: ldloc.2 + IL_001a: ldfld ""int System.ValueTuple.Item1"" + IL_001f: bne.un.s IL_0030 + IL_0021: ldloc.1 + IL_0022: ldfld ""int System.ValueTuple.Item2"" + IL_0027: ldloc.2 + IL_0028: ldfld ""int System.ValueTuple.Item2"" + IL_002d: ceq + IL_002f: ret + IL_0030: ldc.i4.0 + IL_0031: ret +}"); + } + + [Fact] + public void TestILForSimpleNotEqual() + { + var source = @" +class C +{ + static bool M((int, int) t1, (int, int) t2) + { + return t1 != t2; + } +}"; + var comp = CompileAndVerify(source, additionalRefs: s_valueTupleRefs); + comp.VerifyDiagnostics(); + + comp.VerifyIL("C.M", @"{ + // Code size 38 (0x26) + .maxstack 2 + .locals init (System.ValueTuple V_0, + System.ValueTuple V_1) + IL_0000: ldarg.0 + IL_0001: stloc.0 + IL_0002: ldarg.1 + IL_0003: stloc.1 + IL_0004: ldloc.0 + IL_0005: ldfld ""int System.ValueTuple.Item1"" + IL_000a: ldloc.1 + IL_000b: ldfld ""int System.ValueTuple.Item1"" + IL_0010: bne.un.s IL_0024 + IL_0012: ldloc.0 + IL_0013: ldfld ""int System.ValueTuple.Item2"" + IL_0018: ldloc.1 + IL_0019: ldfld ""int System.ValueTuple.Item2"" + IL_001e: ceq + IL_0020: ldc.i4.0 + IL_0021: ceq + IL_0023: ret + IL_0024: ldc.i4.1 + IL_0025: ret +}"); + } + + [Fact] + public void TestILForSimpleEqualOnInTuple() + { + var source = @" +class C +{ + static bool M(in (int, int) t1, in (int, int) t2) + { + return t1 == t2; + } +}"; + var comp = CompileAndVerify(source, additionalRefs: s_valueTupleRefs); + comp.VerifyDiagnostics(); + + // note: the logic to save variables and side-effects results in copying the inputs + comp.VerifyIL("C.M", @"{ + // Code size 45 (0x2d) + .maxstack 2 + .locals init (System.ValueTuple V_0, + System.ValueTuple V_1) + IL_0000: ldarg.0 + IL_0001: ldobj ""System.ValueTuple"" + IL_0006: stloc.0 + IL_0007: ldarg.1 + IL_0008: ldobj ""System.ValueTuple"" + IL_000d: stloc.1 + IL_000e: ldloc.0 + IL_000f: ldfld ""int System.ValueTuple.Item1"" + IL_0014: ldloc.1 + IL_0015: ldfld ""int System.ValueTuple.Item1"" + IL_001a: bne.un.s IL_002b + IL_001c: ldloc.0 + IL_001d: ldfld ""int System.ValueTuple.Item2"" + IL_0022: ldloc.1 + IL_0023: ldfld ""int System.ValueTuple.Item2"" + IL_0028: ceq + IL_002a: ret + IL_002b: ldc.i4.0 + IL_002c: ret +}"); + } + + [Fact] + public void TestILForSimpleEqualOnTupleLiterals() + { + var source = @" +class C +{ + static void Main() + { + M(1, 1); + M(1, 2); + M(2, 1); + } + static void M(int x, byte y) + { + System.Console.Write($""{(x, x) == (y, y)} ""); + } +}"; + var comp = CompileAndVerify(source, additionalRefs: s_valueTupleRefs, expectedOutput: "True False False"); + comp.VerifyDiagnostics(); + + comp.VerifyIL("C.M", @"{ + // Code size 38 (0x26) + .maxstack 3 + .locals init (int V_0, + byte V_1, + byte V_2) + IL_0000: ldstr ""{0} "" + IL_0005: ldarg.0 + IL_0006: ldarg.0 + IL_0007: stloc.0 + IL_0008: ldarg.1 + IL_0009: stloc.1 + IL_000a: ldarg.1 + IL_000b: stloc.2 + IL_000c: ldloc.1 + IL_000d: bne.un.s IL_0015 + IL_000f: ldloc.0 + IL_0010: ldloc.2 + IL_0011: ceq + IL_0013: br.s IL_0016 + IL_0015: ldc.i4.0 + IL_0016: box ""bool"" + IL_001b: call ""string string.Format(string, object)"" + IL_0020: call ""void System.Console.Write(string)"" + IL_0025: ret +}"); + + var tree = comp.Compilation.SyntaxTrees.First(); + var model = comp.Compilation.GetSemanticModel(tree); + + var tupleY = tree.GetCompilationUnitRoot().DescendantNodes().OfType().Last(); + Assert.Equal("(y, y)", tupleY.ToString()); + + // PROTOTYPE(tuple-equality) + return; + + //var tupleYSymbol = model.GetTypeInfo(tupleY); + //Assert.Equal("(System.Byte, System.Byte)", tupleYSymbol.Type.ToTestDisplayString()); + //Assert.Equal("(System.Int32, System.Int32)", tupleYSymbol.ConvertedType.ToTestDisplayString()); + + //var y = tupleY.Arguments[0].Expression; + //var ySymbol = model.GetTypeInfo(y); + //Assert.Equal("System.Byte", ySymbol.Type.ToTestDisplayString()); + //Assert.Equal("System.Int32", ySymbol.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void TestILForNullableElementsEqualsToNull() + { + var source = @" +class C +{ + static void Main() + { + System.Console.Write(M(null, null)); + System.Console.Write(M(1, true)); + } + static bool M(int? i1, bool? b1) + { + var t1 = (i1, b1); + return t1 == (null, null); + } +}"; + var comp = CompileAndVerify(source, additionalRefs: s_valueTupleRefs, expectedOutput: "TrueFalse"); + comp.VerifyDiagnostics(); + + comp.VerifyIL("C.M", @"{ + // Code size 40 (0x28) + .maxstack 2 + .locals init (System.ValueTuple V_0) + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: newobj ""System.ValueTuple..ctor(int?, bool?)"" + IL_0007: stloc.0 + IL_0008: ldloca.s V_0 + IL_000a: ldflda ""int? System.ValueTuple.Item1"" + IL_000f: call ""bool int?.HasValue.get"" + IL_0014: brtrue.s IL_0026 + IL_0016: ldloca.s V_0 + IL_0018: ldflda ""bool? System.ValueTuple.Item2"" + IL_001d: call ""bool bool?.HasValue.get"" + IL_0022: ldc.i4.0 + IL_0023: ceq + IL_0025: ret + IL_0026: ldc.i4.0 + IL_0027: ret +}"); + } + + [Fact] + public void TestILForNullableElementsNotEqualsToNull() + { + var source = @" +class C +{ + static void Main() + { + System.Console.Write(M(null, null)); + System.Console.Write(M(1, true)); + } + static bool M(int? i1, bool? b1) + { + var t1 = (i1, b1); + return t1 != (null, null); + } +}"; + var comp = CompileAndVerify(source, additionalRefs: s_valueTupleRefs, expectedOutput: "FalseTrue"); + comp.VerifyDiagnostics(); + + comp.VerifyIL("C.M", @"{ + // Code size 37 (0x25) + .maxstack 2 + .locals init (System.ValueTuple V_0) + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: newobj ""System.ValueTuple..ctor(int?, bool?)"" + IL_0007: stloc.0 + IL_0008: ldloca.s V_0 + IL_000a: ldflda ""int? System.ValueTuple.Item1"" + IL_000f: call ""bool int?.HasValue.get"" + IL_0014: brtrue.s IL_0023 + IL_0016: ldloca.s V_0 + IL_0018: ldflda ""bool? System.ValueTuple.Item2"" + IL_001d: call ""bool bool?.HasValue.get"" + IL_0022: ret + IL_0023: ldc.i4.1 + IL_0024: ret +}"); + } + + [Fact] + public void TestILForNullableElementsComparedToNonNullValues() + { + var source = @" +class C +{ + static void Main() + { + System.Console.Write(M((null, null))); + System.Console.Write(M((2, true))); + } + static bool M((int?, bool?) t1) + { + return t1 == (2, true); + } +}"; + var comp = CompileAndVerify(source, additionalRefs: s_valueTupleRefs, expectedOutput: "FalseTrue"); + comp.VerifyDiagnostics(); + + comp.VerifyIL("C.M", @"{ + // Code size 66 (0x42) + .maxstack 2 + .locals init (System.ValueTuple V_0, + int? V_1, + int V_2, + bool? V_3, + bool V_4) + IL_0000: ldarg.0 + IL_0001: stloc.0 + IL_0002: ldloc.0 + IL_0003: ldfld ""int? System.ValueTuple.Item1"" + IL_0008: stloc.1 + IL_0009: ldc.i4.2 + IL_000a: stloc.2 + IL_000b: ldloca.s V_1 + IL_000d: call ""int int?.GetValueOrDefault()"" + IL_0012: ldloc.2 + IL_0013: beq.s IL_0018 + IL_0015: ldc.i4.0 + IL_0016: br.s IL_001f + IL_0018: ldloca.s V_1 + IL_001a: call ""bool int?.HasValue.get"" + IL_001f: brfalse.s IL_0040 + IL_0021: ldloc.0 + IL_0022: ldfld ""bool? System.ValueTuple.Item2"" + IL_0027: stloc.3 + IL_0028: ldc.i4.1 + IL_0029: stloc.s V_4 + IL_002b: ldloca.s V_3 + IL_002d: call ""bool bool?.GetValueOrDefault()"" + IL_0032: ldloc.s V_4 + IL_0034: beq.s IL_0038 + IL_0036: ldc.i4.0 + IL_0037: ret + IL_0038: ldloca.s V_3 + IL_003a: call ""bool bool?.HasValue.get"" + IL_003f: ret + IL_0040: ldc.i4.0 + IL_0041: ret +}"); + } + + [Fact] + public void TestILForNullableStructEqualsToNull() + { + var source = @" +struct S +{ + static void Main() + { + S? s = null; + _ = s == null; + System.Console.Write((s, null) == (null, s)); + } +}"; + var comp = CompileAndVerify(source, additionalRefs: s_valueTupleRefs, expectedOutput: "True"); + comp.VerifyDiagnostics(); + + comp.VerifyIL("S.Main", @"{ + // Code size 48 (0x30) + .maxstack 2 + .locals init (S? V_0, //s + S? V_1, + S? V_2) + IL_0000: ldloca.s V_0 + IL_0002: initobj ""S?"" + IL_0008: ldloca.s V_0 + IL_000a: call ""bool S?.HasValue.get"" + IL_000f: pop + IL_0010: ldloc.0 + IL_0011: stloc.1 + IL_0012: ldloc.0 + IL_0013: stloc.2 + IL_0014: ldloca.s V_1 + IL_0016: call ""bool S?.HasValue.get"" + IL_001b: brtrue.s IL_0029 + IL_001d: ldloca.s V_2 + IL_001f: call ""bool S?.HasValue.get"" + IL_0024: ldc.i4.0 + IL_0025: ceq + IL_0027: br.s IL_002a + IL_0029: ldc.i4.0 + IL_002a: call ""void System.Console.Write(bool)"" + IL_002f: ret +}"); + } + + [Fact] + public void TestThisStruct() + { + var source = @" +public struct S +{ + public int I; + public static void Main() + { + S s = new S() { I = 1 }; + s.M(); + } + void M() + { + System.Console.Write((this, 2) == (1, this.Mutate())); + } + + S Mutate() + { + I++; + return this; + } + public static implicit operator S(int value) { return new S() { I = value }; } + public static bool operator==(S s1, S s2) { System.Console.Write($""{s1.I} == {s2.I}, ""); return s1.I == s2.I; } + public static bool operator!=(S s1, S s2) { throw null; } +}"; + // PROTOTYPE(tuple-equality) We need to create a temp for `this`, otherwise it gets mutated + var comp = CompileAndVerify(source, additionalRefs: s_valueTupleRefs, expectedOutput: "2 == 1, False"); + //comp.VerifyDiagnostics(); + } + + [Fact] + public void TestSimpleEqualOnTypelessTupleLiteral() + { + var source = @" +class C +{ + static bool M((string, long) t) + { + return t == (null, 1) && t == (""hello"", 1); + } +}"; + var comp = CompileAndVerify(source, additionalRefs: s_valueTupleRefs); + comp.VerifyDiagnostics(); + var tree = comp.Compilation.SyntaxTrees.First(); + var model = comp.Compilation.GetSemanticModel(tree); + + // PROTOTYPE(tuple-equality) Semantic model + return; + + //var tuple1 = tree.GetCompilationUnitRoot().DescendantNodes().OfType().ElementAt(0); + //var symbol1 = model.GetTypeInfo(tuple1); + //Assert.Null(symbol1.Type); + //Assert.Equal("(System.String, System.Int64)", symbol1.ConvertedType.ToTestDisplayString()); + + //var tuple2 = tree.GetCompilationUnitRoot().DescendantNodes().OfType().ElementAt(1); + //var symbol2 = model.GetTypeInfo(tuple2); + //Assert.Equal("(System.String, System.Int32)", symbol2.Type.ToTestDisplayString()); + //Assert.Equal("(System.String, System.Int64)", symbol2.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void TestOtherOperatorsOnTuples() + { + var source = @" +class C +{ + void M() + { + var t1 = (1, 2); + _ = t1 + t1; // error 1 + _ = t1 > t1; // error 2 + _ = t1 >= t1; // error 3 + _ = !t1; // error 4 + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (7,13): error CS0019: Operator '+' cannot be applied to operands of type '(int, int)' and '(int, int)' + // _ = t1 + t1; // error 1 + Diagnostic(ErrorCode.ERR_BadBinaryOps, "t1 + t1").WithArguments("+", "(int, int)", "(int, int)").WithLocation(7, 13), + // (8,13): error CS0019: Operator '>' cannot be applied to operands of type '(int, int)' and '(int, int)' + // _ = t1 > t1; // error 2 + Diagnostic(ErrorCode.ERR_BadBinaryOps, "t1 > t1").WithArguments(">", "(int, int)", "(int, int)").WithLocation(8, 13), + // (9,13): error CS0019: Operator '>=' cannot be applied to operands of type '(int, int)' and '(int, int)' + // _ = t1 >= t1; // error 3 + Diagnostic(ErrorCode.ERR_BadBinaryOps, "t1 >= t1").WithArguments(">=", "(int, int)", "(int, int)").WithLocation(9, 13), + // (10,13): error CS0023: Operator '!' cannot be applied to operand of type '(int, int)' + // _ = !t1; // error 4 + Diagnostic(ErrorCode.ERR_BadUnaryOp, "!t1").WithArguments("!", "(int, int)").WithLocation(10, 13) + ); + } + + [Fact] + public void TestTypelessTuples() + { + var source = @" +class C +{ + static void Main() + { + string s = null; + System.Console.Write((s, null) == (null, s)); + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "True"); + + // PROTOTYPE(tuple-equality) Semantic model + return; + + //var tree = comp.SyntaxTrees[0]; + //var model = comp.GetSemanticModel(tree); + + //var tuple1 = tree.GetCompilationUnitRoot().DescendantNodes().OfType().ElementAt(0); + //Assert.Equal("(s, null)", tuple1.ToString()); + //var tupleType1 = model.GetTypeInfo(tuple1); + //Assert.Null(tupleType1.Type); + //Assert.Equal("(System.String, System.String)", tupleType1.ConvertedType.ToTestDisplayString()); + + //var tuple2 = tree.GetCompilationUnitRoot().DescendantNodes().OfType().ElementAt(1); + //Assert.Equal("(null, s)", tuple2.ToString()); + //var tupleType2 = model.GetTypeInfo(tuple2); + //Assert.Null(tupleType2.Type); + //Assert.Equal("(System.String, System.String)", tupleType2.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void TestWithNoSideEffectsOrTemps() + { + var source = @" +class C +{ + static void Main() + { + System.Console.Write((1, 2) == (1, 3)); + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "False"); + } + + [Fact] + public void TestSimpleTupleAndTupleType() + { + var source = @" +class C +{ + static void Main() + { + var t1 = (1, 2L); + System.Console.Write(t1 == (1L, 2)); + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "True"); + + // PROTOTYPE(tuple-equality) Semantic model + return; + + //var tree = comp.SyntaxTrees[0]; + //var model = comp.GetSemanticModel(tree); + + //var tuple = tree.GetCompilationUnitRoot().DescendantNodes().OfType().ElementAt(1); + //Assert.Equal("(1L, 2)", tuple.ToString()); + //var tupleType = model.GetTypeInfo(tuple); + //Assert.Equal("(System.Int64, System.Int32)", tupleType.Type.ToTestDisplayString()); + //Assert.Equal("(System.Int64, System.Int64)", tupleType.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void TestNestedTupleAndTupleType() + { + var source = @" +class C +{ + static void Main() + { + var t1 = (1, (2L, ""hello"")); + var t2 = (2, ""hello""); + System.Console.Write(t1 == (1L, t2)); + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "True"); + + // PROTOTYPE(tuple-equality) Semantic model + return; + + //var tree = comp.SyntaxTrees[0]; + //var model = comp.GetSemanticModel(tree); + + //var tuple = tree.GetCompilationUnitRoot().DescendantNodes().OfType().ElementAt(3); + //Assert.Equal("(1L, t2)", tuple.ToString()); + //var tupleType = model.GetTypeInfo(tuple); + //Assert.Equal("(System.Int64, (System.Int32, System.String) t2)", tupleType.Type.ToTestDisplayString()); + //Assert.Equal("(System.Int64, (System.Int64, System.String))", tupleType.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void TestTypelessTupleAndTupleType() + { + var source = @" +class C +{ + static void Main() + { + (string, string) t = (null, null); + System.Console.Write(t == (null, null)); + System.Console.Write(t != (null, null)); + System.Console.Write((1, t) == (1, (null, null))); + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "TrueFalseTrue"); + + // PROTOTYPE(tuple-equality) Semantic model: check type on last tuple and its elements (should be typeless) + return; + + //var tree = comp.SyntaxTrees[0]; + //var model = comp.GetSemanticModel(tree); + + //var tuple = tree.GetCompilationUnitRoot().DescendantNodes().OfType().ElementAt(1); + //Assert.Equal("(null, null)", tuple.ToString()); + //var tupleType = model.GetTypeInfo(tuple); + //Assert.Null(tupleType.Type); + //Assert.Equal("(System.String, System.String)", tupleType.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void TestTypedTupleAndDefault() + { + var source = @" +class C +{ + static void Main() + { + (string, string) t = (null, null); + System.Console.Write(t == default); + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics( + // (7,30): error CS8310: Operator '==' cannot be applied to operand 'default' + // System.Console.Write(t == default); + Diagnostic(ErrorCode.ERR_BadOpOnNullOrDefault, "t == default").WithArguments("==", "default").WithLocation(7, 30) + ); + } + + [Fact] + public void TestNullableTupleAndDefault() + { + var source = @" +class C +{ + static void Main() + { + (string, string)? t = (null, null); + System.Console.Write(t == default); + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics( + // (7,30): error CS8310: Operator '==' cannot be applied to operand 'default' + // System.Console.Write(t == default); + Diagnostic(ErrorCode.ERR_BadOpOnNullOrDefault, "t == default").WithArguments("==", "default").WithLocation(7, 30) + ); + } + + [Fact] + public void TestTypedTupleAndTupleOfDefaults() + { + var source = @" +class C +{ + static void Main() + { + (string, string)? t = (null, null); + System.Console.Write(t == (default, default)); + System.Console.Write(t != (default, default)); + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "TrueFalse"); + // PROTOTYPE(tuple-equality) Expand this test + } + + [Fact] + public void TestNullableStructAndDefault() + { + var source = @" +struct S +{ + static void M(string s) + { + S? ns = new S(); + _ = ns == null; + _ = s == null; + _ = ns == default; + _ = (ns, ns) == (default, default); + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (9,13): error CS8310: Operator '==' cannot be applied to operand 'default' + // _ = ns == default; + Diagnostic(ErrorCode.ERR_BadOpOnNullOrDefault, "ns == default").WithArguments("==", "default").WithLocation(9, 13), + // (10,13): error CS8310: Operator '==' cannot be applied to operand 'default' + // _ = (ns, ns) == (default, default); + Diagnostic(ErrorCode.ERR_BadOpOnNullOrDefault, "(ns, ns) == (default, default)").WithArguments("==", "default").WithLocation(10, 13), + // (10,13): error CS8310: Operator '==' cannot be applied to operand 'default' + // _ = (ns, ns) == (default, default); + Diagnostic(ErrorCode.ERR_BadOpOnNullOrDefault, "(ns, ns) == (default, default)").WithArguments("==", "default").WithLocation(10, 13) + ); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree); + var literals = tree.GetCompilationUnitRoot().DescendantNodes().OfType(); + + var nullLiteral = literals.ElementAt(0); + Assert.Equal("null", nullLiteral.ToString()); + Assert.Null(model.GetTypeInfo(nullLiteral).ConvertedType); + + var nullLiteral2 = literals.ElementAt(1); + Assert.Equal("null", nullLiteral2.ToString()); + Assert.Null(model.GetTypeInfo(nullLiteral2).Type); + Assert.Equal("System.String", model.GetTypeInfo(nullLiteral2).ConvertedType.ToTestDisplayString()); + + // PROTOTYPE(tuple-equality) Semantic model + return; + + //var defaultLiteral = literals.ElementAt(2); + //Assert.Equal("default", defaultLiteral.ToString()); + //Assert.Equal("System.Object", model.GetTypeInfo(defaultLiteral).ConvertedType.ToTestDisplayString()); + + //var defaultLiteral2 = literals.ElementAt(3); + //Assert.Equal("default", defaultLiteral2.ToString()); + //Assert.Equal("System.Object", model.GetTypeInfo(defaultLiteral2).ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void TestMixedTupleLiteralsAndTypes() + { + var source = @" +class C +{ + static void Main() + { + (string, string) t = (null, null); + System.Console.Write((t, (null, null)) == ((null, null), t)); + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "True"); + + // PROTOTYPE(tuple-equality) Semantic model: expect nulls to have type string + return; + + //var tree = comp.SyntaxTrees[0]; + //var model = comp.GetSemanticModel(tree); + + //var tuple = tree.GetCompilationUnitRoot().DescendantNodes().OfType().ElementAt(3); + //Assert.Equal("((null, null), t)", tuple.ToString()); + //var tupleType = model.GetTypeInfo(tuple); + //Assert.Null(tupleType.Type); + //Assert.Equal("((System.String, System.String), (System.String, System.String))", + // tupleType.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void TestAllNulls() + { + var source = @" +class C +{ + static void Main() + { + System.Console.Write(null == null); + System.Console.Write((null, null) == (null, null)); + System.Console.Write((null, null) != (null, null)); + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "TrueTrueFalse"); + // PROTOTYPE(tuple-equality) Semantic model: check that null and tuples are typeless + } + + [Fact] + public void TestFailedInference() + { + var source = @" +class C +{ + static void Main() + { + System.Console.Write((null, null, null) == (null, () => { }, Main)); + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (6,30): error CS0019: Operator '==' cannot be applied to operands of type '' and 'lambda expression' + // System.Console.Write((null, null, null) == (null, () => { }, Main)); + Diagnostic(ErrorCode.ERR_BadBinaryOps, "(null, null, null) == (null, () => { }, Main)").WithArguments("==", "", "lambda expression").WithLocation(6, 30), + // (6,30): error CS0019: Operator '==' cannot be applied to operands of type '' and 'method group' + // System.Console.Write((null, null, null) == (null, () => { }, Main)); + Diagnostic(ErrorCode.ERR_BadBinaryOps, "(null, null, null) == (null, () => { }, Main)").WithArguments("==", "", "method group").WithLocation(6, 30) + ); + + // PROTOTYPE(tuple-equality) Semantic model: check that null and tuples are typeless + return; + + //var tree = comp.SyntaxTrees[0]; + //var model = comp.GetSemanticModel(tree); + + //var tuple1 = tree.GetCompilationUnitRoot().DescendantNodes().OfType().ElementAt(0); + //Assert.Equal("(null, null)", tuple1.ToString()); + //var tupleType1 = model.GetTypeInfo(tuple1); + //Assert.Null(tupleType1.Type); + //Assert.Equal("(System.Object, ?)", tupleType1.ConvertedType.ToTestDisplayString()); + + //var tuple2 = tree.GetCompilationUnitRoot().DescendantNodes().OfType().ElementAt(1); + //Assert.Equal("(null, () => { })", tuple2.ToString()); + //var tupleType2 = model.GetTypeInfo(tuple2); + //Assert.Null(tupleType2.Type); + //Assert.Equal("(System.Object, ?)", tupleType2.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void TestFailedConversion() + { + var source = @" +class C +{ + static void M(string s) + { + System.Console.Write((s, s) == (1, () => { })); + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (6,30): error CS0019: Operator '==' cannot be applied to operands of type 'string' and 'int' + // System.Console.Write((s, s) == (1, () => { })); + Diagnostic(ErrorCode.ERR_BadBinaryOps, "(s, s) == (1, () => { })").WithArguments("==", "string", "int").WithLocation(6, 30), + // (6,30): error CS0019: Operator '==' cannot be applied to operands of type 'string' and 'lambda expression' + // System.Console.Write((s, s) == (1, () => { })); + Diagnostic(ErrorCode.ERR_BadBinaryOps, "(s, s) == (1, () => { })").WithArguments("==", "string", "lambda expression").WithLocation(6, 30) + ); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree); + + // PROTOTYPE(tuple-equality) Semantic model + return; + + //var tuple1 = tree.GetCompilationUnitRoot().DescendantNodes().OfType().ElementAt(0); + //Assert.Equal("(s, s)", tuple1.ToString()); + //var tupleType1 = model.GetTypeInfo(tuple1); + //Assert.Equal("(System.String, System.String)", tupleType1.Type.ToTestDisplayString()); + //Assert.Equal("(System.Object, System.String)", tupleType1.ConvertedType.ToTestDisplayString()); + + //var tuple2 = tree.GetCompilationUnitRoot().DescendantNodes().OfType().ElementAt(1); + //Assert.Equal("(1, () => { })", tuple2.ToString()); + //var tupleType2 = model.GetTypeInfo(tuple2); + //Assert.Null(tupleType2.Type); + //Assert.Equal("(System.Object, ?)", tupleType2.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void TestDynamic() + { + var source = @" +public class C +{ + public static void Main() + { + dynamic d1 = 1; + dynamic d2 = 2; + System.Console.Write($""{(d1, 2) == (1, d2)} ""); + System.Console.Write($""{(d1, 2) != (1, d2)} ""); + + System.Console.Write($""{(d1, 20) == (10, d2)} ""); + System.Console.Write($""{(d1, 20) != (10, d2)} ""); + } +}"; + var comp = CreateStandardCompilation(source, references: new[] { ValueTupleRef, SystemRuntimeFacadeRef, CSharpRef, SystemCoreRef }, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "True False False True"); + } + + [Fact] + public void TestDynamicWithConstants() + { + var source = @" +public class C +{ + public static void Main() + { + System.Console.Write($""{((dynamic)true, (dynamic)false) == ((dynamic)true, (dynamic)false)} ""); + } +}"; + var comp = CreateStandardCompilation(source, references: new[] { ValueTupleRef, SystemRuntimeFacadeRef, CSharpRef, SystemCoreRef }, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "True"); + } + + [Fact] + public void TestDynamic_WithTypelessExpression() + { + var source = @" +public class C +{ + public static void Main() + { + dynamic d1 = 1; + dynamic d2 = 2; + System.Console.Write((d1, 2) == (() => 1, d2)); + } +}"; + var comp = CreateStandardCompilation(source, references: new[] { ValueTupleRef, SystemRuntimeFacadeRef, CSharpRef, SystemCoreRef }, options: TestOptions.DebugExe); + comp.VerifyDiagnostics( + // (8,30): error CS0019: Operator '==' cannot be applied to operands of type 'dynamic' and 'lambda expression' + // System.Console.Write((d1, 2) == (() => 1, d2)); + Diagnostic(ErrorCode.ERR_BadBinaryOps, "(d1, 2) == (() => 1, d2)").WithArguments("==", "dynamic", "lambda expression").WithLocation(8, 30) + ); + } + + [Fact] + public void TestDynamic_WithBooleanConstants() + { + var source = @" +public class C +{ + public static void Main() + { + System.Console.Write(((dynamic)true, (dynamic)false) == (true, false)); + System.Console.Write(((dynamic)true, (dynamic)false) != (true, false)); + } +}"; + var comp = CreateStandardCompilation(source, references: new[] { ValueTupleRef, SystemRuntimeFacadeRef, CSharpRef, SystemCoreRef }, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "TrueFalse"); + } + + [Fact] + public void TestDynamic_WithBadType() + { + var source = @" +public class C +{ + public static void Main() + { + dynamic d1 = 1; + dynamic d2 = 2; + try + { + bool b = ((d1, 2) == (""hello"", d2)); + } + catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException e) + { + System.Console.Write(e.Message); + } + } +}"; + var comp = CreateStandardCompilation(source, references: new[] { ValueTupleRef, SystemRuntimeFacadeRef, CSharpRef, SystemCoreRef }, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "Operator '==' cannot be applied to operands of type 'int' and 'string'"); + } + + [Fact] + public void TestDynamic_WithNull() + { + var source = @" +public class C +{ + public static void Main() + { + dynamic d1 = null; + dynamic d2 = null; + System.Console.Write((d1, null) == (null, d2)); + } +}"; + var comp = CreateStandardCompilation(source, references: new[] { ValueTupleRef, SystemRuntimeFacadeRef, CSharpRef, SystemCoreRef }, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "True"); + // PROTOTYPE(tuple-equality) verify converted type on null + } + + [Fact] + public void TestBadConstraintOnTuple() + { + var source = @" +ref struct S +{ + void M(S s1, S s2) + { + System.Console.Write(("""", s1) == (null, s2)); + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (6,35): error CS0306: The type 'S' may not be used as a type argument + // System.Console.Write(("", s1) == (null, s2)); + Diagnostic(ErrorCode.ERR_BadTypeArgument, "s1").WithArguments("S").WithLocation(6, 35), + // (6,30): error CS0019: Operator '==' cannot be applied to operands of type 'S' and 'S' + // System.Console.Write(("", s1) == (null, s2)); + Diagnostic(ErrorCode.ERR_BadBinaryOps, @"("""", s1) == (null, s2)").WithArguments("==", "S", "S").WithLocation(6, 30) + ); + } + + [Fact] + public void TestErrorInTuple() + { + var source = @" +public class C +{ + public void M() + { + if (error1 == (error2, 3)) { } + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (6,13): error CS0103: The name 'error1' does not exist in the current context + // if (error1 == (error2, 3)) { } + Diagnostic(ErrorCode.ERR_NameNotInContext, "error1").WithArguments("error1").WithLocation(6, 13), + // (6,24): error CS0103: The name 'error2' does not exist in the current context + // if (error1 == (error2, 3)) { } + Diagnostic(ErrorCode.ERR_NameNotInContext, "error2").WithArguments("error2").WithLocation(6, 24) + ); + } + + [Fact] + public void TestWithTypelessTuple() + { + var source = @" +public class C +{ + public void M() + { + var t = (null, null); + if (null == (() => {}) ) {} + if ("""" == 1) {} + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (6,13): error CS0815: Cannot assign (, ) to an implicitly-typed variable + // var t = (null, null); + Diagnostic(ErrorCode.ERR_ImplicitlyTypedVariableAssignedBadValue, "t = (null, null)").WithArguments("(, )").WithLocation(6, 13), + // (7,13): error CS0019: Operator '==' cannot be applied to operands of type '' and 'lambda expression' + // if (null == (() => {}) ) {} + Diagnostic(ErrorCode.ERR_BadBinaryOps, "null == (() => {})").WithArguments("==", "", "lambda expression").WithLocation(7, 13), + // (8,13): error CS0019: Operator '==' cannot be applied to operands of type 'string' and 'int' + // if ("" == 1) {} + Diagnostic(ErrorCode.ERR_BadBinaryOps, @""""" == 1").WithArguments("==", "string", "int").WithLocation(8, 13) + ); + } + + [Fact] + public void TestCustomOperatorPreferred() + { + var source = @" +namespace System +{ + public struct ValueTuple + { + public T1 Item1; + public T2 Item2; + + public ValueTuple(T1 item1, T2 item2) + { + this.Item1 = item1; + this.Item2 = item2; + } + + public static bool operator ==(ValueTuple t1, ValueTuple t2) + => throw null; + public static bool operator !=(ValueTuple t1, ValueTuple t2) + => throw null; + + public override bool Equals(object o) + => throw null; + public override int GetHashCode() + => throw null; + } +} +public class C +{ + public static void Main() + { + var t1 = (1, 1); + var t2 = (2, 2); + System.Console.Write(t1 == t2); + System.Console.Write(t1 != t2); + } +} +"; + var comp = CreateStandardCompilation(source, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + // Note: tuple equality picked ahead of custom operator== (small compat break) + CompileAndVerify(comp, expectedOutput: "FalseTrue"); + } + + [Fact] + void TestTupleEqualityPreferredOverCustomOperator_Nested() + { + string source = @" +public class C +{ + public static void Main() + { + System.Console.Write( (1, 2, (3, 4)) == (1, 2, (3, 4)) ); + } +} +namespace System +{ + public struct ValueTuple + { + public T1 Item1; + public T2 Item2; + + public ValueTuple(T1 item1, T2 item2) + { + this.Item1 = item1; + this.Item2 = item2; + } + + public static bool operator ==(ValueTuple t1, ValueTuple t2) + => throw null; + public static bool operator !=(ValueTuple t1, ValueTuple t2) + => throw null; + + public override bool Equals(object o) + => throw null; + + public override int GetHashCode() + => throw null; + } + public struct ValueTuple + { + public T1 Item1; + public T2 Item2; + public T3 Item3; + + public ValueTuple(T1 item1, T2 item2, T3 item3) + { + this.Item1 = item1; + this.Item2 = item2; + this.Item3 = item3; + } + } +} +"; + + var comp = CreateStandardCompilation(source, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + + // Note: tuple equality picked ahead of custom operator== + CompileAndVerify(comp, expectedOutput: "True"); + } + + [Fact] + public void TestNaN() + { + var source = @" +public class C +{ + public static void Main() + { + var t1 = (System.Double.NaN, 1); + var t2 = (System.Double.NaN, 1); + System.Console.Write($""{t1 == t2} {t1.Equals(t2)} {t1 != t2} {t1 == (System.Double.NaN, 1)}""); + } +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "False True True False"); + } + + [Fact] + public void TestTopLevelDynamic() + { + var source = @" +public class C +{ + public static void Main() + { + dynamic d1 = (1, 1); + + try + { + _ = d1 == (1, 1); + } + catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException e) + { + System.Console.WriteLine(e.Message); + } + + try + { + _ = d1 != (1, 2); + } + catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException e) + { + System.Console.WriteLine(e.Message); + } + } +} +"; + var comp = CreateStandardCompilation(source, options: TestOptions.DebugExe, + references: new[] { ValueTupleRef, SystemRuntimeFacadeRef, CSharpRef, SystemCoreRef }); + comp.VerifyDiagnostics(); + + CompileAndVerify(comp, expectedOutput: +@"Operator '==' cannot be applied to operands of type 'System.ValueTuple' and 'System.ValueTuple' +Operator '!=' cannot be applied to operands of type 'System.ValueTuple' and 'System.ValueTuple'"); + } + + [Fact] + public void TestNestedDynamic() + { + var source = @" +public class C +{ + public static void Main() + { + dynamic d1 = (1, 1, 1); + + try + { + _ = (2, d1) == (2, (1, 1, 1)); + } + catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException e) + { + System.Console.WriteLine(e.Message); + } + + try + { + _ = (3, d1) != (3, (1, 2, 3)); + } + catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException e) + { + System.Console.WriteLine(e.Message); + } + } +} +"; + var comp = CreateStandardCompilation(source, options: TestOptions.DebugExe, + references: new[] { ValueTupleRef, SystemRuntimeFacadeRef, CSharpRef, SystemCoreRef }); + comp.VerifyDiagnostics(); + + CompileAndVerify(comp, expectedOutput: +@"Operator '==' cannot be applied to operands of type 'System.ValueTuple' and 'System.ValueTuple' +Operator '!=' cannot be applied to operands of type 'System.ValueTuple' and 'System.ValueTuple'"); + } + + [Fact] + public void TestComparisonWithDeconstructionResult() + { + var source = @" +public class C +{ + public static void Main() + { + var b1 = (1, 2) == ((_, _) = new C()); + var b2 = (1, 42) != ((_, _) = new C()); + var b3 = (1, 42) == ((_, _) = new C()); // false + var b4 = ((_, _) = new C()) == (1, 2); + var b5 = ((_, _) = new C()) != (1, 42); + var b6 = ((_, _) = new C()) == (1, 42); // false + System.Console.Write($""{b1} {b2} {b3} {b4} {b5} {b6}""); + } + public void Deconstruct(out int x, out int y) + { + x = 1; + y = 2; + } +} +"; + var comp = CreateStandardCompilation(source, options: TestOptions.DebugExe, references: s_valueTupleRefs); + comp.VerifyDiagnostics(); + + CompileAndVerify(comp, expectedOutput: @"True True False True True False"); + } + + [Fact] + public void TestComparisonWithDeconstruction() + { + var source = @" +public class C +{ + public static void Main() + { + var b1 = (1, 2) == new C(); + } + public void Deconstruct(out int x, out int y) + { + x = 1; + y = 2; + } +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (6,18): error CS0019: Operator '==' cannot be applied to operands of type '(int, int)' and 'C' + // var b1 = (1, 2) == new C(); + Diagnostic(ErrorCode.ERR_BadBinaryOps, "(1, 2) == new C()").WithArguments("==", "(int, int)", "C").WithLocation(6, 18) + ); + } + + [Fact] + public void TestEvaluationOrderOnTupleLiteral() + { + var source = @" +public class C +{ + public static void Main() + { + System.Console.Write($""{EXPRESSION}""); + } +} +namespace System +{ + public struct ValueTuple + { + public T1 Item1; + public T2 Item2; + + public ValueTuple(T1 item1, T2 item2) + { + this.Item1 = item1; + this.Item2 = item2; + throw null; + } + } +} +public class Base +{ + public int I; + public Base(int i) { I = i; } +} +public class A : Base +{ + public A(int i) : base(i) + { + System.Console.Write($""A:{i}, ""); + } + public static bool operator ==(A a, Y y) + { + System.Console.Write($""A({a.I}) == Y({y.I}), ""); + return a.I == y.I; + } + public static bool operator !=(A a, Y y) + { + System.Console.Write($""A({a.I}) != Y({y.I}), ""); + return a.I != y.I; + } + public override bool Equals(object o) + => throw null; + public override int GetHashCode() + => throw null; +} +public class X : Base +{ + public X(int i) : base(i) + { + System.Console.Write($""X:{i}, ""); + } +} +public class Y : Base +{ + public Y(int i) : base(i) + { + System.Console.Write($""Y:{i}, ""); + } + public static implicit operator Y(X x) + { + System.Console.Write(""X -> ""); + return new Y(x.I); + } +} +"; + + validate("(new A(1), new A(2)) == (new X(1), new Y(2))", "A:1, A:2, X:1, Y:2, X -> Y:1, A(1) == Y(1), A(2) == Y(2), True"); + validate("(new A(1), new A(2)) == (new X(30), new Y(40))", "A:1, A:2, X:30, Y:40, X -> Y:30, A(1) == Y(30), False"); + validate("(new A(1), new A(2)) == (new X(1), new Y(50))", "A:1, A:2, X:1, Y:50, X -> Y:1, A(1) == Y(1), A(2) == Y(50), False"); + + validate("(new A(1), new A(2)) != (new X(1), new Y(2))", "A:1, A:2, X:1, Y:2, X -> Y:1, A(1) != Y(1), A(2) != Y(2), False"); + validate("(new A(1), new A(2)) != (new Y(1), new X(2))", "A:1, A:2, Y:1, X:2, A(1) != Y(1), X -> Y:2, A(2) != Y(2), False"); + // PROTOTYPE(tuple-equality) test case where conversion is on last tuple element on the left side + + validate("(new A(1), new A(2)) != (new X(30), new Y(40))", "A:1, A:2, X:30, Y:40, X -> Y:30, A(1) != Y(30), True"); + validate("(new A(1), new A(2)) != (new X(50), new Y(2))", "A:1, A:2, X:50, Y:2, X -> Y:50, A(1) != Y(50), True"); + validate("(new A(1), new A(2)) != (new X(1), new Y(60))", "A:1, A:2, X:1, Y:60, X -> Y:1, A(1) != Y(1), A(2) != Y(60), True"); + + void validate(string expression, string expected) + { + var comp = CreateStandardCompilation(source.Replace("EXPRESSION", expression), options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: expected); + } + } + + [Fact] + public void TestEvaluationOrderOnTupleType() + { + var source = @" +namespace System +{ + public struct ValueTuple + { + public T1 Item1; + public T2 Item2; + + public ValueTuple(T1 item1, T2 item2) + { + System.Console.Write(""ValueTuple2, ""); + this.Item1 = item1; + this.Item2 = item2; + } + } + public struct ValueTuple + { + public ValueTuple(T1 item1, T2 item2, T3 item3) + { + // ValueTuple'3 required (constructed in bound tree), but not emitted + throw null; + } + } +} +public class Base +{ + public int I; + public Base(int i) { I = i; } +} +public class A : Base +{ + public A(int i) : base(i) + { + System.Console.Write($""A:{i}, ""); + } + public static bool operator ==(A a, Y y) + { + System.Console.Write($""A({a.I}) == Y({y.I}), ""); + return true; + } + public static bool operator !=(A a, Y y) + => throw null; + public override bool Equals(object o) + => throw null; + public override int GetHashCode() + => throw null; +} +public class X : Base +{ + public X(int i) : base(i) + { + System.Console.Write($""X:{i}, ""); + } +} +public class Y : Base +{ + public Y(int i) : base(i) + { + System.Console.Write($""Y:{i}, ""); + } + public static implicit operator Y(X x) + { + System.Console.Write(""X -> ""); + return new Y(x.I); + } +} +public class C +{ + public static void Main() + { + System.Console.Write($""{(new A(1), GetTuple(), new A(4)) == (new X(5), (new X(6), new Y(7)), new Y(8))}""); + } + public static (A, A) GetTuple() + { + System.Console.Write($""GetTuple, ""); + return (new A(30), new A(40)); + } +} +"; + var comp = CreateStandardCompilation(source, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "A:1, GetTuple, A:30, A:40, ValueTuple2, A:4, X:5, X:6, Y:7, Y:8, X -> Y:5, A(1) == Y(5), X -> Y:6, A(30) == Y(6), A(40) == Y(7), A(4) == Y(8), True"); + } + + [Fact] + public void TestEvaluationOrderOnTupleType2() + { + var source = @" +namespace System +{ + public struct ValueTuple + { + public T1 Item1; + public T2 Item2; + + public ValueTuple(T1 item1, T2 item2) + { + System.Console.Write(""ValueTuple2, ""); + this.Item1 = item1; + this.Item2 = item2; + } + } + public struct ValueTuple + { + public ValueTuple(T1 item1, T2 item2, T3 item3) + { + // ValueTuple'3 required (constructed in bound tree), but not emitted + throw null; + } + } +} +public class Base +{ + public int I; + public Base(int i) { I = i; } +} +public class A : Base +{ + public A(int i) : base(i) + { + System.Console.Write($""A:{i}, ""); + } + public static bool operator ==(A a, Y y) + { + System.Console.Write($""A({a.I}) == Y({y.I}), ""); + return true; + } + public static bool operator !=(A a, Y y) + => throw null; + public override bool Equals(object o) + => throw null; + public override int GetHashCode() + => throw null; +} +public class X : Base +{ + public X(int i) : base(i) + { + System.Console.Write($""X:{i}, ""); + } +} +public class Y : Base +{ + public Y(int i) : base(i) + { + System.Console.Write($""Y:{i}, ""); + } + public static implicit operator Y(X x) + { + System.Console.Write($""X:{x.I} -> ""); + return new Y(x.I); + } +} +public class C +{ + public static void Main() + { + System.Console.Write($""{(new A(1), (new A(2), new A(3)), new A(4)) == (new X(5), GetTuple(), new Y(8))}""); + } + public static (X, Y) GetTuple() + { + System.Console.Write($""GetTuple, ""); + return (new X(6), new Y(7)); + } +} +"; + var comp = CreateStandardCompilation(source, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: @"A:1, A:2, A:3, A:4, X:5, GetTuple, X:6, Y:7, ValueTuple2, Y:8, X:5 -> Y:5, A(1) == Y(5), X:6 -> Y:6, A(2) == Y(6), A(3) == Y(7), A(4) == Y(8), True"); + } + + [Fact] + public void TestEvaluationOrderOnTupleType3() + { + var source = @" +namespace System +{ + public struct ValueTuple + { + public T1 Item1; + public T2 Item2; + + public ValueTuple(T1 item1, T2 item2) + { + System.Console.Write(""ValueTuple2, ""); + this.Item1 = item1; + this.Item2 = item2; + } + } + public struct ValueTuple + { + public ValueTuple(T1 item1, T2 item2, T3 item3) + { + // ValueTuple'3 required (constructed in bound tree), but not emitted + throw null; + } + } +} +public class Base +{ + public int I; + public Base(int i) { I = i; } +} +public class A : Base +{ + public A(int i) : base(i) + { + System.Console.Write($""A:{i}, ""); + } + public static bool operator ==(A a, Y y) + { + System.Console.Write($""A({a.I}) == Y({y.I}), ""); + return true; + } + public static bool operator !=(A a, Y y) + => throw null; + public override bool Equals(object o) + => throw null; + public override int GetHashCode() + => throw null; +} +public class X : Base +{ + public X(int i) : base(i) + { + System.Console.Write($""X:{i}, ""); + } +} +public class Y : Base +{ + public Y(int i) : base(i) + { + System.Console.Write($""Y:{i}, ""); + } + public static implicit operator Y(X x) + { + System.Console.Write(""X -> ""); + return new Y(x.I); + } +} +public class C +{ + public static void Main() + { + System.Console.Write($""{GetTuple() == (new X(6), new Y(7))}""); + } + public static (A, A) GetTuple() + { + System.Console.Write($""GetTuple, ""); + return (new A(30), new A(40)); + } +} +"; + var comp = CreateStandardCompilation(source, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "GetTuple, A:30, A:40, ValueTuple2, X:6, Y:7, X -> Y:6, A(30) == Y(6), A(40) == Y(7), True"); + } + + [Fact] + public void TestObsoleteEqualityOperator() + { + var source = @" +class C +{ + void M() + { + System.Console.WriteLine($""{(new A(), new A()) == (new X(), new Y())}""); + System.Console.WriteLine($""{(new A(), new A()) != (new X(), new Y())}""); + } +} +public class A +{ + [System.Obsolete(""obsolete"", true)] + public static bool operator ==(A a, Y y) + => throw null; + [System.Obsolete(""obsolete too"", true)] + public static bool operator !=(A a, Y y) + => throw null; + public override bool Equals(object o) + => throw null; + public override int GetHashCode() + => throw null; +} +public class X +{ +} +public class Y +{ + public static implicit operator Y(X x) + => throw null; +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (6,37): error CS0619: 'A.operator ==(A, Y)' is obsolete: 'obsolete' + // System.Console.WriteLine($"{(new A(), new A()) == (new X(), new Y())}"); + Diagnostic(ErrorCode.ERR_DeprecatedSymbolStr, "(new A(), new A()) == (new X(), new Y())").WithArguments("A.operator ==(A, Y)", "obsolete").WithLocation(6, 37), + // (6,37): error CS0619: 'A.operator ==(A, Y)' is obsolete: 'obsolete' + // System.Console.WriteLine($"{(new A(), new A()) == (new X(), new Y())}"); + Diagnostic(ErrorCode.ERR_DeprecatedSymbolStr, "(new A(), new A()) == (new X(), new Y())").WithArguments("A.operator ==(A, Y)", "obsolete").WithLocation(6, 37), + // (7,37): error CS0619: 'A.operator !=(A, Y)' is obsolete: 'obsolete too' + // System.Console.WriteLine($"{(new A(), new A()) != (new X(), new Y())}"); + Diagnostic(ErrorCode.ERR_DeprecatedSymbolStr, "(new A(), new A()) != (new X(), new Y())").WithArguments("A.operator !=(A, Y)", "obsolete too").WithLocation(7, 37), + // (7,37): error CS0619: 'A.operator !=(A, Y)' is obsolete: 'obsolete too' + // System.Console.WriteLine($"{(new A(), new A()) != (new X(), new Y())}"); + Diagnostic(ErrorCode.ERR_DeprecatedSymbolStr, "(new A(), new A()) != (new X(), new Y())").WithArguments("A.operator !=(A, Y)", "obsolete too").WithLocation(7, 37) + ); + } + + [Fact] + public void TestDefiniteAssignment() + { + var source = @" +class C +{ + void M() + { + int error1; + System.Console.Write((1, 2) == (error1, 2)); + + int error2; + System.Console.Write((1, (error2, 3)) == (1, (2, 3))); + } +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (7,41): error CS0165: Use of unassigned local variable 'error1' + // System.Console.Write((1, 2) == (error1, 2)); + Diagnostic(ErrorCode.ERR_UseDefViolation, "error1").WithArguments("error1").WithLocation(7, 41), + // (10,35): error CS0165: Use of unassigned local variable 'error2' + // System.Console.Write((1, (error2, 3)) == (1, (2, 3))); + Diagnostic(ErrorCode.ERR_UseDefViolation, "error2").WithArguments("error2").WithLocation(10, 35) + ); + } + + [Fact] + public void TestEqualityOfTypeConvertingToTuple() + { + var source = @" +class C +{ + private int i; + void M() + { + System.Console.Write(this == (1, 1)); + System.Console.Write((1, 1) == this); + } + public static implicit operator (int, int)(C c) + { + return (c.i, c.i); + } + C(int i) { this.i = i; } +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (7,30): error CS0019: Operator '==' cannot be applied to operands of type 'C' and '(int, int)' + // System.Console.Write(this == (1, 1)); + Diagnostic(ErrorCode.ERR_BadBinaryOps, "this == (1, 1)").WithArguments("==", "C", "(int, int)").WithLocation(7, 30), + // (8,30): error CS0019: Operator '==' cannot be applied to operands of type '(int, int)' and 'C' + // System.Console.Write((1, 1) == this); + Diagnostic(ErrorCode.ERR_BadBinaryOps, "(1, 1) == this").WithArguments("==", "(int, int)", "C").WithLocation(8, 30) + ); + } + + [Fact] + public void TestEqualityOfTypeConvertingFromTuple() + { + var source = @" +class C +{ + private int i; + public static void Main() + { + var c = new C(2); + System.Console.Write(c == (1, 1)); + System.Console.Write((1, 1) == c); + } + public static implicit operator C((int, int) x) + { + return new C(x.Item1 + x.Item2); + } + public static bool operator ==(C c1, C c2) + => c1.i == c2.i; + public static bool operator !=(C c1, C c2) + => throw null; + public override int GetHashCode() + => throw null; + public override bool Equals(object other) + => throw null; + C(int i) { this.i = i; } +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "TrueTrue"); + } + + [Fact] + public void TestEqualityOfTypeComparableWithTuple() + { + var source = @" +class C +{ + private static void Main() + { + System.Console.Write(new C() == (1, 1)); + System.Console.Write(new C() != (1, 1)); + } + public static bool operator ==(C c, (int, int) t) + { + return t.Item1 + t.Item2 == 2; + } + public static bool operator !=(C c, (int, int) t) + { + return t.Item1 + t.Item2 != 2; + } + public override bool Equals(object o) + => throw null; + public override int GetHashCode() + => throw null; +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "TrueFalse"); + } + + [Fact] + public void TestOfTwoUnrelatedTypes() + { + var source = @" +class A { } +class C +{ + static void M() + { + System.Console.Write(new C() == new A()); + System.Console.Write((1, new C()) == (1, new A())); + } +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (7,30): error CS0019: Operator '==' cannot be applied to operands of type 'C' and 'A' + // System.Console.Write(new C() == new A()); + Diagnostic(ErrorCode.ERR_BadBinaryOps, "new C() == new A()").WithArguments("==", "C", "A").WithLocation(7, 30), + // (8,30): error CS0019: Operator '==' cannot be applied to operands of type 'C' and 'A' + // System.Console.Write((1, new C()) == (1, new A())); + Diagnostic(ErrorCode.ERR_BadBinaryOps, "(1, new C()) == (1, new A())").WithArguments("==", "C", "A").WithLocation(8, 30) + ); + } + + [Fact] + public void TestOfTwoUnrelatedTypes2() + { + var source = @" +class A { } +class C +{ + static void M(string s, System.Exception e) + { + System.Console.Write(s == 3); + System.Console.Write((1, s) == (1, e)); + } +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (7,30): error CS0019: Operator '==' cannot be applied to operands of type 'string' and 'int' + // System.Console.Write(s == 3); + Diagnostic(ErrorCode.ERR_BadBinaryOps, "s == 3").WithArguments("==", "string", "int").WithLocation(7, 30), + // (8,30): error CS0019: Operator '==' cannot be applied to operands of type 'string' and 'Exception' + // System.Console.Write((1, s) == (1, e)); + Diagnostic(ErrorCode.ERR_BadBinaryOps, "(1, s) == (1, e)").WithArguments("==", "string", "System.Exception").WithLocation(8, 30) + ); + } + + [Fact] + public void TestBadRefCompare() + { + var source = @" +class C +{ + static void M() + { + string s = ""11""; + object o = s + s; + (object, object) t = default; + + bool b = o == s; + bool b2 = t == (s, s); // no warning + } +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (10,18): warning CS0252: Possible unintended reference comparison; to get a value comparison, cast the left hand side to type 'string' + // bool b = o == s; + Diagnostic(ErrorCode.WRN_BadRefCompareLeft, "o == s").WithArguments("string").WithLocation(10, 18) + ); + } + + [Fact] + public void TestEqualOnNullableVsNullableTuples() + { + var source = @" +class C +{ + public static void Main() + { + Compare(null, null); + Compare(null, (1, 2)); + Compare((2, 3), null); + Compare((4, 4), (4, 4)); + Compare((5, 5), (10, 10)); + } + private static void Compare((int, int)? nt1, (int, int)? nt2) + { + System.Console.Write($""{nt1 == nt2} ""); + } +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + var verifier = CompileAndVerify(comp, expectedOutput: "True False False True False"); + + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree); + + var comparison = tree.GetCompilationUnitRoot().DescendantNodes().OfType().Single(); + var nt1 = comparison.Left; + var nt1Type = model.GetTypeInfo(nt1); + Assert.Equal("nt1", nt1.ToString()); + Assert.Equal("(System.Int32, System.Int32)?", nt1Type.Type.ToTestDisplayString()); + Assert.Equal("(System.Int32, System.Int32)?", nt1Type.ConvertedType.ToTestDisplayString()); + + var nt2 = comparison.Right; + var nt2Type = model.GetTypeInfo(nt2); + Assert.Equal("nt2", nt2.ToString()); + Assert.Equal("(System.Int32, System.Int32)?", nt2Type.Type.ToTestDisplayString()); + Assert.Equal("(System.Int32, System.Int32)?", nt2Type.ConvertedType.ToTestDisplayString()); + + verifier.VerifyIL("C.Compare", @"{ + // Code size 104 (0x68) + .maxstack 3 + .locals init ((int, int)? V_0, + (int, int)? V_1, + bool V_2, + System.ValueTuple V_3, + System.ValueTuple V_4) + IL_0000: nop + IL_0001: ldstr ""{0} "" + IL_0006: ldarg.0 + IL_0007: stloc.0 + IL_0008: ldarg.1 + IL_0009: stloc.1 + IL_000a: ldloca.s V_0 + IL_000c: call ""bool (int, int)?.HasValue.get"" + IL_0011: stloc.2 + IL_0012: ldloc.2 + IL_0013: ldloca.s V_1 + IL_0015: call ""bool (int, int)?.HasValue.get"" + IL_001a: beq.s IL_001f + IL_001c: ldc.i4.0 + IL_001d: br.s IL_0057 + IL_001f: ldloc.2 + IL_0020: brtrue.s IL_0025 + IL_0022: ldc.i4.1 + IL_0023: br.s IL_0057 + IL_0025: ldloca.s V_0 + IL_0027: call ""(int, int) (int, int)?.GetValueOrDefault()"" + IL_002c: stloc.3 + IL_002d: ldloca.s V_1 + IL_002f: call ""(int, int) (int, int)?.GetValueOrDefault()"" + IL_0034: stloc.s V_4 + IL_0036: ldloc.3 + IL_0037: ldfld ""int System.ValueTuple.Item1"" + IL_003c: ldloc.s V_4 + IL_003e: ldfld ""int System.ValueTuple.Item1"" + IL_0043: bne.un.s IL_0056 + IL_0045: ldloc.3 + IL_0046: ldfld ""int System.ValueTuple.Item2"" + IL_004b: ldloc.s V_4 + IL_004d: ldfld ""int System.ValueTuple.Item2"" + IL_0052: ceq + IL_0054: br.s IL_0057 + IL_0056: ldc.i4.0 + IL_0057: box ""bool"" + IL_005c: call ""string string.Format(string, object)"" + IL_0061: call ""void System.Console.Write(string)"" + IL_0066: nop + IL_0067: ret +}"); + } + + [Fact] + public void TestEqualOnNullableVsNullableTuples_NeverNull() + { + var source = @" +class C +{ + public static void Main() + { + System.Console.Write(((int, int)?) (1, 2) == ((int, int)?) (1, 2)); + } +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "True"); + } + + [Fact] + public void TestNotEqualOnNullableVsNullableTuples() + { + var source = @" +class C +{ + public static void Main() + { + Compare(null, null); + Compare(null, (1, 2)); + Compare((2, 3), null); + Compare((4, 4), (4, 4)); + Compare((5, 5), (10, 10)); + } + private static void Compare((int, int)? nt1, (int, int)? nt2) + { + System.Console.Write($""{nt1 != nt2} ""); + } +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + var verifier = CompileAndVerify(comp, expectedOutput: "False True True False True"); + + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree); + + var comparison = tree.GetCompilationUnitRoot().DescendantNodes().OfType().Single(); + var nt1 = comparison.Left; + var nt1Type = model.GetTypeInfo(nt1); + Assert.Equal("nt1", nt1.ToString()); + Assert.Equal("(System.Int32, System.Int32)?", nt1Type.Type.ToTestDisplayString()); + Assert.Equal("(System.Int32, System.Int32)?", nt1Type.ConvertedType.ToTestDisplayString()); + + var nt2 = comparison.Right; + var nt2Type = model.GetTypeInfo(nt2); + Assert.Equal("nt2", nt2.ToString()); + Assert.Equal("(System.Int32, System.Int32)?", nt2Type.Type.ToTestDisplayString()); + Assert.Equal("(System.Int32, System.Int32)?", nt2Type.ConvertedType.ToTestDisplayString()); + + verifier.VerifyIL("C.Compare", @"{ + // Code size 107 (0x6b) + .maxstack 3 + .locals init ((int, int)? V_0, + (int, int)? V_1, + bool V_2, + System.ValueTuple V_3, + System.ValueTuple V_4) + IL_0000: nop + IL_0001: ldstr ""{0} "" + IL_0006: ldarg.0 + IL_0007: stloc.0 + IL_0008: ldarg.1 + IL_0009: stloc.1 + IL_000a: ldloca.s V_0 + IL_000c: call ""bool (int, int)?.HasValue.get"" + IL_0011: stloc.2 + IL_0012: ldloc.2 + IL_0013: ldloca.s V_1 + IL_0015: call ""bool (int, int)?.HasValue.get"" + IL_001a: beq.s IL_001f + IL_001c: ldc.i4.1 + IL_001d: br.s IL_005a + IL_001f: ldloc.2 + IL_0020: brtrue.s IL_0025 + IL_0022: ldc.i4.0 + IL_0023: br.s IL_005a + IL_0025: ldloca.s V_0 + IL_0027: call ""(int, int) (int, int)?.GetValueOrDefault()"" + IL_002c: stloc.3 + IL_002d: ldloca.s V_1 + IL_002f: call ""(int, int) (int, int)?.GetValueOrDefault()"" + IL_0034: stloc.s V_4 + IL_0036: ldloc.3 + IL_0037: ldfld ""int System.ValueTuple.Item1"" + IL_003c: ldloc.s V_4 + IL_003e: ldfld ""int System.ValueTuple.Item1"" + IL_0043: bne.un.s IL_0059 + IL_0045: ldloc.3 + IL_0046: ldfld ""int System.ValueTuple.Item2"" + IL_004b: ldloc.s V_4 + IL_004d: ldfld ""int System.ValueTuple.Item2"" + IL_0052: ceq + IL_0054: ldc.i4.0 + IL_0055: ceq + IL_0057: br.s IL_005a + IL_0059: ldc.i4.1 + IL_005a: box ""bool"" + IL_005f: call ""string string.Format(string, object)"" + IL_0064: call ""void System.Console.Write(string)"" + IL_0069: nop + IL_006a: ret +}"); + } + + [Fact] + public void TestNotEqualOnNullableVsNullableNestedTuples() + { + var source = @" +class C +{ + public static void Main() + { + Compare((1, null), (1, null), true); + Compare(null, (1, (2, 3)), false); + Compare((1, (2, 3)), (1, null), false); + Compare((1, (4, 4)), (1, (4, 4)), true); + Compare((1, (5, 5)), (1, (10, 10)), false); + System.Console.Write(""Success""); + } + private static void Compare((int, (int, int)?)? nt1, (int, (int, int)?)? nt2, bool expectMatch) + { + if (expectMatch != (nt1 == nt2) || expectMatch == (nt1 != nt2)) + { + throw null; + } + } +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "Success"); + } + + [Fact] + public void TestEqualOnNullableVsNullableTuples_WithImplicitConversion() + { + var source = @" +class C +{ + public static void Main() + { + Compare(null, null); + Compare(null, (1, 2)); + Compare((2, 3), null); + Compare((4, 4), (4, 4)); + Compare((5, 5), (10, 10)); + } + private static void Compare((int, int)? nt1, (byte, long)? nt2) + { + System.Console.Write($""{nt1 == nt2} ""); + } +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + var verifier = CompileAndVerify(comp, expectedOutput: "True False False True False"); + + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree); + + // PROTOTYPE(tuple-equality) Semantic model + return; + +// var comparison = tree.GetCompilationUnitRoot().DescendantNodes().OfType().Single(); +// var nt1 = comparison.Left; +// var nt1Type = model.GetTypeInfo(nt1); +// Assert.Equal("nt1", nt1.ToString()); +// Assert.Equal("(System.Int32, System.Int32)?", nt1Type.Type.ToTestDisplayString()); +// Assert.Equal("(System.Int32, System.Int64)?", nt1Type.ConvertedType.ToTestDisplayString()); + +// var nt2 = comparison.Right; +// var nt2Type = model.GetTypeInfo(nt2); +// Assert.Equal("nt2", nt2.ToString()); +// Assert.Equal("(System.Byte, System.Int64)?", nt2Type.Type.ToTestDisplayString()); +// Assert.Equal("(System.Int32, System.Int64)?", nt2Type.ConvertedType.ToTestDisplayString()); + +// verifier.VerifyIL("C.Compare", @"{ +// // Code size 217 (0xd9) +// .maxstack 3 +// .locals init ((int, long)? V_0, +// bool V_1, +// System.ValueTuple V_2, +// (int, long)? V_3, +// System.ValueTuple V_4, +// (int, int)? V_5, +// (int, long)? V_6, +// System.ValueTuple V_7, +// (byte, long)? V_8, +// System.ValueTuple V_9) +// IL_0000: nop +// IL_0001: ldstr ""{0} "" +// IL_0006: ldarg.0 +// IL_0007: stloc.s V_5 +// IL_0009: ldloca.s V_5 +// IL_000b: call ""bool (int, int)?.HasValue.get"" +// IL_0010: brtrue.s IL_001e +// IL_0012: ldloca.s V_6 +// IL_0014: initobj ""(int, long)?"" +// IL_001a: ldloc.s V_6 +// IL_001c: br.s IL_0040 +// IL_001e: ldloca.s V_5 +// IL_0020: call ""(int, int) (int, int)?.GetValueOrDefault()"" +// IL_0025: stloc.s V_7 +// IL_0027: ldloc.s V_7 +// IL_0029: ldfld ""int System.ValueTuple.Item1"" +// IL_002e: ldloc.s V_7 +// IL_0030: ldfld ""int System.ValueTuple.Item2"" +// IL_0035: conv.i8 +// IL_0036: newobj ""System.ValueTuple..ctor(int, long)"" +// IL_003b: newobj ""(int, long)?..ctor((int, long))"" +// IL_0040: stloc.0 +// IL_0041: ldloca.s V_0 +// IL_0043: call ""bool (int, long)?.HasValue.get"" +// IL_0048: stloc.1 +// IL_0049: ldarg.1 +// IL_004a: stloc.s V_8 +// IL_004c: ldloca.s V_8 +// IL_004e: call ""bool (byte, long)?.HasValue.get"" +// IL_0053: brtrue.s IL_0061 +// IL_0055: ldloca.s V_6 +// IL_0057: initobj ""(int, long)?"" +// IL_005d: ldloc.s V_6 +// IL_005f: br.s IL_0082 +// IL_0061: ldloca.s V_8 +// IL_0063: call ""(byte, long) (byte, long)?.GetValueOrDefault()"" +// IL_0068: stloc.s V_9 +// IL_006a: ldloc.s V_9 +// IL_006c: ldfld ""byte System.ValueTuple.Item1"" +// IL_0071: ldloc.s V_9 +// IL_0073: ldfld ""long System.ValueTuple.Item2"" +// IL_0078: newobj ""System.ValueTuple..ctor(int, long)"" +// IL_007d: newobj ""(int, long)?..ctor((int, long))"" +// IL_0082: stloc.3 +// IL_0083: ldloc.1 +// IL_0084: ldloca.s V_3 +// IL_0086: call ""bool (int, long)?.HasValue.get"" +// IL_008b: beq.s IL_0090 +// IL_008d: ldc.i4.0 +// IL_008e: br.s IL_00c8 +// IL_0090: ldloc.1 +// IL_0091: brtrue.s IL_0096 +// IL_0093: ldc.i4.1 +// IL_0094: br.s IL_00c8 +// IL_0096: ldloca.s V_0 +// IL_0098: call ""(int, long) (int, long)?.GetValueOrDefault()"" +// IL_009d: stloc.2 +// IL_009e: ldloca.s V_3 +// IL_00a0: call ""(int, long) (int, long)?.GetValueOrDefault()"" +// IL_00a5: stloc.s V_4 +// IL_00a7: ldloc.2 +// IL_00a8: ldfld ""int System.ValueTuple.Item1"" +// IL_00ad: ldloc.s V_4 +// IL_00af: ldfld ""int System.ValueTuple.Item1"" +// IL_00b4: bne.un.s IL_00c7 +// IL_00b6: ldloc.2 +// IL_00b7: ldfld ""long System.ValueTuple.Item2"" +// IL_00bc: ldloc.s V_4 +// IL_00be: ldfld ""long System.ValueTuple.Item2"" +// IL_00c3: ceq +// IL_00c5: br.s IL_00c8 +// IL_00c7: ldc.i4.0 +// IL_00c8: box ""bool"" +// IL_00cd: call ""string string.Format(string, object)"" +// IL_00d2: call ""void System.Console.Write(string)"" +// IL_00d7: nop +// IL_00d8: ret +//}"); + } + + [Fact] + public void TestOnNullableVsNullableTuples_WithImplicitCustomConversion() + { + var source = @" +class C +{ + int _value; + public C(int v) { _value = v; } + + public static void Main() + { + Compare(null, null); + Compare(null, (1, new C(20))); + Compare((new C(30), 3), null); + Compare((new C(4), 4), (4, new C(4))); + Compare((new C(5), 5), (10, new C(10))); + Compare((new C(6), 6), (6, new C(20))); + } + private static void Compare((C, int)? nt1, (int, C)? nt2) + { + System.Console.Write($""{nt1 == nt2} ""); + } + public static implicit operator int(C c) + { + System.Console.Write($""Convert{c._value} ""); + return c._value; + } +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + var verifier = CompileAndVerify(comp, expectedOutput: "True False False Convert4 Convert4 True Convert5 False Convert6 Convert20 False "); + + // PROTOTYPE(tuple-equality) Semantic model + return; + +// var tree = comp.SyntaxTrees.First(); +// var model = comp.GetSemanticModel(tree); + +// var comparison = tree.GetCompilationUnitRoot().DescendantNodes().OfType().Single(); +// var nt1 = comparison.Left; +// var nt1Type = model.GetTypeInfo(nt1); +// Assert.Equal("nt1", nt1.ToString()); +// Assert.Equal("(C, System.Int32)?", nt1Type.Type.ToTestDisplayString()); +// Assert.Equal("(System.Int32, System.Int32)?", nt1Type.ConvertedType.ToTestDisplayString()); + +// var nt2 = comparison.Right; +// var nt2Type = model.GetTypeInfo(nt2); +// Assert.Equal("nt2", nt2.ToString()); +// Assert.Equal("(System.Int32, C)?", nt2Type.Type.ToTestDisplayString()); +// Assert.Equal("(System.Int32, System.Int32)?", nt2Type.ConvertedType.ToTestDisplayString()); +// verifier.VerifyIL("C.Compare", @"{ +// // Code size 226 (0xe2) +// .maxstack 3 +// .locals init ((int, int)? V_0, +// bool V_1, +// System.ValueTuple V_2, +// (int, int)? V_3, +// System.ValueTuple V_4, +// (C, int)? V_5, +// (int, int)? V_6, +// System.ValueTuple V_7, +// (int, C)? V_8, +// System.ValueTuple V_9) +// IL_0000: nop +// IL_0001: ldstr ""{0} "" +// IL_0006: ldarg.0 +// IL_0007: stloc.s V_5 +// IL_0009: ldloca.s V_5 +// IL_000b: call ""bool (C, int)?.HasValue.get"" +// IL_0010: brtrue.s IL_001e +// IL_0012: ldloca.s V_6 +// IL_0014: initobj ""(int, int)?"" +// IL_001a: ldloc.s V_6 +// IL_001c: br.s IL_0044 +// IL_001e: ldloca.s V_5 +// IL_0020: call ""(C, int) (C, int)?.GetValueOrDefault()"" +// IL_0025: stloc.s V_7 +// IL_0027: ldloc.s V_7 +// IL_0029: ldfld ""C System.ValueTuple.Item1"" +// IL_002e: call ""int C.op_Implicit(C)"" +// IL_0033: ldloc.s V_7 +// IL_0035: ldfld ""int System.ValueTuple.Item2"" +// IL_003a: newobj ""System.ValueTuple..ctor(int, int)"" +// IL_003f: newobj ""(int, int)?..ctor((int, int))"" +// IL_0044: stloc.0 +// IL_0045: ldloca.s V_0 +// IL_0047: call ""bool (int, int)?.HasValue.get"" +// IL_004c: stloc.1 +// IL_004d: ldarg.1 +// IL_004e: stloc.s V_8 +// IL_0050: ldloca.s V_8 +// IL_0052: call ""bool (int, C)?.HasValue.get"" +// IL_0057: brtrue.s IL_0065 +// IL_0059: ldloca.s V_6 +// IL_005b: initobj ""(int, int)?"" +// IL_0061: ldloc.s V_6 +// IL_0063: br.s IL_008b +// IL_0065: ldloca.s V_8 +// IL_0067: call ""(int, C) (int, C)?.GetValueOrDefault()"" +// IL_006c: stloc.s V_9 +// IL_006e: ldloc.s V_9 +// IL_0070: ldfld ""int System.ValueTuple.Item1"" +// IL_0075: ldloc.s V_9 +// IL_0077: ldfld ""C System.ValueTuple.Item2"" +// IL_007c: call ""int C.op_Implicit(C)"" +// IL_0081: newobj ""System.ValueTuple..ctor(int, int)"" +// IL_0086: newobj ""(int, int)?..ctor((int, int))"" +// IL_008b: stloc.3 +// IL_008c: ldloc.1 +// IL_008d: ldloca.s V_3 +// IL_008f: call ""bool (int, int)?.HasValue.get"" +// IL_0094: beq.s IL_0099 +// IL_0096: ldc.i4.0 +// IL_0097: br.s IL_00d1 +// IL_0099: ldloc.1 +// IL_009a: brtrue.s IL_009f +// IL_009c: ldc.i4.1 +// IL_009d: br.s IL_00d1 +// IL_009f: ldloca.s V_0 +// IL_00a1: call ""(int, int) (int, int)?.GetValueOrDefault()"" +// IL_00a6: stloc.2 +// IL_00a7: ldloca.s V_3 +// IL_00a9: call ""(int, int) (int, int)?.GetValueOrDefault()"" +// IL_00ae: stloc.s V_4 +// IL_00b0: ldloc.2 +// IL_00b1: ldfld ""int System.ValueTuple.Item1"" +// IL_00b6: ldloc.s V_4 +// IL_00b8: ldfld ""int System.ValueTuple.Item1"" +// IL_00bd: bne.un.s IL_00d0 +// IL_00bf: ldloc.2 +// IL_00c0: ldfld ""int System.ValueTuple.Item2"" +// IL_00c5: ldloc.s V_4 +// IL_00c7: ldfld ""int System.ValueTuple.Item2"" +// IL_00cc: ceq +// IL_00ce: br.s IL_00d1 +// IL_00d0: ldc.i4.0 +// IL_00d1: box ""bool"" +// IL_00d6: call ""string string.Format(string, object)"" +// IL_00db: call ""void System.Console.Write(string)"" +// IL_00e0: nop +// IL_00e1: ret +//}"); + } + + [Fact] + public void TestOnNullableVsNonNullableTuples() + { + var source = @" +class C +{ + public static void Main() + { + M(null); + M((1, 2)); + M((10, 20)); + } + private static void M((byte, int)? nt) + { + System.Console.Write((1, 2) == nt); + } +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + + var verifier = CompileAndVerify(comp, expectedOutput: "FalseTrueFalse"); + verifier.VerifyIL("C.M", @"{ + // Code size 53 (0x35) + .maxstack 2 + .locals init ((byte, int)? V_0, + System.ValueTuple V_1) + IL_0000: nop + IL_0001: ldarg.0 + IL_0002: stloc.0 + IL_0003: ldloca.s V_0 + IL_0005: call ""bool (byte, int)?.HasValue.get"" + IL_000a: brtrue.s IL_000f + IL_000c: ldc.i4.0 + IL_000d: br.s IL_002e + IL_000f: br.s IL_0011 + IL_0011: ldloca.s V_0 + IL_0013: call ""(byte, int) (byte, int)?.GetValueOrDefault()"" + IL_0018: stloc.1 + IL_0019: ldc.i4.1 + IL_001a: ldloc.1 + IL_001b: ldfld ""byte System.ValueTuple.Item1"" + IL_0020: bne.un.s IL_002d + IL_0022: ldc.i4.2 + IL_0023: ldloc.1 + IL_0024: ldfld ""int System.ValueTuple.Item2"" + IL_0029: ceq + IL_002b: br.s IL_002e + IL_002d: ldc.i4.0 + IL_002e: call ""void System.Console.Write(bool)"" + IL_0033: nop + IL_0034: ret +}"); + + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree); + + // PROTOTYPE(tuple-equality) Semantic model + return; + + //var comparison = tree.GetCompilationUnitRoot().DescendantNodes().OfType().Single(); + //var tuple = comparison.Left; + //var tupleType = model.GetTypeInfo(tuple); + //Assert.Equal("(1, 2)", tuple.ToString()); + //Assert.Equal("(System.Int32, System.Int32)", tupleType.Type.ToTestDisplayString()); + //Assert.Equal("(System.Int32, System.Int32)?", tupleType.ConvertedType.ToTestDisplayString()); + + //var nt = comparison.Right; + //var ntType = model.GetTypeInfo(nt); + //Assert.Equal("nt", nt.ToString()); + //Assert.Equal("(System.Byte, System.Int32)?", ntType.Type.ToTestDisplayString()); + //Assert.Equal("(System.Int32, System.Int32)?", ntType.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void TestOnNullableVsNonNullableTuples_WithCustomConversion() + { + var source = @" +class C +{ + int _value; + public C(int v) { _value = v; } + public static void Main() + { + M(null); + M((new C(1), 2)); + M((new C(10), 20)); + } + private static void M((C, int)? nt) + { + System.Console.Write($""{(1, 2) == nt} ""); + System.Console.Write($""{nt == (1, 2)} ""); + } + public static implicit operator int(C c) + { + System.Console.Write($""Convert{c._value} ""); + return c._value; + } +}"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + + var verifier = CompileAndVerify(comp, expectedOutput: "False False Convert1 True Convert1 True Convert10 False Convert10 False"); + + // PROTOTYPE(tuple-equality) Semantic model + return; + + //var tree = comp.SyntaxTrees.First(); + //var model = comp.GetSemanticModel(tree); + + //var comparison = tree.GetCompilationUnitRoot().DescendantNodes().OfType().First(); + //Assert.Equal("(1, 2) == nt", comparison.ToString()); + //var tuple = comparison.Left; + //var tupleType = model.GetTypeInfo(tuple); + //Assert.Equal("(1, 2)", tuple.ToString()); + //Assert.Equal("(System.Int32, System.Int32)", tupleType.Type.ToTestDisplayString()); + //Assert.Equal("(System.Int32, System.Int32)?", tupleType.ConvertedType.ToTestDisplayString()); + + //var nt = comparison.Right; + //var ntType = model.GetTypeInfo(nt); + //Assert.Equal("nt", nt.ToString()); + //Assert.Equal("(C, System.Int32)?", ntType.Type.ToTestDisplayString()); + //Assert.Equal("(System.Int32, System.Int32)?", ntType.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void TestOnNullableVsNonNullableTuples3() + { + var source = @" +class C +{ + public static void Main() + { + M(null); + M((1, 2)); + M((10, 20)); + } + private static void M((int, int)? nt) + { + System.Console.Write(nt == (1, 2)); + } +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + + var verifier = CompileAndVerify(comp, expectedOutput: "FalseTrueFalse"); + verifier.VerifyIL("C.M", @"{ + // Code size 59 (0x3b) + .maxstack 2 + .locals init ((int, int)? V_0, + bool V_1, + System.ValueTuple V_2) + IL_0000: nop + IL_0001: ldarg.0 + IL_0002: stloc.0 + IL_0003: ldloca.s V_0 + IL_0005: call ""bool (int, int)?.HasValue.get"" + IL_000a: stloc.1 + IL_000b: ldloc.1 + IL_000c: brtrue.s IL_0011 + IL_000e: ldc.i4.0 + IL_000f: br.s IL_0034 + IL_0011: ldloc.1 + IL_0012: brtrue.s IL_0017 + IL_0014: ldc.i4.1 + IL_0015: br.s IL_0034 + IL_0017: ldloca.s V_0 + IL_0019: call ""(int, int) (int, int)?.GetValueOrDefault()"" + IL_001e: stloc.2 + IL_001f: ldloc.2 + IL_0020: ldfld ""int System.ValueTuple.Item1"" + IL_0025: ldc.i4.1 + IL_0026: bne.un.s IL_0033 + IL_0028: ldloc.2 + IL_0029: ldfld ""int System.ValueTuple.Item2"" + IL_002e: ldc.i4.2 + IL_002f: ceq + IL_0031: br.s IL_0034 + IL_0033: ldc.i4.0 + IL_0034: call ""void System.Console.Write(bool)"" + IL_0039: nop + IL_003a: ret +}"); + } + + [Fact] + public void TestOnNullableVsLiteralTuples() + { + var source = @" +class C +{ + public static void Main() + { + CheckNull(null); + CheckNull((1, 2)); + } + private static void CheckNull((int, int)? nt) + { + System.Console.Write($""{nt == null} ""); + System.Console.Write($""{nt != null} ""); + } +} +"; + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "True False False True"); + // PROTOTYPE(tuple-equality) Semantic model: check type of null + } + + [Fact] + public void TestOnLongTuple() + { + var source = @" +class C +{ + public static void Main() + { + Assert(MakeLongTuple(1) == MakeLongTuple(1)); + Assert(!(MakeLongTuple(1) != MakeLongTuple(1))); + + Assert(MakeLongTuple(1) == (1, 1, 1, 1, 1, 1, 1, 1, 1)); + Assert(!(MakeLongTuple(1) != (1, 1, 1, 1, 1, 1, 1, 1, 1))); + + Assert(MakeLongTuple(1) == MakeLongTuple(1, 1)); + Assert(!(MakeLongTuple(1) != MakeLongTuple(1, 1))); + + Assert(!(MakeLongTuple(1) == MakeLongTuple(1, 2))); + Assert(!(MakeLongTuple(1) == MakeLongTuple(2, 1))); + Assert(MakeLongTuple(1) != MakeLongTuple(1, 2)); + Assert(MakeLongTuple(1) != MakeLongTuple(2, 1)); + + System.Console.Write(""Success""); + } + private static (int, int, int, int, int, int, int, int, int) MakeLongTuple(int x) + => (x, x, x, x, x, x, x, x, x); + + private static (int?, int, int?, int, int?, int, int?, int, int?)? MakeLongTuple(int? x, int y) + => (x, y, x, y, x, y, x, y, x); + + private static void Assert(bool test) + { + if (!test) + { + throw null; + } + } +} +"; + + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "Success"); + } + + [Fact] + public void TestOn1Tuple_FromRest() + { + var source = @" +class C +{ + public static bool M() + { + var x1 = MakeLongTuple(1).Rest; + + bool b1 = x1 == x1; + bool b2 = x1 != x1; + + return b1 && b2; + } + private static (int, int, int, int, int, int, int, int?) MakeLongTuple(int? x) + => throw null; + + public bool Unused((int, int, int, int, int, int, int, int?) t) + { + return t.Rest == t.Rest; + } +} +"; + + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (8,19): error CS0019: Operator '==' cannot be applied to operands of type 'ValueTuple' and 'ValueTuple' + // bool b1 = x1 == x1; + Diagnostic(ErrorCode.ERR_BadBinaryOps, "x1 == x1").WithArguments("==", "ValueTuple", "ValueTuple").WithLocation(8, 19), + // (9,19): error CS0019: Operator '!=' cannot be applied to operands of type 'ValueTuple' and 'ValueTuple' + // bool b2 = x1 != x1; + Diagnostic(ErrorCode.ERR_BadBinaryOps, "x1 != x1").WithArguments("!=", "ValueTuple", "ValueTuple").WithLocation(9, 19), + // (18,16): error CS0019: Operator '==' cannot be applied to operands of type 'ValueTuple' and 'ValueTuple' + // return t.Rest == t.Rest; + Diagnostic(ErrorCode.ERR_BadBinaryOps, "t.Rest == t.Rest").WithArguments("==", "ValueTuple", "ValueTuple").WithLocation(18, 16) + ); + + // PROTOTYPE(tuple-equality) Semantic model + return; + + //var tree = comp.SyntaxTrees.First(); + //var model = comp.GetSemanticModel(tree); + //var comparison = tree.GetRoot().DescendantNodes().OfType().Last(); + //Assert.Equal("t.Rest == t.Rest", comparison.ToString()); + + //var left = model.GetTypeInfo(comparison.Left); + //Assert.Equal("ValueTuple", left.Type.ToTestDisplayString()); + //Assert.Equal("System.Object", left.ConvertedType.ToTestDisplayString()); + //Assert.True(left.Type.IsTupleType); // PROTOTYPE(tuple-equality) Need to investigate this + } + + [Fact] + public void TestOn1Tuple_FromValueTuple() + { + var source = @" +using System; +class C +{ + public static bool M() + { + var x1 = ValueTuple.Create((int?)1); + + bool b1 = x1 == x1; + bool b2 = x1 != x1; + + return b1 && b2; + } +} +"; + + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (9,19): error CS0019: Operator '==' cannot be applied to operands of type 'ValueTuple' and 'ValueTuple' + // bool b1 = x1 == x1; + Diagnostic(ErrorCode.ERR_BadBinaryOps, "x1 == x1").WithArguments("==", "ValueTuple", "ValueTuple").WithLocation(9, 19), + // (10,19): error CS0019: Operator '!=' cannot be applied to operands of type 'ValueTuple' and 'ValueTuple' + // bool b2 = x1 != x1; + Diagnostic(ErrorCode.ERR_BadBinaryOps, "x1 != x1").WithArguments("!=", "ValueTuple", "ValueTuple").WithLocation(10, 19) + ); + } + + [Fact] + public void TestOnTupleOfDecimals() + { + var source = @" +class C +{ + public static void Main() + { + System.Console.Write(Compare((1, 2), (1, 2))); + System.Console.Write(Compare((1, 2), (10, 20))); + } + public static bool Compare((decimal, decimal) t1, (decimal, decimal) t2) + { + return t1 == t2; + } +} +"; + + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + var verifier = CompileAndVerify(comp, expectedOutput: "TrueFalse"); + verifier.VerifyIL("C.Compare", @"{ + // Code size 49 (0x31) + .maxstack 2 + .locals init (System.ValueTuple V_0, + System.ValueTuple V_1, + bool V_2) + IL_0000: nop + IL_0001: ldarg.0 + IL_0002: stloc.0 + IL_0003: ldarg.1 + IL_0004: stloc.1 + IL_0005: ldloc.0 + IL_0006: ldfld ""decimal System.ValueTuple.Item1"" + IL_000b: ldloc.1 + IL_000c: ldfld ""decimal System.ValueTuple.Item1"" + IL_0011: call ""bool decimal.op_Equality(decimal, decimal)"" + IL_0016: brfalse.s IL_002b + IL_0018: ldloc.0 + IL_0019: ldfld ""decimal System.ValueTuple.Item2"" + IL_001e: ldloc.1 + IL_001f: ldfld ""decimal System.ValueTuple.Item2"" + IL_0024: call ""bool decimal.op_Equality(decimal, decimal)"" + IL_0029: br.s IL_002c + IL_002b: ldc.i4.0 + IL_002c: stloc.2 + IL_002d: br.s IL_002f + IL_002f: ldloc.2 + IL_0030: ret +}"); + } + + [Fact] + public void TestSideEffectsAreSavedToTemps() + { + var source = @" +class C +{ + public static void Main() + { + int i = 0; + System.Console.Write((i++, i++, i++) == (0, 1, 2)); + } +} +"; + + var comp = CreateStandardCompilation(source, references: s_valueTupleRefs, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + var verifier = CompileAndVerify(comp, expectedOutput: "True"); + } + + [Fact] + public void TestNonBoolComparisonResult_WithTrueFalseOperators() + { + var source = @" +using static System.Console; +public class C +{ + public static void Main() + { + Write($""{REPLACE}""); + } +} +public class Base +{ + public int I; + public Base(int i) { I = i; } +} +public class A : Base +{ + public A(int i) : base(i) + { + Write($""A:{i}, ""); + } + public static NotBool operator ==(A a, Y y) + { + Write(""A == Y, ""); + return new NotBool(a.I == y.I); + } + public static NotBool operator !=(A a, Y y) + { + Write(""A != Y, ""); + return new NotBool(a.I != y.I); + } + public override bool Equals(object o) + => throw null; + public override int GetHashCode() + => throw null; +} +public class X : Base +{ + public X(int i) : base(i) + { + Write($""X:{i}, ""); + } +} +public class Y : Base +{ + public Y(int i) : base(i) + { + Write($""Y:{i}, ""); + } + public static implicit operator Y(X x) + { + Write(""X -> ""); + return new Y(x.I); + } +} +public class NotBool +{ + public bool B; + public NotBool(bool value) + { + B = value; + } + public static bool operator true(NotBool b) + { + Write($""NotBool.true -> {b.B}, ""); + return b.B; + } + public static bool operator false(NotBool b) + { + Write($""NotBool.false -> {!b.B}, ""); + return !b.B; + } +} +"; + + validate("(new A(1), new A(2)) == (new X(1), new Y(2))", "A:1, A:2, X:1, Y:2, X -> Y:1, A == Y, NotBool.false -> False, A == Y, NotBool.false -> False, True"); + validate("(new A(1), new A(2)) == (new X(1), new Y(20))", "A:1, A:2, X:1, Y:20, X -> Y:1, A == Y, NotBool.false -> False, A == Y, NotBool.false -> True, False"); + + validate("(new A(1), new A(2)) != (new X(1), new Y(2))", "A:1, A:2, X:1, Y:2, X -> Y:1, A != Y, NotBool.true -> False, A != Y, NotBool.true -> False, False"); + validate("(new A(1), new A(2)) != (new X(1), new Y(20))", "A:1, A:2, X:1, Y:20, X -> Y:1, A != Y, NotBool.true -> False, A != Y, NotBool.true -> True, True"); + + validate("((dynamic)new A(1), new A(2)) == (new X(1), (dynamic)new Y(2))", "A:1, A:2, X:1, Y:2, X -> Y:1, A == Y, NotBool.false -> False, A == Y, NotBool.false -> False, True"); + validate("((dynamic)new A(1), new A(2)) == (new X(1), (dynamic)new Y(20))", "A:1, A:2, X:1, Y:20, X -> Y:1, A == Y, NotBool.false -> False, A == Y, NotBool.false -> True, False"); + + validate("((dynamic)new A(1), new A(2)) != (new X(1), (dynamic)new Y(2))", "A:1, A:2, X:1, Y:2, X -> Y:1, A != Y, NotBool.true -> False, A != Y, NotBool.true -> False, False"); + validate("((dynamic)new A(1), new A(2)) != (new X(1), (dynamic)new Y(20))", "A:1, A:2, X:1, Y:20, X -> Y:1, A != Y, NotBool.true -> False, A != Y, NotBool.true -> True, True"); + + void validate(string expression, string expected) + { + var comp = CreateStandardCompilation(source.Replace("REPLACE", expression), + references: new[] { ValueTupleRef, SystemRuntimeFacadeRef, CSharpRef, SystemCoreRef }, options: TestOptions.DebugExe); + + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: expected); + } + } + + [Fact] + public void TestNonBoolComparisonResult_WithTrueFalseOperatorsOnBase() + { + var source = @" +using static System.Console; +public class C +{ + public static void Main() + { + Write($""{REPLACE}""); + } +} +public class Base +{ + public int I; + public Base(int i) { I = i; } +} +public class A : Base +{ + public A(int i) : base(i) + { + Write($""A:{i}, ""); + } + public static NotBool operator ==(A a, Y y) + { + Write(""A == Y, ""); + return new NotBool(a.I == y.I); + } + public static NotBool operator !=(A a, Y y) + { + Write(""A != Y, ""); + return new NotBool(a.I != y.I); + } + public override bool Equals(object o) + => throw null; + public override int GetHashCode() + => throw null; +} +public class X : Base +{ + public X(int i) : base(i) + { + Write($""X:{i}, ""); + } +} +public class Y : Base +{ + public Y(int i) : base(i) + { + Write($""Y:{i}, ""); + } + public static implicit operator Y(X x) + { + Write(""X -> ""); + return new Y(x.I); + } +} +public class NotBoolBase +{ + public bool B; + public NotBoolBase(bool value) + { + B = value; + } + public static bool operator true(NotBoolBase b) + { + Write($""NotBoolBase.true -> {b.B}, ""); + return b.B; + } + public static bool operator false(NotBoolBase b) + { + Write($""NotBoolBase.false -> {!b.B}, ""); + return !b.B; + } +} +public class NotBool : NotBoolBase +{ + public NotBool(bool value) : base(value) { } +} +"; + + // This tests the case where the custom operators false/true need an input conversion that's not just an identity conversion (in this case, it's an implicit reference conversion) + validate("(new A(1), new A(2)) == (new X(1), new Y(2))", "A:1, A:2, X:1, Y:2, X -> Y:1, A == Y, NotBoolBase.false -> False, A == Y, NotBoolBase.false -> False, True"); + validate("(new A(1), new A(2)) == (new X(1), new Y(20))", "A:1, A:2, X:1, Y:20, X -> Y:1, A == Y, NotBoolBase.false -> False, A == Y, NotBoolBase.false -> True, False"); + + validate("(new A(1), new A(2)) != (new X(1), new Y(2))", "A:1, A:2, X:1, Y:2, X -> Y:1, A != Y, NotBoolBase.true -> False, A != Y, NotBoolBase.true -> False, False"); + validate("(new A(1), new A(2)) != (new X(1), new Y(20))", "A:1, A:2, X:1, Y:20, X -> Y:1, A != Y, NotBoolBase.true -> False, A != Y, NotBoolBase.true -> True, True"); + + validate("((dynamic)new A(1), new A(2)) == (new X(1), (dynamic)new Y(2))", "A:1, A:2, X:1, Y:2, X -> Y:1, A == Y, NotBoolBase.false -> False, A == Y, NotBoolBase.false -> False, True"); + validate("((dynamic)new A(1), new A(2)) == (new X(1), (dynamic)new Y(20))", "A:1, A:2, X:1, Y:20, X -> Y:1, A == Y, NotBoolBase.false -> False, A == Y, NotBoolBase.false -> True, False"); + + validate("((dynamic)new A(1), new A(2)) != (new X(1), (dynamic)new Y(2))", "A:1, A:2, X:1, Y:2, X -> Y:1, A != Y, NotBoolBase.true -> False, A != Y, NotBoolBase.true -> False, False"); + validate("((dynamic)new A(1), new A(2)) != (new X(1), (dynamic)new Y(20))", "A:1, A:2, X:1, Y:20, X -> Y:1, A != Y, NotBoolBase.true -> False, A != Y, NotBoolBase.true -> True, True"); + + void validate(string expression, string expected) + { + var comp = CreateStandardCompilation(source.Replace("REPLACE", expression), + references: new[] { ValueTupleRef, SystemRuntimeFacadeRef, CSharpRef, SystemCoreRef }, options: TestOptions.DebugExe); + + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: expected); + } + } + + [Fact] + public void TestNonBoolComparisonResult_WithImplicitBoolConversion() + { + var source = @" +using static System.Console; +public class C +{ + public static void Main() + { + Write($""{REPLACE}""); + } +} +public class Base +{ + public int I; + public Base(int i) { I = i; } +} +public class A : Base +{ + public A(int i) : base(i) + { + Write($""A:{i}, ""); + } + public static NotBool operator ==(A a, Y y) + { + Write(""A == Y, ""); + return new NotBool(a.I == y.I); + } + public static NotBool operator !=(A a, Y y) + { + Write(""A != Y, ""); + return new NotBool(a.I != y.I); + } + public override bool Equals(object o) + => throw null; + public override int GetHashCode() + => throw null; +} +public class X : Base +{ + public X(int i) : base(i) + { + Write($""X:{i}, ""); + } +} +public class Y : Base +{ + public Y(int i) : base(i) + { + Write($""Y:{i}, ""); + } + public static implicit operator Y(X x) + { + Write(""X -> ""); + return new Y(x.I); + } +} +public class NotBool +{ + public bool B; + public NotBool(bool value) + { + B = value; + } + public static implicit operator bool(NotBool b) + { + Write($""NotBool -> bool:{b.B}, ""); + return b.B; + } +} +"; + + validate("(new A(1), new A(2)) == (new X(1), new Y(2))", "A:1, A:2, X:1, Y:2, X -> Y:1, A == Y, NotBool -> bool:True, A == Y, NotBool -> bool:True, True"); + validate("(new A(1), new A(2)) == (new X(10), new Y(2))", "A:1, A:2, X:10, Y:2, X -> Y:10, A == Y, NotBool -> bool:False, False"); + validate("(new A(1), new A(2)) == (new X(1), new Y(20))", "A:1, A:2, X:1, Y:20, X -> Y:1, A == Y, NotBool -> bool:True, A == Y, NotBool -> bool:False, False"); + + validate("(new A(1), new A(2)) != (new X(1), new Y(2))", "A:1, A:2, X:1, Y:2, X -> Y:1, A != Y, NotBool -> bool:False, A != Y, NotBool -> bool:False, False"); + validate("(new A(1), new A(2)) != (new X(10), new Y(2))", "A:1, A:2, X:10, Y:2, X -> Y:10, A != Y, NotBool -> bool:True, True"); + validate("(new A(1), new A(2)) != (new X(1), new Y(20))", "A:1, A:2, X:1, Y:20, X -> Y:1, A != Y, NotBool -> bool:False, A != Y, NotBool -> bool:True, True"); + + void validate(string expression, string expected) + { + var comp = CreateStandardCompilation(source.Replace("REPLACE", expression), + references: new[] { ValueTupleRef, SystemRuntimeFacadeRef }, options: TestOptions.DebugExe); + + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: expected); + } + } + + [Fact] + public void TestNonBoolComparisonResult_WithoutImplicitBoolConversion() + { + var source = @" +using static System.Console; +public class C +{ + public static void Main() + { + Write($""{REPLACE}""); + } +} +public class Base +{ + public Base(int i) { } +} +public class A : Base +{ + public A(int i) : base(i) + => throw null; + public static NotBool operator ==(A a, Y y) + => throw null; + public static NotBool operator !=(A a, Y y) + => throw null; + public override bool Equals(object o) + => throw null; + public override int GetHashCode() + => throw null; +} +public class X : Base +{ + public X(int i) : base(i) + => throw null; +} +public class Y : Base +{ + public Y(int i) : base(i) + => throw null; + public static implicit operator Y(X x) + => throw null; +} +public class NotBool +{ +} +"; + + validate("(new A(1), new A(2)) == (new X(1), new Y(2))", + // (7,18): error CS0029: Cannot implicitly convert type 'NotBool' to 'bool' + // Write($"{(new A(1), new A(2)) == (new X(1), new Y(2))}"); + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(new A(1), new A(2)) == (new X(1), new Y(2))").WithArguments("NotBool", "bool").WithLocation(7, 18), + // (7,18): error CS0029: Cannot implicitly convert type 'NotBool' to 'bool' + // Write($"{(new A(1), new A(2)) == (new X(1), new Y(2))}"); + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(new A(1), new A(2)) == (new X(1), new Y(2))").WithArguments("NotBool", "bool").WithLocation(7, 18) + ); + + validate("(new A(1), 2) != (new X(1), 2)", + // (7,18): error CS0029: Cannot implicitly convert type 'NotBool' to 'bool' + // Write($"{(new A(1), 2) != (new X(1), 2)}"); + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(new A(1), 2) != (new X(1), 2)").WithArguments("NotBool", "bool").WithLocation(7, 18) + ); + + void validate(string expression, params DiagnosticDescription[] expected) + { + var comp = CreateStandardCompilation(source.Replace("REPLACE", expression), references: new[] { ValueTupleRef, SystemRuntimeFacadeRef }); + comp.VerifyDiagnostics(expected); + } + } + + [Fact] + public void TestNonBoolComparisonResult_WithExplicitBoolConversion() + { + var source = @" +using static System.Console; +public class C +{ + public static void Main() + { + Write($""{(new A(1), new A(2)) == (new X(1), new Y(2))}""); + } +} +public class Base +{ + public int I; + public Base(int i) { I = i; } +} +public class A : Base +{ + public A(int i) : base(i) + => throw null; + public static NotBool operator ==(A a, Y y) + => throw null; + public static NotBool operator !=(A a, Y y) + => throw null; + public override bool Equals(object o) + => throw null; + public override int GetHashCode() + => throw null; +} +public class X : Base +{ + public X(int i) : base(i) + => throw null; +} +public class Y : Base +{ + public Y(int i) : base(i) + => throw null; + public static implicit operator Y(X x) + => throw null; +} +public class NotBool +{ + public NotBool(bool value) + => throw null; + public static explicit operator bool(NotBool b) + => throw null; +} +"; + + var comp = CreateStandardCompilation(source, references: new[] { ValueTupleRef, SystemRuntimeFacadeRef }); + comp.VerifyDiagnostics( + // (7,18): error CS0029: Cannot implicitly convert type 'NotBool' to 'bool' + // Write($"{(new A(1), new A(2)) == (new X(1), new Y(2))}"); + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(new A(1), new A(2)) == (new X(1), new Y(2))").WithArguments("NotBool", "bool").WithLocation(7, 18), + // (7,18): error CS0029: Cannot implicitly convert type 'NotBool' to 'bool' + // Write($"{(new A(1), new A(2)) == (new X(1), new Y(2))}"); + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(new A(1), new A(2)) == (new X(1), new Y(2))").WithArguments("NotBool", "bool").WithLocation(7, 18) + ); + } + + [Fact] + public void TestNullableBoolComparisonResult_WithTrueFalseOperators() + { + var source = @" +public class C +{ + public static void M(A a) + { + _ = (a, a) == (a, a); + if (a == a) { } + } +} +public class A +{ + public A(int i) + => throw null; + public static bool? operator ==(A a1, A a2) + => throw null; + public static bool? operator !=(A a1, A a2) + => throw null; + public override bool Equals(object o) + => throw null; + public override int GetHashCode() + => throw null; +} +"; + + var comp = CreateStandardCompilation(source, references: new[] { ValueTupleRef, SystemRuntimeFacadeRef }); + comp.VerifyDiagnostics( + // (6,13): error CS0029: Cannot implicitly convert type 'bool?' to 'bool' + // _ = (a, a) == (a, a); + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(a, a) == (a, a)").WithArguments("bool?", "bool").WithLocation(6, 13), + // (6,13): error CS0029: Cannot implicitly convert type 'bool?' to 'bool' + // _ = (a, a) == (a, a); + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(a, a) == (a, a)").WithArguments("bool?", "bool").WithLocation(6, 13), + // (7,13): error CS0266: Cannot implicitly convert type 'bool?' to 'bool'. An explicit conversion exists (are you missing a cast?) + // if (a == a) { } + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "a == a").WithArguments("bool?", "bool").WithLocation(7, 13), + // (7,13): warning CS1718: Comparison made to same variable; did you mean to compare something else? + // if (a == a) { } + Diagnostic(ErrorCode.WRN_ComparisonToSelf, "a == a").WithLocation(7, 13) + ); + } + + [Fact] + void TestValueTupleWithObsoleteEqualityOperator() + { + string source = @" +public class C +{ + public static void Main() + { + System.Console.Write((1, 2) == (3, 4)); + } +} +namespace System +{ + public struct ValueTuple + { + public T1 Item1; + public T2 Item2; + + public ValueTuple(T1 item1, T2 item2) + { + this.Item1 = item1; + this.Item2 = item2; + } + + [System.Obsolete] + public static bool operator==(ValueTuple t1, ValueTuple t2) + => throw null; + + public static bool operator!=(ValueTuple t1, ValueTuple t2) + => throw null; + + public override bool Equals(object other) + => throw null; + + public override int GetHashCode() + => throw null; + } +} +"; + + var comp = CreateStandardCompilation(source, options: TestOptions.DebugExe); + comp.VerifyDiagnostics(); + + // Note: tuple equality picked ahead of custom operator== + CompileAndVerify(comp, expectedOutput: "False"); + } + } +} + +// PROTOTYPE(tuple-equality) +// Test with tuple element names (semantic model) +// Test tuples with casts or nested casts diff --git a/src/Test/Utilities/Portable/Traits/CompilerFeature.cs b/src/Test/Utilities/Portable/Traits/CompilerFeature.cs index 8d176ca617c6..623d80061890 100644 --- a/src/Test/Utilities/Portable/Traits/CompilerFeature.cs +++ b/src/Test/Utilities/Portable/Traits/CompilerFeature.cs @@ -28,5 +28,6 @@ public enum CompilerFeature PrivateProtected, PEVerifyCompat, RefConditionalOperator, + TupleEquality, } }