Prechádzať zdrojové kódy

Implement Operator Overloading (#893)

Gökhan Kurt 4 rokov pred
rodič
commit
517900f469

+ 1 - 0
.gitignore

@@ -161,3 +161,4 @@ project.lock.json
 
 .idea
 BenchmarkDotNet.Artifacts*
+.vscode

+ 75 - 0
Jint.Tests/Runtime/MethodAmbiguityTests.cs

@@ -0,0 +1,75 @@
+using Jint.Native;
+using System;
+using Xunit;
+
+namespace Jint.Tests.Runtime
+{
+    public class MethodAmbiguityTests : IDisposable
+    {
+        private readonly Engine _engine;
+
+        public MethodAmbiguityTests()
+        {
+            _engine = new Engine(cfg => cfg
+                .AllowOperatorOverloading())
+                .SetValue("log", new Action<object>(Console.WriteLine))
+                .SetValue("throws", new Func<Action, Exception>(Assert.Throws<Exception>))
+                .SetValue("assert", new Action<bool>(Assert.True))
+                .SetValue("assertFalse", new Action<bool>(Assert.False))
+                .SetValue("equal", new Action<object, object>(Assert.Equal))
+                .SetValue("TestClass", typeof(TestClass))
+                .SetValue("ChildTestClass", typeof(ChildTestClass))
+            ;
+        }
+
+        void IDisposable.Dispose()
+        {
+        }
+
+        private void RunTest(string source)
+        {
+            _engine.Execute(source);
+        }
+
+        public class TestClass
+        {
+            public int TestMethod(double a, string b, double c) => 0;
+            public int TestMethod(double a, double b, double c) => 1;
+            public int TestMethod(TestClass a, string b, double c) => 2;
+            public int TestMethod(TestClass a, TestClass b, double c) => 3;
+            public int TestMethod(TestClass a, TestClass b, TestClass c) => 4;
+            public int TestMethod(TestClass a, double b, string c) => 5;
+            public int TestMethod(ChildTestClass a, double b, string c) => 6;
+            public int TestMethod(ChildTestClass a, string b, JsValue c) => 7;
+
+            public static implicit operator TestClass(double i) => new TestClass();
+            public static implicit operator double(TestClass tc) => 0;
+            public static explicit operator string(TestClass tc) => "";
+        }
+
+        public class ChildTestClass : TestClass { }
+
+        [Fact]
+        public void BestMatchingMethodShouldBeCalled()
+        {
+            RunTest(@"
+                var tc = new TestClass();
+                var cc = new ChildTestClass();
+
+                equal(0, tc.TestMethod(0, '', 0));
+                equal(1, tc.TestMethod(0, 0, 0));
+                equal(2, tc.TestMethod(tc, '', 0));
+                equal(3, tc.TestMethod(tc, tc, 0));
+                equal(4, tc.TestMethod(tc, tc, tc));
+                equal(5, tc.TestMethod(tc, tc, ''));
+                equal(5, tc.TestMethod(0, 0, ''));
+
+                equal(6, tc.TestMethod(cc, 0, ''));
+                equal(1, tc.TestMethod(cc, 0, 0));
+                equal(6, tc.TestMethod(cc, cc, ''));
+                equal(6, tc.TestMethod(cc, 0, tc));
+                equal(7, tc.TestMethod(cc, '', {}));
+            ");
+        }
+    }
+}

+ 261 - 0
Jint.Tests/Runtime/OperatorOverloadingTests.cs

@@ -0,0 +1,261 @@
+using System;
+using Xunit;
+
+namespace Jint.Tests.Runtime
+{
+    public class OperatorOverloadingTests : IDisposable
+    {
+        private readonly Engine _engine;
+
+        public OperatorOverloadingTests()
+        {
+            _engine = new Engine(cfg => cfg
+                .AllowOperatorOverloading())
+                .SetValue("log", new Action<object>(Console.WriteLine))
+                .SetValue("assert", new Action<bool>(Assert.True))
+                .SetValue("assertFalse", new Action<bool>(Assert.False))
+                .SetValue("equal", new Action<object, object>(Assert.Equal))
+                .SetValue("Vector2", typeof(Vector2))
+                .SetValue("Vector3", typeof(Vector3))
+                .SetValue("Vector2Child", typeof(Vector2Child))
+            ;
+        }
+
+        void IDisposable.Dispose()
+        {
+        }
+
+        private void RunTest(string source)
+        {
+            _engine.Execute(source);
+        }
+
+        public class Vector2
+        {
+            public double X { get; }
+            public double Y { get; }
+            public double SqrMagnitude => X * X + Y * Y;
+            public double Magnitude => Math.Sqrt(SqrMagnitude);
+
+            public Vector2(double x, double y)
+            {
+                X = x;
+                Y = y;
+            }
+
+            public static Vector2 operator +(Vector2 left, Vector2 right) => new Vector2(left.X + right.X, left.Y + right.Y);
+            public static Vector2 operator +(Vector2 left, double right) => new Vector2(left.X + right, left.Y + right);
+            public static Vector2 operator +(string left, Vector2 right) => new Vector2(right.X, right.Y);
+            public static Vector2 operator +(double left, Vector2 right) => new Vector2(right.X + left, right.Y + left);
+            public static Vector2 operator *(Vector2 left, double right) => new Vector2(left.X * right, left.Y * right);
+            public static Vector2 operator /(Vector2 left, double right) => new Vector2(left.X / right, left.Y / right);
+
+            public static bool operator >(Vector2 left, Vector2 right) => left.Magnitude > right.Magnitude;
+            public static bool operator <(Vector2 left, Vector2 right) => left.Magnitude < right.Magnitude;
+            public static bool operator >=(Vector2 left, Vector2 right) => left.Magnitude >= right.Magnitude;
+            public static bool operator <=(Vector2 left, Vector2 right) => left.Magnitude <= right.Magnitude;
+            public static Vector2 operator %(Vector2 left, Vector2 right) => new Vector2(left.X % right.X, left.Y % right.Y);
+            public static double operator &(Vector2 left, Vector2 right) => left.X * right.X + left.Y * right.Y;
+            public static Vector2 operator |(Vector2 left, Vector2 right) => right * ((left & right) / right.SqrMagnitude);
+
+
+            public static double operator +(Vector2 operand) => operand.Magnitude;
+            public static Vector2 operator -(Vector2 operand) => new Vector2(-operand.X, -operand.Y);
+            public static bool operator !(Vector2 operand) => operand.Magnitude == 0;
+            public static Vector2 operator ~(Vector2 operand) => new Vector2(operand.Y, operand.X);
+            public static Vector2 operator ++(Vector2 operand) => new Vector2(operand.X + 1, operand.Y + 1);
+            public static Vector2 operator --(Vector2 operand) => new Vector2(operand.X - 1, operand.Y - 1);
+
+            public static implicit operator Vector3(Vector2 val) => new Vector3(val.X, val.Y, 0);
+            public static bool operator !=(Vector2 left, Vector2 right) => !(left == right);
+            public static bool operator ==(Vector2 left, Vector2 right) => left.X == right.X && left.Y == right.Y;
+            public override bool Equals(object obj) => ReferenceEquals(this, obj);
+            public override int GetHashCode() => X.GetHashCode() + Y.GetHashCode();
+        }
+
+        public class Vector2Child : Vector2
+        {
+            public Vector2Child(double x, double y) : base(x, y) { }
+
+            public static Vector2Child operator +(Vector2Child left, double right) => new Vector2Child(left.X + 2 * right, left.Y + 2 * right);
+        }
+
+        public class Vector3
+        {
+            public double X { get; }
+            public double Y { get; }
+            public double Z { get; }
+
+            public Vector3(double x, double y, double z)
+            {
+                X = x;
+                Y = y;
+                Z = z;
+            }
+
+            public static Vector3 operator +(Vector3 left, double right) => new Vector3(left.X + right, left.Y + right, left.Z + right);
+            public static Vector3 operator +(double left, Vector3 right) => new Vector3(right.X + left, right.Y + left, right.Z + left);
+            public static Vector3 operator +(Vector3 left, Vector3 right) => new Vector3(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
+        }
+
+        [Fact]
+        public void OperatorOverloading_BinaryOperators()
+        {
+            RunTest(@"
+                var v1 = new Vector2(1, 2);
+                var v2 = new Vector2(3, 4);
+                var n = 6;
+
+                var r1 = v1 + v2;
+                equal(4, r1.X);
+                equal(6, r1.Y);
+
+                var r2 = n + v1;
+                equal(7, r2.X);
+                equal(8, r2.Y);
+
+                var r3 = v1 + n;
+                equal(7, r3.X);
+                equal(8, r3.Y);
+
+                var r4 = v1 * n;
+                equal(6, r4.X);
+                equal(12, r4.Y);
+
+                var r5 = v1 / n;
+                equal(1 / 6, r5.X);
+                equal(2 / 6, r5.Y);
+
+                var r6 = v2 % new Vector2(2, 3);
+                equal(1, r6.X);
+                equal(1, r6.Y);
+
+                var r7 = v2 & v1;
+                equal(11, r7);
+
+                var r8 = new Vector2(3, 4) | new Vector2(2, 0);
+                equal(3, r8.X);
+                equal(0, r8.Y);
+
+                
+                var vSmall = new Vector2(3, 4);
+                var vBig = new Vector2(4, 4);
+
+                assert(vSmall < vBig);
+                assert(vSmall <= vBig);
+                assert(vSmall <= vSmall);
+                assert(vBig > vSmall);
+                assert(vBig >= vSmall);
+                assert(vBig >= vBig);
+
+                assertFalse(vSmall > vSmall);
+                assertFalse(vSmall < vSmall);
+                assertFalse(vSmall > vBig);
+                assertFalse(vSmall >= vBig);
+                assertFalse(vBig < vBig);
+                assertFalse(vBig > vBig);
+                assertFalse(vBig < vSmall);
+                assertFalse(vBig <= vSmall);
+            ");
+        }
+
+        [Fact]
+        public void OperatorOverloading_ShouldCoerceTypes()
+        {
+            RunTest(@"
+                var v1 = new Vector2(1, 2);
+                var v2 = new Vector3(4, 5, 6);
+                var res = v1 + v2;
+                equal(5, res.X);
+                equal(7, res.Y);
+                equal(6, res.Z);
+            ");
+        }
+
+        [Fact]
+        public void OperatorOverloading_ShouldWorkForEqualityButNotForStrictEquality()
+        {
+            RunTest(@"
+                var v1 = new Vector2(1, 2);
+                var v2 = new Vector2(1, 2);
+                assert(v1 == v2);
+                assertFalse(v1 != v2);
+                assert(v1 !== v2);
+                assertFalse(v1 === v2);
+
+
+                var z1 = new Vector3(1, 2, 3);
+                var z2 = new Vector3(1, 2, 3);
+                assertFalse(z1 == z2);
+            ");
+        }
+
+        [Fact]
+        public void OperatorOverloading_UnaryOperators()
+        {
+            RunTest(@"
+                var v0 = new Vector2(0, 0);
+                var v = new Vector2(3, 4);
+                var rv = -v;
+                var bv = ~v;
+                
+                assert(!v0);
+                assertFalse(!v);
+
+                equal(0, +v0);
+                equal(5, +v);
+                equal(5, +rv);
+                equal(-3, rv.X);
+                equal(-4, rv.Y);
+
+                equal(4, bv.X);
+                equal(3, bv.Y);
+            ");
+        }
+
+        [Fact]
+        public void OperatorOverloading_IncrementOperatorShouldWork()
+        {
+            RunTest(@"
+                var v = new Vector2(3, 22);
+                var original = v;
+                var pre = ++v;
+                var post = v++;
+
+                equal(3, original.X);
+                equal(4, pre.X);
+                equal(4, post.X);
+                equal(5, v.X);
+
+                var decPre = --v;
+                var decPost = v--;
+
+                equal(4, decPre.X);
+                equal(4, decPost.X);
+                equal(3, v.X);
+            ");
+        }
+
+        [Fact]
+        public void OperatorOverloading_ShouldWorkOnDerivedClasses()
+        {
+            RunTest(@"
+                var v1 = new Vector2Child(1, 2);
+                var v2 = new Vector2Child(3, 4);
+                var n = 5;
+
+                var v1v2 = v1 + v2;
+                var v1n = v1 + n;
+
+                // Uses the (Vector2 + Vector2) operator on the parent class
+                equal(4, v1v2.X);
+                equal(6, v1v2.Y);
+
+                // Uses the (Vector2Child + double) operator on the child class
+                equal(11, v1n.X);
+                equal(12, v1n.Y);
+            ");
+        }
+
+    }
+}

+ 6 - 0
Jint/Extensions/ReflectionExtensions.cs

@@ -44,6 +44,12 @@ namespace Jint.Extensions
                 .Where(m => m.IsExtensionMethod());
         }
 
+        internal static IEnumerable<MethodInfo> GetOperatorOverloadMethods(this Type type)
+        {
+            return type.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
+                .Where(m => m.IsSpecialName);
+        }
+
         private static bool IsExtensionMethod(this MethodBase methodInfo)
         {
             return methodInfo.IsDefined(typeof(ExtensionAttribute), true);

+ 10 - 1
Jint/Options.cs

@@ -24,6 +24,7 @@ namespace Jint
         private DebuggerStatementHandling _debuggerStatementHandling;
         private bool _allowClr;
         private bool _allowClrWrite = true;
+        private bool _allowOperatorOverloading;
         private readonly List<IObjectConverter> _objectConverters = new();
         private Func<Engine, object, ObjectInstance> _wrapObjectHandler;
         private MemberAccessorDelegate _memberAccessor;
@@ -127,7 +128,7 @@ namespace Jint
                 JsValue key = overloads.Key;
                 PropertyDescriptor descriptorWithFallback = null;
                 PropertyDescriptor descriptorWithoutFallback = null;
-                
+
                 if (prototype.HasOwnProperty(key) && prototype.GetOwnProperty(key).Value is ClrFunctionInstance clrFunctionInstance)
                 {
                     descriptorWithFallback = CreateMethodInstancePropertyDescriptor(clrFunctionInstance);
@@ -210,6 +211,12 @@ namespace Jint
             return this;
         }
 
+        public Options AllowOperatorOverloading(bool allow = true)
+        {
+            _allowOperatorOverloading = allow;
+            return this;
+        }
+
         /// <summary>
         /// Exceptions thrown from CLR code are converted to JavaScript errors and
         /// can be used in at try/catch statement. By default these exceptions are bubbled
@@ -335,6 +342,8 @@ namespace Jint
 
         internal bool _IsClrWriteAllowed => _allowClrWrite;
 
+        internal bool _IsOperatorOverloadingAllowed => _allowOperatorOverloading;
+
         internal Predicate<Exception> _ClrExceptionsHandler => _clrExceptionsHandler;
 
         internal List<Assembly> _LookupAssemblies => _lookupAssemblies;

+ 30 - 7
Jint/Runtime/Interop/DefaultTypeConverter.cs

@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Dynamic;
+using System.Linq;
 using System.Linq.Expressions;
 using System.Reflection;
 using Jint.Extensions;
@@ -16,8 +17,10 @@ namespace Jint.Runtime.Interop
 
 #if NETSTANDARD
         private static readonly ConcurrentDictionary<(Type Source, Type Target), bool> _knownConversions = new ConcurrentDictionary<(Type Source, Type Target), bool>();
+        private static readonly ConcurrentDictionary<(Type Source, Type Target), MethodInfo> _knownCastOperators = new ConcurrentDictionary<(Type Source, Type Target), MethodInfo>();
 #else
         private static readonly ConcurrentDictionary<string, bool> _knownConversions = new ConcurrentDictionary<string, bool>();
+        private static readonly ConcurrentDictionary<string, MethodInfo> _knownCastOperators = new ConcurrentDictionary<string, MethodInfo>();
 #endif
 
         private static readonly Type nullableType = typeof(Nullable<>);
@@ -28,7 +31,7 @@ namespace Jint.Runtime.Interop
         private static readonly Type engineType = typeof(Engine);
         private static readonly Type typeType = typeof(Type);
 
-        private static readonly MethodInfo convertChangeType = typeof(Convert).GetMethod("ChangeType", new [] { objectType, typeType, typeof(IFormatProvider) });
+        private static readonly MethodInfo convertChangeType = typeof(Convert).GetMethod("ChangeType", new[] { objectType, typeType, typeof(IFormatProvider) });
         private static readonly MethodInfo jsValueFromObject = jsValueType.GetMethod(nameof(JsValue.FromObject));
         private static readonly MethodInfo jsValueToObject = jsValueType.GetMethod(nameof(JsValue.ToObject));
 
@@ -76,7 +79,7 @@ namespace Jint.Runtime.Interop
             // is the javascript value an ICallable instance ?
             if (valueType == iCallableType)
             {
-                var function = (Func<JsValue, JsValue[], JsValue>)value;
+                var function = (Func<JsValue, JsValue[], JsValue>) value;
 
                 if (typeof(Delegate).IsAssignableFrom(type) && !type.IsAbstract)
                 {
@@ -119,11 +122,11 @@ namespace Jint.Runtime.Interop
                             Expression.Convert(
                                 Expression.Call(
                                     null,
-                                    convertChangeType, 
-                                    Expression.Call(callExpression, jsValueToObject), 
-                                    Expression.Constant(method.ReturnType), 
+                                    convertChangeType,
+                                    Expression.Call(callExpression, jsValueToObject),
+                                    Expression.Constant(method.ReturnType),
                                     Expression.Constant(System.Globalization.CultureInfo.InvariantCulture, typeof(IFormatProvider))
-                                    ), 
+                                    ),
                                 method.ReturnType
                                 ),
                             new ReadOnlyCollection<ParameterExpression>(@params)).Compile();
@@ -168,7 +171,7 @@ namespace Jint.Runtime.Interop
                 }
 
                 // reference types - return null if no valid constructor is found
-                if(!type.IsValueType)
+                if (!type.IsValueType)
                 {
                     var found = false;
                     foreach (var constructor in constructors)
@@ -210,6 +213,26 @@ namespace Jint.Runtime.Interop
                 return obj;
             }
 
+            if (_engine.Options._IsOperatorOverloadingAllowed)
+            {
+#if NETSTANDARD
+                var key = (valueType, type);
+#else
+                var key = $"{valueType}->{type}";
+#endif
+
+                var castOperator = _knownCastOperators.GetOrAdd(key, _ =>
+                    valueType.GetOperatorOverloadMethods()
+                    .Concat(type.GetOperatorOverloadMethods())
+                    .FirstOrDefault(m => type.IsAssignableFrom(m.ReturnType)
+                        && (m.Name == "op_Implicit" || m.Name == "op_Explicit")));
+
+                if (castOperator != null)
+                {
+                    return castOperator.Invoke(null, new[] { value });
+                }
+            }
+
             return System.Convert.ChangeType(value, type, formatProvider);
         }
 

+ 47 - 1
Jint/Runtime/Interop/MethodDescriptor.cs

@@ -1,3 +1,4 @@
+using Jint.Native;
 using System;
 using System.Collections.Generic;
 using System.Reflection;
@@ -7,7 +8,7 @@ namespace Jint.Runtime.Interop
 {
     internal class MethodDescriptor
     {
-        private MethodDescriptor(MethodBase method)
+        internal MethodDescriptor(MethodBase method)
         {
             Method = method;
             Parameters = method.GetParameters();
@@ -100,5 +101,50 @@ namespace Jint.Runtime.Interop
 
             return descriptors;
         }
+
+        public JsValue Call(Engine _engine, object instance, JsValue[] arguments)
+        {
+            var parameters = new object[arguments.Length];
+            var methodParameters = Parameters;
+            try
+            {
+                for (var i = 0; i < arguments.Length; i++)
+                {
+                    var parameterType = methodParameters[i].ParameterType;
+
+                    if (typeof(JsValue).IsAssignableFrom(parameterType))
+                    {
+                        parameters[i] = arguments[i];
+                    }
+                    else
+                    {
+                        parameters[i] = _engine.ClrTypeConverter.Convert(
+                            arguments[i].ToObject(),
+                            parameterType,
+                            System.Globalization.CultureInfo.InvariantCulture);
+                    }
+                }
+
+                if (Method is MethodInfo m)
+                {
+                    var retVal = m.Invoke(instance, parameters);
+                    return JsValue.FromObject(_engine, retVal);
+                }
+                else if (Method is ConstructorInfo c)
+                {
+                    var retVal = c.Invoke(parameters);
+                    return JsValue.FromObject(_engine, retVal);
+                }
+                else
+                {
+                    throw new Exception("Method is unknown type");
+                }
+            }
+            catch (TargetInvocationException exception)
+            {
+                ExceptionHelper.ThrowMeaningfulException(_engine, exception);
+                return null;
+            }
+        }
     }
 }

+ 4 - 32
Jint/Runtime/Interop/TypeReference.cs

@@ -64,40 +64,12 @@ namespace Jint.Runtime.Interop
             foreach (var tuple in TypeConverter.FindBestMatch(_engine, constructors, _ => arguments))
             {
                 var method = tuple.Item1;
+                var retVal = method.Call(Engine, null, arguments);
+                var result = TypeConverter.ToObject(Engine, retVal);
 
-                var parameters = new object[arguments.Length];
-                var methodParameters = method.Parameters;
-                try
-                {
-                    for (var i = 0; i < arguments.Length; i++)
-                    {
-                        var parameterType = methodParameters[i].ParameterType;
-
-                        if (typeof(JsValue).IsAssignableFrom(parameterType))
-                        {
-                            parameters[i] = arguments[i];
-                        }
-                        else
-                        {
-                            parameters[i] = Engine.ClrTypeConverter.Convert(
-                                arguments[i].ToObject(),
-                                parameterType,
-                                CultureInfo.InvariantCulture);
-                        }
-                    }
-
-                    var constructor = (ConstructorInfo) method.Method;
-                    var instance = constructor.Invoke(parameters);
-                    var result = TypeConverter.ToObject(Engine, FromObject(Engine, instance));
-
-                    // todo: cache method info
+                // todo: cache method info
 
-                    return result;
-                }
-                catch (TargetInvocationException exception)
-                {
-                    ExceptionHelper.ThrowMeaningfulException(_engine, exception);
-                }
+                return result;
             }
 
             return ExceptionHelper.ThrowTypeError<ObjectInstance>(_engine, "No public methods with the specified arguments were found.");

+ 137 - 1
Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs

@@ -1,5 +1,9 @@
 using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Reflection;
 using Esprima.Ast;
+using Jint.Extensions;
 using Jint.Native;
 using Jint.Native.Object;
 using Jint.Runtime.Interop;
@@ -8,6 +12,13 @@ namespace Jint.Runtime.Interpreter.Expressions
 {
     internal abstract class JintBinaryExpression : JintExpression
     {
+#if NETSTANDARD
+        private static readonly ConcurrentDictionary<(string OperatorName, System.Type Left, System.Type Right), MethodDescriptor> _knownOperators = 
+            new ConcurrentDictionary<(string OperatorName, System.Type Left, System.Type Right), MethodDescriptor>();
+#else
+        private static readonly ConcurrentDictionary<string, MethodDescriptor> _knownOperators = new ConcurrentDictionary<string, MethodDescriptor>();
+#endif
+
         private readonly JintExpression _left;
         private readonly JintExpression _right;
 
@@ -17,6 +28,46 @@ namespace Jint.Runtime.Interpreter.Expressions
             _right = Build(engine, expression.Right);
         }
 
+        protected bool TryOperatorOverloading(string clrName, out object result)
+        {
+            var leftValue = _left.GetValue();
+            var rightValue = _right.GetValue();
+
+            var left = leftValue.ToObject();
+            var right = rightValue.ToObject();
+
+            if (left != null && right != null)
+            {
+                var leftType = left.GetType();
+                var rightType = right.GetType();
+                var arguments = new[] { leftValue, rightValue };
+
+#if NETSTANDARD
+                var key = (clrName, leftType, rightType);
+#else
+                var key = $"{clrName}->{leftType}->{rightType}";
+#endif
+                var method = _knownOperators.GetOrAdd(key, _ =>
+                {
+                    var leftMethods = leftType.GetOperatorOverloadMethods();
+                    var rightMethods = rightType.GetOperatorOverloadMethods();
+
+                    var methods = leftMethods.Concat(rightMethods).Where(x => x.Name == clrName && x.GetParameters().Length == 2);
+                    var _methods = MethodDescriptor.Build(methods.ToArray());
+
+                    return TypeConverter.FindBestMatch(_engine, _methods, _ => arguments).FirstOrDefault()?.Item1;
+                });
+
+                if (method != null)
+                {
+                    result = method.Call(_engine, null, arguments);
+                    return true;
+                }
+            }
+            result = null;
+            return false;
+        }
+
         internal static JintExpression Build(Engine engine, BinaryExpression expression)
         {
             JintBinaryExpression result;
@@ -210,6 +261,12 @@ namespace Jint.Runtime.Interpreter.Expressions
 
             protected override object EvaluateInternal()
             {
+                if (_engine.Options._IsOperatorOverloadingAllowed
+                    && TryOperatorOverloading("op_LessThan", out var opResult))
+                {
+                    return opResult;
+                }
+
                 var left = _left.GetValue();
                 var right = _right.GetValue();
                 var value = Compare(left, right);
@@ -228,6 +285,12 @@ namespace Jint.Runtime.Interpreter.Expressions
 
             protected override object EvaluateInternal()
             {
+                if (_engine.Options._IsOperatorOverloadingAllowed
+                    && TryOperatorOverloading("op_GreaterThan", out var opResult))
+                {
+                    return opResult;
+                }
+
                 var left = _left.GetValue();
                 var right = _right.GetValue();
                 var value = Compare(right, left, false);
@@ -246,6 +309,12 @@ namespace Jint.Runtime.Interpreter.Expressions
 
             protected override object EvaluateInternal()
             {
+                if (_engine.Options._IsOperatorOverloadingAllowed
+                    && TryOperatorOverloading("op_Addition", out var opResult))
+                {
+                    return opResult;
+                }
+
                 var left = _left.GetValue();
                 var right = _right.GetValue();
 
@@ -269,6 +338,12 @@ namespace Jint.Runtime.Interpreter.Expressions
 
             protected override object EvaluateInternal()
             {
+                if (_engine.Options._IsOperatorOverloadingAllowed
+                    && TryOperatorOverloading("op_Subtraction", out var opResult))
+                {
+                    return opResult;
+                }
+
                 var left = _left.GetValue();
                 var right = _right.GetValue();
 
@@ -286,6 +361,12 @@ namespace Jint.Runtime.Interpreter.Expressions
 
             protected override object EvaluateInternal()
             {
+                if (_engine.Options._IsOperatorOverloadingAllowed
+                    && TryOperatorOverloading("op_Multiply", out var opResult))
+                {
+                    return opResult;
+                }
+
                 var left = _left.GetValue();
                 var right = _right.GetValue();
 
@@ -311,6 +392,12 @@ namespace Jint.Runtime.Interpreter.Expressions
 
             protected override object EvaluateInternal()
             {
+                if (_engine.Options._IsOperatorOverloadingAllowed
+                    && TryOperatorOverloading("op_Division", out var opResult))
+                {
+                    return opResult;
+                }
+
                 var left = _left.GetValue();
                 var right = _right.GetValue();
 
@@ -329,6 +416,12 @@ namespace Jint.Runtime.Interpreter.Expressions
 
             protected override object EvaluateInternal()
             {
+                if (_engine.Options._IsOperatorOverloadingAllowed
+                    && TryOperatorOverloading(_invert ? "op_Inequality" : "op_Equality", out var opResult))
+                {
+                    return opResult;
+                }
+
                 var left = _left.GetValue();
                 var right = _right.GetValue();
 
@@ -349,6 +442,12 @@ namespace Jint.Runtime.Interpreter.Expressions
 
             protected override object EvaluateInternal()
             {
+                if (_engine.Options._IsOperatorOverloadingAllowed
+                    && TryOperatorOverloading(_leftFirst ? "op_GreaterThanOrEqual" : "op_LessThanOrEqual", out var opResult))
+                {
+                    return opResult;
+                }
+
                 var leftValue = _left.GetValue();
                 var rightValue = _right.GetValue();
 
@@ -419,6 +518,12 @@ namespace Jint.Runtime.Interpreter.Expressions
 
             protected override object EvaluateInternal()
             {
+                if (_engine.Options._IsOperatorOverloadingAllowed
+                    && TryOperatorOverloading("op_Modulus", out var opResult))
+                {
+                    return opResult;
+                }
+
                 var left = _left.GetValue();
                 var right = _right.GetValue();
 
@@ -437,11 +542,36 @@ namespace Jint.Runtime.Interpreter.Expressions
                     return Undefined.Instance;
                 }
 
-                return JsNumber.Create(TypeConverter.ToNumber(left) % TypeConverter.ToNumber(right));            }
+                return JsNumber.Create(TypeConverter.ToNumber(left) % TypeConverter.ToNumber(right));
+            }
         }
 
         private sealed class BitwiseBinaryExpression : JintBinaryExpression
         {
+            private string OperatorClrName
+            {
+                get
+                {
+                    switch (_operator)
+                    {
+                        case BinaryOperator.BitwiseAnd:
+                            return "op_BitwiseAnd";
+                        case BinaryOperator.BitwiseOr:
+                            return "op_BitwiseOr";
+                        case BinaryOperator.BitwiseXOr:
+                            return "op_ExclusiveOr";
+                        case BinaryOperator.LeftShift:
+                            return "op_LeftShift";
+                        case BinaryOperator.RightShift:
+                            return "op_RightShift";
+                        case BinaryOperator.UnsignedRightShift:
+                            return "op_UnsignedRightShift";
+                        default:
+                            return null;
+                    }
+                }
+            }
+
             private readonly BinaryOperator _operator;
 
             public BitwiseBinaryExpression(Engine engine, BinaryExpression expression) : base(engine, expression)
@@ -451,6 +581,12 @@ namespace Jint.Runtime.Interpreter.Expressions
 
             protected override object EvaluateInternal()
             {
+                if (_engine.Options._IsOperatorOverloadingAllowed
+                    && TryOperatorOverloading(OperatorClrName, out var opResult))
+                {
+                    return opResult;
+                }
+
                 var left = _left.GetValue();
                 var right = _right.GetValue();
 

+ 78 - 2
Jint/Runtime/Interpreter/Expressions/JintUnaryExpression.cs

@@ -1,12 +1,24 @@
 using Esprima.Ast;
+using Jint.Extensions;
 using Jint.Native;
 using Jint.Runtime.Environments;
+using Jint.Runtime.Interop;
 using Jint.Runtime.References;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Reflection;
 
 namespace Jint.Runtime.Interpreter.Expressions
 {
     internal sealed class JintUnaryExpression : JintExpression
     {
+#if NETSTANDARD
+        private static readonly ConcurrentDictionary<(string OperatorName, System.Type Operand), MethodDescriptor> _knownOperators = 
+            new ConcurrentDictionary<(string OperatorName, System.Type Operand), MethodDescriptor>();
+#else
+        private static readonly ConcurrentDictionary<string, MethodDescriptor> _knownOperators = new ConcurrentDictionary<string, MethodDescriptor>();
+#endif
+
         private readonly JintExpression _argument;
         private readonly UnaryOperator _operator;
 
@@ -42,6 +54,34 @@ namespace Jint.Runtime.Interpreter.Expressions
 
         protected override object EvaluateInternal()
         {
+            if (_engine.Options._IsOperatorOverloadingAllowed)
+            {
+                string operatorClrName = null;
+                switch (_operator)
+                {
+                    case UnaryOperator.Plus:
+                        operatorClrName = "op_UnaryPlus";
+                        break;
+                    case UnaryOperator.Minus:
+                        operatorClrName = "op_UnaryNegation";
+                        break;
+                    case UnaryOperator.BitwiseNot:
+                        operatorClrName = "op_OnesComplement";
+                        break;
+                    case UnaryOperator.LogicalNot:
+                        operatorClrName = "op_LogicalNot";
+                        break;
+                    default:
+                        break;
+                }
+
+                if (operatorClrName != null &&
+                    TryOperatorOverloading(_engine, _argument.GetValue(), operatorClrName, out var result))
+                {
+                    return result;
+                }
+            }
+
             switch (_operator)
             {
                 case UnaryOperator.Plus:
@@ -83,9 +123,9 @@ namespace Jint.Runtime.Interpreter.Expressions
                         {
                             ExceptionHelper.ThrowReferenceError(_engine, r);
                         }
-                        
+
                         var o = TypeConverter.ToObject(_engine, r.GetBase());
-                        var deleteStatus  = o.Delete(r.GetReferencedName());
+                        var deleteStatus = o.Delete(r.GetReferencedName());
                         if (!deleteStatus && r.IsStrictReference())
                         {
                             ExceptionHelper.ThrowTypeError(_engine);
@@ -169,5 +209,41 @@ namespace Jint.Runtime.Interpreter.Expressions
             var n = TypeConverter.ToNumber(minusValue);
             return JsNumber.Create(double.IsNaN(n) ? double.NaN : n * -1);
         }
+
+        internal static bool TryOperatorOverloading(Engine _engine, JsValue value, string clrName, out JsValue result)
+        {
+            var operand = value.ToObject();
+
+            if (operand != null)
+            {
+                var operandType = operand.GetType();
+                var arguments = new[] { value };
+
+#if NETSTANDARD
+                var key = (clrName, operandType);
+#else
+                var key = $"{clrName}->{operandType}";
+#endif
+                var method = _knownOperators.GetOrAdd(key, _ =>
+                {
+                    var foundMethod = operandType.GetOperatorOverloadMethods()
+                        .FirstOrDefault(x => x.Name == clrName && x.GetParameters().Length == 1);
+
+                    if (foundMethod != null)
+                    {
+                        return new MethodDescriptor(foundMethod);
+                    }
+                    return null;
+                });
+
+                if (method != null)
+                {
+                    result = method.Call(_engine, null, arguments);
+                    return true;
+                }
+            }
+            result = null;
+            return false;
+        }
     }
 }

+ 39 - 7
Jint/Runtime/Interpreter/Expressions/JintUpdateExpression.cs

@@ -30,7 +30,7 @@ namespace Jint.Runtime.Interpreter.Expressions
             }
             else if (expression.Operator == UnaryOperator.Decrement)
             {
-                _change = - 1;
+                _change = -1;
             }
             else
             {
@@ -61,16 +61,32 @@ namespace Jint.Runtime.Interpreter.Expressions
 
             var value = _engine.GetValue(reference, false);
             var isInteger = value._type == InternalTypes.Integer;
-            var newValue = isInteger
-                ? JsNumber.Create(value.AsInteger() + _change)
-                : JsNumber.Create(TypeConverter.ToNumber(value) + _change);
+
+            JsValue newValue = null;
+
+            var operatorOverloaded = false;
+            if (_engine.Options._IsOperatorOverloadingAllowed)
+            {
+                if (JintUnaryExpression.TryOperatorOverloading(_engine, _argument.GetValue(), _change > 0 ? "op_Increment" : "op_Decrement", out var result))
+                {
+                    operatorOverloaded = true;
+                    newValue = result;
+                }
+            }
+
+            if (!operatorOverloaded)
+            {
+                newValue = isInteger
+                    ? JsNumber.Create(value.AsInteger() + _change)
+                    : JsNumber.Create(TypeConverter.ToNumber(value) + _change);
+            }
 
             _engine.PutValue(reference, newValue);
             _engine._referencePool.Return(reference);
 
             return _prefix
                 ? newValue
-                : (isInteger ? value : JsNumber.Create(TypeConverter.ToNumber(value)));
+                : (isInteger || operatorOverloaded ? value : JsNumber.Create(TypeConverter.ToNumber(value)));
         }
 
         private JsValue UpdateIdentifier()
@@ -91,14 +107,30 @@ namespace Jint.Runtime.Interpreter.Expressions
                 }
 
                 var isInteger = value._type == InternalTypes.Integer;
-                var newValue = isInteger
+
+                JsValue newValue = null;
+
+                var operatorOverloaded = false;
+                if (_engine.Options._IsOperatorOverloadingAllowed)
+                {
+                    if (JintUnaryExpression.TryOperatorOverloading(_engine, _argument.GetValue(), _change > 0 ? "op_Increment" : "op_Decrement", out var result))
+                    {
+                        operatorOverloaded = true;
+                        newValue = result;
+                    }
+                }
+
+                if (!operatorOverloaded)
+                {
+                    newValue = isInteger
                     ? JsNumber.Create(value.AsInteger() + _change)
                     : JsNumber.Create(TypeConverter.ToNumber(value) + _change);
+                }
 
                 environmentRecord.SetMutableBinding(name.Key.Name, newValue, strict);
                 return _prefix
                     ? newValue
-                    : (isInteger ? value : JsNumber.Create(TypeConverter.ToNumber(value)));
+                    : (isInteger || operatorOverloaded ? value : JsNumber.Create(TypeConverter.ToNumber(value)));
             }
 
             return null;

+ 51 - 19
Jint/Runtime/TypeConverter.cs

@@ -1,8 +1,11 @@
 using System;
 using System.Collections.Generic;
 using System.Globalization;
+using System.Linq;
+using System.Reflection;
 using System.Runtime.CompilerServices;
 using Esprima.Ast;
+using Jint.Extensions;
 using Jint.Native;
 using Jint.Native.Number;
 using Jint.Native.Number.Dtoa;
@@ -108,7 +111,7 @@ namespace Jint.Runtime
                 }
             }
 
-            return OrdinaryToPrimitive(oi, preferredType == Types.None ? Types.Number :  preferredType);
+            return OrdinaryToPrimitive(oi, preferredType == Types.None ? Types.Number : preferredType);
         }
 
         /// <summary>
@@ -118,7 +121,7 @@ namespace Jint.Runtime
         {
             JsString property1;
             JsString property2;
-            
+
             if (hint == Types.String)
             {
                 property1 = (JsString) "toString";
@@ -335,7 +338,7 @@ namespace Jint.Runtime
 
             return integer;
         }
-        
+
         /// <summary>
         /// https://tc39.es/ecma262/#sec-tointeger
         /// </summary>
@@ -398,7 +401,7 @@ namespace Jint.Runtime
         /// </summary>
         public static ushort ToUint16(JsValue o)
         {
-            return  o._type == InternalTypes.Integer
+            return o._type == InternalTypes.Integer
                 ? (ushort) (uint) o.AsInteger()
                 : (ushort) (uint) ToNumber(o);
         }
@@ -446,7 +449,7 @@ namespace Jint.Runtime
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         internal static string ToString(double d)
         {
-            if (d > long.MinValue && d < long.MaxValue  && Math.Abs(d % 1) <= DoubleIsIntegerTolerance)
+            if (d > long.MinValue && d < long.MaxValue && Math.Abs(d % 1) <= DoubleIsIntegerTolerance)
             {
                 // we are dealing with integer that can be cached
                 return ToString((long) d);
@@ -585,7 +588,7 @@ namespace Jint.Runtime
             {
                 var parameterInfos = m.Parameters;
                 var arguments = argumentProvider(m);
-                if (arguments.Length <= parameterInfos.Length 
+                if (arguments.Length <= parameterInfos.Length
                     && arguments.Length >= parameterInfos.Length - m.ParameterDefaultValuesCount)
                 {
                     if (methods.Length == 0 && arguments.Length == 0)
@@ -604,28 +607,35 @@ namespace Jint.Runtime
                 yield break;
             }
 
+            List<Tuple<int, Tuple<MethodDescriptor, JsValue[]>>> scoredList = null;
+
             foreach (var tuple in matchingByParameterCount)
             {
-                var perfectMatch = true;
+                var score = 0;
                 var parameters = tuple.Item1.Parameters;
                 var arguments = tuple.Item2;
                 for (var i = 0; i < arguments.Length; i++)
                 {
                     var jsValue = arguments[i];
                     var arg = jsValue.ToObject();
+                    var argType = arg?.GetType();
                     var paramType = parameters[i].ParameterType;
                     if (arg == null)
                     {
                         if (!TypeIsNullable(paramType))
                         {
-                            perfectMatch = false;
-                            break;
+                            score -= 10000;
                         }
                     }
-                    else if (arg.GetType() != paramType)
+                    else if (paramType == typeof(JsValue))
+                    {
+                        // JsValue is convertible to. But it is still not a perfect match
+                        score -= 1;
+                    }
+                    else if (argType != paramType)
                     {
                         // check if we can do conversion from int value to enum
-                        if (paramType.IsEnum && 
+                        if (paramType.IsEnum &&
                             jsValue is JsNumber jsNumber
                             && jsNumber.IsInteger()
                             && Enum.IsDefined(paramType, jsNumber.AsInteger()))
@@ -634,24 +644,46 @@ namespace Jint.Runtime
                         }
                         else
                         {
-                            // no can do
-                            perfectMatch = false;
-                            break;
+                            if (paramType.IsAssignableFrom(argType))
+                            {
+                                score -= 10;
+                            }
+                            else
+                            {
+                                if (argType.GetOperatorOverloadMethods()
+                                  .Any(m => paramType.IsAssignableFrom(m.ReturnType) &&
+                                    (m.Name == "op_Implicit" ||
+                                    m.Name == "op_Explicit")))
+                                {
+                                    score -= 100;
+                                }
+                                else
+                                {
+                                    score -= 1000;
+                                }
+                            }
                         }
                     }
                 }
 
-                if (perfectMatch)
+                if (score == 0)
                 {
-                    yield return new Tuple<MethodDescriptor, JsValue[]>(tuple.Item1, arguments);
+                    yield return Tuple.Create(tuple.Item1, arguments);
                     yield break;
                 }
+                else
+                {
+                    scoredList ??= new List<Tuple<int, Tuple<MethodDescriptor, JsValue[]>>>();
+                    scoredList.Add(Tuple.Create(score, tuple));
+                }
             }
 
-            for (var i = 0; i < matchingByParameterCount.Count; i++)
+            if (scoredList != null)
             {
-                var tuple = matchingByParameterCount[i];
-                yield return new Tuple<MethodDescriptor, JsValue[]>(tuple.Item1, tuple.Item2);
+                foreach (var item in scoredList.OrderByDescending(x => x.Item1))
+                {
+                    yield return item.Item2;
+                }
             }
         }