trinalbadger587
trinalbadger587

Reputation: 2109

Issues with operator resolution in value types (no references added)

I was working on a project and I found that the operator I used and the one I declared were not equal.

enter image description here

I made a minimum reproducible example:

var tree = CSharpSyntaxTree.ParseText(@"
bool a = 3 > 5;
namespace System{
    public struct Int32
    {
        public static extern bool operator > (int a, int b);
    }
    public struct Boolean { }
}");
var compilation = CSharpCompilation.Create("bla").AddSyntaxTrees(tree);
var model = compilation.GetSemanticModel(tree);

var usedSymbol     = model.GetSymbolInfo(tree.GetRoot().DescendantNodes().OfType<BinaryExpressionSyntax>().Single()).Symbol;
var declaredSymbol = model.GetDeclaredSymbol(tree.GetRoot().DescendantNodes().OfType<OperatorDeclarationSyntax>().Single());

Console.WriteLine(
    $"{declaredSymbol} and {usedSymbol} are {(declaredSymbol.Equals(usedSymbol) ? "" : "not ")}equal.");

// int.operator >(int, int) and int.operator >(int, int) are not equal.

See on .NET Fiddle!

Why aren't these operators that seem the same showing that they are equal?

Upvotes: 0

Views: 91

Answers (1)

Sean Skelly
Sean Skelly

Reputation: 1324

I modified your code, and using Reflection together with a peek at the Roslyn source, have found that usedSymbol and declaredSymbol end up as two distinct Symbol types.

        var tree = CSharpSyntaxTree.ParseText(@"
bool a = 3 > 5;
namespace System{
    public struct Int32
    {
        public static extern bool operator > (int a, int b);
    }
    public struct Boolean { }
}");
        var compilation = CSharpCompilation.Create("bla").AddSyntaxTrees(tree);
        var model = compilation.GetSemanticModel(tree);

        var usedSymbol     = model.GetSymbolInfo(tree.GetRoot().DescendantNodes().OfType<BinaryExpressionSyntax>().Single()).Symbol;
        var declaredSymbol = model.GetDeclaredSymbol(tree.GetRoot().DescendantNodes().OfType<OperatorDeclarationSyntax>().Single());

        Type used = usedSymbol.GetType();
        Type declared = declaredSymbol.GetType();

        var usedUnderlying = used.GetField("_underlying", BindingFlags.NonPublic | BindingFlags.Instance);
        var usedUnderlyingValue = usedUnderlying.GetValue(usedSymbol);
        var declaredUnderlying = declared.GetField("_underlying", BindingFlags.NonPublic | BindingFlags.Instance);
        var declaredUnderlyingValue = declaredUnderlying.GetValue(declaredSymbol);

        Type usedSymbolType = usedUnderlyingValue.GetType(); //SynthesizedIntrinsicOperatorSymbol
        Type declaredSymbolType = declaredUnderlyingValue.GetType(); //SourceUserDefinedOperatorSymbol

        Console.WriteLine(usedSymbolType.ToString());
        Console.WriteLine(declaredSymbolType.ToString());

        Console.WriteLine(
            $"{declaredSymbol} and {usedSymbol} are {(declaredSymbol.Equals(usedSymbol) ? "" : "not ")}equal.");

The types for the two representations of the symbol do not match. One is SynthesizedIntrinsicOperatorSymbol, and the other is SourceUserDefinedOperatorSymbol. Ultimately, this is why equality doesn't work - it seems to not have been implemented for these two types.

For example, equality for SynthesizedIntrinsicOperatorSymbol does a type check, which would fail in this use case:

    public override bool Equals(Symbol obj, TypeCompareKind compareKind)
    {
        if (obj == (object)this)
        {
            return true;
        }

        var other = obj as SynthesizedIntrinsicOperatorSymbol;

        if ((object)other == null)
        {
            return false;
        }

        if (_isCheckedBuiltin == other._isCheckedBuiltin &&
            _parameters.Length == other._parameters.Length &&
            string.Equals(_name, other._name, StringComparison.Ordinal) &&
            TypeSymbol.Equals(_containingType, other._containingType, compareKind) &&
            TypeSymbol.Equals(_returnType, other._returnType, compareKind))
        {
            for (int i = 0; i < _parameters.Length; i++)
            {
                if (!TypeSymbol.Equals(_parameters[i].Type, other._parameters[i].Type, compareKind))
                {
                    return false;
                }
            }

            return true;
        }

        return false;
    }

Looking into the other type, SourceUserDefinedOperatorSymbol, reveals that equality is implemented on a base class many layers deep: Symbols.MethodSymbol. Nothing in the inheritance chain for SourceUserDefinedOperatorSymbol overrides equality and implements a special equality check.

In looking at the source for MethodSymbol, it does not override Object.Equals(object). (It does override a related method; more on that later.)

MethodSymbol is derived from Symbol. The source of Symbol shows that it does override Object.Equals(object), which in turn calls another Equals function. Note the implementation and the comments:

    public sealed override bool Equals(object obj)
    {
        return this.Equals(obj as Symbol, SymbolEqualityComparer.Default.CompareKind);
    }

    // By default we don't consider the compareKind, and do reference equality. This can be overridden.
    public virtual bool Equals(Symbol other, TypeCompareKind compareKind)
    {
        return (object)this == other;
    }

So it seems this class just returns reference equality by design.

The Equals(Symbol, TypeCompareKind) method is virtual, and the MethodSymbol class overrides it, but only to check specific types. Because nothing in the inheritance chain for this type (SourceUserDefinedOperatorSymbol) overrides the equality methods, your code would still end up calling the base version that uses reference equality:

    public override bool Equals(Symbol other, TypeCompareKind compareKind)
    {
        if (other is SubstitutedMethodSymbol sms)
        {
            return sms.Equals(this, compareKind);
        }

        if (other is NativeIntegerMethodSymbol nms)
        {
            return nms.Equals(this, compareKind);
        }

        return base.Equals(other, compareKind);
    }

Upvotes: 1

Related Questions