0

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?

trinalbadger587
  • 1,905
  • 1
  • 18
  • 36
  • From `Object.Equals` [docs](https://learn.microsoft.com/en-us/dotnet/api/system.object.equals?view=net-5.0): "If the current instance is a reference type, the Equals(Object) method tests for reference equality, and a call to the Equals(Object) method is equivalent to a call to the ReferenceEquals method." So, it's very possible that these types did not override their `Object.Equals` method, and therefore your code is checking _reference_ equality and not true object equality. – Sean Skelly Sep 17 '21 at 23:34
  • @SeanSkelly, no I am sure that the `Equals` method on `ISymbol` is overriden. https://dotnetfiddle.net/iE0aRs – trinalbadger587 Sep 17 '21 at 23:58

1 Answers1

1

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);
    }
Sean Skelly
  • 1,229
  • 7
  • 13
  • Yes, it seems like something about it being an intrinsic operator is tripping it up. – trinalbadger587 Sep 18 '21 at 02:50
  • I have created a new question which asks how to convert this user-defined operator type into the intrinsic which is found in the code. https://stackoverflow.com/questions/69239551/how-to-get-synthesizedintrinsicoperatorsymbol-instead-of-sourceuserdefinedoperat?noredirect=1&lq=1 – trinalbadger587 Sep 19 '21 at 01:03