Selaa lähdekoodia

Improve operator overloading logic (#1011)

* cleanup and refactor code
* use struct record instead of value tuples and custom code
* prefer more specific types over object
* prefer CLR array against JS array match over changing types
Marko Lahma 3 vuotta sitten
vanhempi
commit
1cf0e2bfb4

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

@@ -1,5 +1,7 @@
 using Jint.Native;
 using System;
+using System.Dynamic;
+using Jint.Runtime.Interop;
 using Xunit;
 
 namespace Jint.Tests.Runtime
@@ -83,5 +85,35 @@ namespace Jint.Tests.Runtime
                 equal('int:string', tc[10] + ':' + tc['Whistler']);
             ");
         }
+
+        [Fact]
+        public void ShouldFavorOtherOverloadsOverObjectParameter()
+        {
+            var engine = new Engine(cfg => cfg.AllowOperatorOverloading());
+            engine.SetValue("Class1", TypeReference.CreateTypeReference<Class1>(engine));
+            engine.SetValue("Class2", TypeReference.CreateTypeReference<Class2>(engine));
+
+            Assert.Equal("Class1.Double[]", engine.Evaluate("Class1.Print([ 1, 2 ]);"));
+            Assert.Equal("Class1.ExpandoObject", engine.Evaluate("Class1.Print({ x: 1, y: 2 });"));
+            Assert.Equal("Class1.Int32", engine.Evaluate("Class1.Print(5);"));
+            Assert.Equal("Class2.Double[]", engine.Evaluate("Class2.Print([ 1, 2 ]); "));
+            Assert.Equal("Class2.ExpandoObject", engine.Evaluate("Class2.Print({ x: 1, y: 2 });"));
+            Assert.Equal("Class2.Int32", engine.Evaluate("Class2.Print(5);"));
+        }
+
+        private struct Class1
+        {
+            public static string Print(ExpandoObject eo) => nameof(Class1) + "." + nameof(ExpandoObject);
+            public static string Print(double[] a) => nameof(Class1) + "." + nameof(Double) + "[]";
+            public static string Print(int i) => nameof(Class1) + "." + nameof(Int32);
+        }
+
+        private struct Class2
+        {
+            public static string Print(ExpandoObject eo) => nameof(Class2) + "." + nameof(ExpandoObject);
+            public static string Print(double[] a) => nameof(Class2) + "." + nameof(Double) + "[]";
+            public static string Print(int i) => nameof(Class2) + "." + nameof(Int32);
+            public static string Print(object o) => nameof(Class2) + "." + nameof(Object);
+        }
     }
 }

+ 38 - 7
Jint.Tests/Runtime/OperatorOverloadingTests.cs

@@ -1,9 +1,11 @@
 using System;
+using Jint.Native.String;
+using Jint.Runtime.Interop;
 using Xunit;
 
 namespace Jint.Tests.Runtime
 {
-    public class OperatorOverloadingTests : IDisposable
+    public class OperatorOverloadingTests
     {
         private readonly Engine _engine;
 
@@ -17,12 +19,7 @@ namespace Jint.Tests.Runtime
                 .SetValue("equal", new Action<object, object>(Assert.Equal))
                 .SetValue("Vector2", typeof(Vector2))
                 .SetValue("Vector3", typeof(Vector3))
-                .SetValue("Vector2Child", typeof(Vector2Child))
-            ;
-        }
-
-        void IDisposable.Dispose()
-        {
+                .SetValue("Vector2Child", typeof(Vector2Child));
         }
 
         private void RunTest(string source)
@@ -99,6 +96,29 @@ namespace Jint.Tests.Runtime
             public static Vector3 operator +(Vector3 left, Vector3 right) => new Vector3(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
         }
 
+        private struct Vector2D
+        {
+            public double X { get; set; }
+            public double Y { get; set; }
+
+            public Vector2D(double x, double y)
+            {
+                X = x;
+                Y = y;
+            }
+
+
+            public static Vector2D operator +(Vector2D lhs, Vector2D rhs)
+            {
+                return new Vector2D(lhs.X + rhs.X, lhs.Y + rhs.Y);
+            }
+
+            public override string ToString()
+            {
+                return $"({X}, {Y})";
+            }
+        }
+
         [Fact]
         public void OperatorOverloading_BinaryOperators()
         {
@@ -339,5 +359,16 @@ namespace Jint.Tests.Runtime
             ");
         }
 
+        [Fact]
+        public void ShouldAllowStringConcatenateForOverloaded()
+        {
+            var engine = new Engine(cfg => cfg.AllowOperatorOverloading());
+            engine.SetValue("Vector2D", TypeReference.CreateTypeReference<Vector2D>(engine));
+            engine.SetValue("log", new Action<object>(Console.WriteLine));
+
+            engine.Evaluate("let v1 = new Vector2D(1, 2);");
+            Assert.Equal("(1, 2)", engine.Evaluate("new String(v1)").As<StringInstance>().PrimitiveValue.ToString());
+            Assert.Equal("### (1, 2) ###", engine.Evaluate("'### ' + v1 + ' ###'"));
+        }
     }
 }

+ 1 - 11
Jint/EsprimaExtensions.cs

@@ -256,16 +256,6 @@ namespace Jint
             return new Record(property, closure);
         }
 
-        internal readonly struct Record
-        {
-            public Record(JsValue key, ScriptFunctionInstance closure)
-            {
-                Key = key;
-                Closure = closure;
-            }
-
-            public readonly JsValue Key;
-            public readonly ScriptFunctionInstance Closure;
-        }
+        internal readonly record struct Record(JsValue Key, ScriptFunctionInstance Closure);
     }
 }

+ 11 - 21
Jint/Native/Array/ArrayInstance.cs

@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System.Collections;
+using System.Collections.Generic;
 using System.Runtime.CompilerServices;
 
 using Jint.Native.Object;
@@ -8,7 +9,7 @@ using Jint.Runtime.Descriptors;
 
 namespace Jint.Native.Array
 {
-    public class ArrayInstance : ObjectInstance
+    public class ArrayInstance : ObjectInstance, IEnumerable<JsValue>
     {
         internal PropertyDescriptor _length;
 
@@ -684,11 +685,14 @@ namespace Jint.Native.Array
             var length = GetLength();
             for (uint i = 0; i < length; i++)
             {
-                if (TryGetValue(i, out JsValue outValue))
-                {
-                    yield return outValue;
-                }
-            };
+                TryGetValue(i, out var outValue);
+                yield return outValue;
+            }
+        }
+
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return GetEnumerator();
         }
 
         internal uint Push(JsValue[] arguments)
@@ -853,20 +857,6 @@ namespace Jint.Native.Array
             }
         }
 
-        internal ArrayInstance ToArray(Engine engine)
-        {
-            var length = GetLength();
-            var array = _engine.Realm.Intrinsics.Array.ConstructFast(length);
-            for (uint i = 0; i < length; i++)
-            {
-                if (TryGetValue(i, out var kValue))
-                {
-                    array.SetIndexValue(i, kValue, updateLength: false);
-                }
-            }
-            return array;
-        }
-
         /// <summary>
         /// Fast path for concatenating sane-sized arrays, we assume size has been calculated.
         /// </summary>

+ 1 - 19
Jint/Native/Date/DatePrototype.cs

@@ -1177,26 +1177,8 @@ namespace Jint.Native.Date
         {
             return IsFinite(value1) && IsFinite(value2) &&  IsFinite(value3) && IsFinite(value4);
         }
-        private readonly struct Date
-        {
-            public Date(int year, int month, int day)
-            {
-                Year = year;
-                Month = month;
-                Day = day;
-            }
-
-            public readonly int Year;
-            public readonly int Month;
-            public readonly int Day;
 
-            public void Deconstruct(out int year, out int month, out int day)
-            {
-                year = Year;
-                month = Month;
-                day = Day;
-            }
-        }
+        private readonly record struct Date(int Year, int Month, int Day);
 
         private static readonly int[] kDaysInMonths = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
 

+ 2 - 12
Jint/Native/JsNull.cs

@@ -26,22 +26,12 @@ namespace Jint.Native
                 return true;
             }
 
-            if (!(obj is JsNull s))
-            {
-                return false;
-            }
-
-            return Equals(s);
+            return obj is JsNull s && Equals(s);
         }
 
         public bool Equals(JsNull other)
         {
-            if (ReferenceEquals(null, other))
-            {
-                return false;
-            }
-
-            return true;
+            return !ReferenceEquals(null, other);
         }
     }
 }

+ 2 - 12
Jint/Native/JsUndefined.cs

@@ -26,22 +26,12 @@ namespace Jint.Native
                 return true;
             }
 
-            if (!(obj is JsUndefined s))
-            {
-                return false;
-            }
-
-            return Equals(s);
+            return obj is JsUndefined s && Equals(s);
         }
 
         public bool Equals(JsUndefined other)
         {
-            if (ReferenceEquals(null, other))
-            {
-                return false;
-            }
-
-            return true;
+            return !ReferenceEquals(null, other);
         }
     }
 }

+ 14 - 21
Jint/Native/Object/ObjectInstance.cs

@@ -885,11 +885,6 @@ namespace Jint.Native.Object
         {
         }
 
-        public override string ToString()
-        {
-            return TypeConverter.ToString(this);
-        }
-
         public override object ToObject()
         {
             return ToObject(new ObjectTraverseStack(_engine));
@@ -897,37 +892,30 @@ namespace Jint.Native.Object
 
         private object ToObject(ObjectTraverseStack stack)
         {
-            stack.Enter(this);
             if (this is IObjectWrapper wrapper)
             {
-                stack.Exit();
                 return wrapper.Target;
             }
 
+            stack.Enter(this);
             object converted = null;
             switch (Class)
             {
                 case ObjectClass.Array:
                     if (this is ArrayInstance arrayInstance)
                     {
-                        var len = arrayInstance.Length;
-                        var result = new object[len];
-                        for (uint k = 0; k < len; k++)
+                        var result = new object[arrayInstance.Length];
+                        for (uint i = 0; i < result.Length; i++)
                         {
-                            var pk = TypeConverter.ToJsString(k);
-                            var kpresent = arrayInstance.HasProperty(pk);
-                            if (kpresent)
+                            var value = arrayInstance[i];
+                            object valueToSet = null;
+                            if (!value.IsUndefined())
                             {
-                                var kvalue = arrayInstance.Get(k);
-                                var value = kvalue is ObjectInstance oi
+                                valueToSet = value is ObjectInstance oi
                                     ? oi.ToObject(stack)
-                                    : kvalue.ToObject();
-                                result[k] = value;
-                            }
-                            else
-                            {
-                                result[k] = null;
+                                    : value.ToObject();
                             }
+                            result[i] = valueToSet;
                         }
                         converted = result;
                     }
@@ -1356,5 +1344,10 @@ namespace Jint.Native.Object
 
             return false;
         }
+
+        public override string ToString()
+        {
+            return TypeConverter.ToString(this);
+        }
     }
 }

+ 2 - 12
Jint/Runtime/Environments/PrivateEnvironmentRecord.cs

@@ -8,19 +8,9 @@ namespace Jint.Runtime.Environments
     internal sealed class PrivateEnvironmentRecord
     {
         private readonly PrivateEnvironmentRecord _outerPrivateEnvironment;
-        private List<PrivateName> _names = new List<PrivateName>();
+        private readonly List<PrivateName> _names = new();
 
-        private readonly struct PrivateName
-        {
-            public PrivateName(string name, string description)
-            {
-                Name = name;
-                Description = description;
-            }
-
-            public readonly string Name;
-            public readonly string Description;
-        }
+        private readonly record struct PrivateName(string Name, string Description);
 
         public PrivateEnvironmentRecord(PrivateEnvironmentRecord outerPrivEnv)
         {

+ 0 - 38
Jint/Runtime/Interop/ClrPropertyDescriptorFactoriesKey.cs

@@ -1,38 +0,0 @@
-using System;
-
-namespace Jint.Runtime.Interop
-{
-    internal readonly struct ClrPropertyDescriptorFactoriesKey : IEquatable<ClrPropertyDescriptorFactoriesKey>
-    {
-        public ClrPropertyDescriptorFactoriesKey(Type type, Key propertyName)
-        {
-            Type = type;
-            PropertyName = propertyName;
-        }
-
-        private readonly Type Type;
-        private readonly Key PropertyName;
-
-        public bool Equals(ClrPropertyDescriptorFactoriesKey other)
-        {
-            return Type == other.Type && PropertyName == other.PropertyName;
-        }
-
-        public override bool Equals(object obj)
-        {
-            if (ReferenceEquals(null, obj))
-            {
-                return false;
-            }
-            return obj is ClrPropertyDescriptorFactoriesKey other && Equals(other);
-        }
-
-        public override int GetHashCode()
-        {
-            unchecked
-            {
-                return (Type.GetHashCode() * 397) ^ PropertyName.GetHashCode();
-            }
-        }
-    }
-}

+ 11 - 25
Jint/Runtime/Interop/DefaultTypeConverter.cs

@@ -1,8 +1,6 @@
 using System;
 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;
@@ -15,13 +13,10 @@ namespace Jint.Runtime.Interop
     {
         private readonly Engine _engine;
 
-#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 readonly record struct TypeConversionKey(Type Source, Type Target);
+
+        private static readonly ConcurrentDictionary<TypeConversionKey, bool> _knownConversions = new();
+        private static readonly ConcurrentDictionary<TypeConversionKey, MethodInfo> _knownCastOperators = new();
 
         private static readonly Type intType = typeof(int);
         private static readonly Type iCallableType = typeof(Func<JsValue, JsValue[], JsValue>);
@@ -150,7 +145,7 @@ namespace Jint.Runtime.Interop
 
                 var targetElementType = type.GetElementType();
                 var itemsConverted = new object[source.Length];
-                for (int i = 0; i < source.Length; i++)
+                for (var i = 0; i < source.Length; i++)
                 {
                     itemsConverted[i] = Convert(source[i], targetElementType, formatProvider);
                 }
@@ -159,7 +154,8 @@ namespace Jint.Runtime.Interop
                 return result;
             }
 
-            if (value is ExpandoObject eObj)
+            var typeDescriptor = TypeDescriptor.Get(valueType);
+            if (typeDescriptor.IsStringKeyedGenericDictionary)
             {
                 // public empty constructor required
                 var constructors = type.GetConstructors();
@@ -188,7 +184,6 @@ namespace Jint.Runtime.Interop
                     }
                 }
 
-                var dict = (IDictionary<string, object>) eObj;
                 var obj = Activator.CreateInstance(type, System.Array.Empty<object>());
 
                 var members = type.GetMembers();
@@ -202,7 +197,7 @@ namespace Jint.Runtime.Interop
                     }
 
                     var name = member.Name.UpperToLowerCamelCase();
-                    if (dict.TryGetValue(name, out var val))
+                    if (typeDescriptor.TryGetValue(value, name, out var val))
                     {
                         var output = Convert(val, member.GetDefinedType(), formatProvider);
                         member.SetValue(obj, output);
@@ -214,17 +209,12 @@ namespace Jint.Runtime.Interop
 
             if (_engine.Options.Interop.AllowOperatorOverloading)
             {
-#if NETSTANDARD
-                var key = (valueType, type);
-#else
-                var key = $"{valueType}->{type}";
-#endif
+                var key = new TypeConversionKey(valueType, type);
 
                 var castOperator = _knownCastOperators.GetOrAdd(key, _ =>
                     valueType.GetOperatorOverloadMethods()
                     .Concat(type.GetOperatorOverloadMethods())
-                    .FirstOrDefault(m => type.IsAssignableFrom(m.ReturnType)
-                        && (m.Name == "op_Implicit" || m.Name == "op_Explicit")));
+                    .FirstOrDefault(m => type.IsAssignableFrom(m.ReturnType) && m.Name is "op_Implicit" or "op_Explicit"));
 
                 if (castOperator != null)
                 {
@@ -250,11 +240,7 @@ namespace Jint.Runtime.Interop
 
         public virtual bool TryConvert(object value, Type type, IFormatProvider formatProvider, out object converted)
         {
-#if NETSTANDARD
-            var key = value == null ? (null, type) : (value.GetType(), type);
-#else
-            var key = value == null ? $"Null->{type}" : $"{value.GetType()}->{type}";
-#endif
+            var key = new TypeConversionKey(value?.GetType(), type);
 
             // string conversion is not stable, "filter" -> int is invalid, "0" -> int is valid
             var canConvert = value is string || _knownConversions.GetOrAdd(key, _ =>

+ 1 - 3
Jint/Runtime/Interop/MethodInfoFunctionInstance.cs

@@ -48,10 +48,8 @@ namespace Jint.Runtime.Interop
             var converter = Engine.ClrTypeConverter;
 
             object[] parameters = null;
-            foreach (var tuple in TypeConverter.FindBestMatch(_methods, ArgumentProvider))
+            foreach (var (method, arguments, _) in TypeConverter.FindBestMatch(_engine, _methods, ArgumentProvider))
             {
-                var method = tuple.Item1;
-                var arguments = tuple.Item2;
                 var methodParameters = method.Parameters;
 
                 if (parameters == null || parameters.Length != methodParameters.Length)

+ 5 - 0
Jint/Runtime/Interop/ObjectWrapper.cs

@@ -84,6 +84,11 @@ namespace Jint.Runtime.Interop
             return true;
         }
 
+        public override object ToObject()
+        {
+            return Target;
+        }
+
         public override JsValue Get(JsValue property, JsValue receiver)
         {
             if (property.IsInteger() && Target is IList list)

+ 6 - 2
Jint/Runtime/Interop/TypeReference.cs

@@ -30,6 +30,11 @@ namespace Jint.Runtime.Interop
 
         public Type ReferenceType { get; }
 
+        public static TypeReference CreateTypeReference<T>(Engine engine)
+        {
+            return CreateTypeReference(engine, typeof(T));
+        }
+
         public static TypeReference CreateTypeReference(Engine engine, Type type)
         {
             return new TypeReference(engine, type);
@@ -55,9 +60,8 @@ namespace Jint.Runtime.Interop
                 ReferenceType,
                 t => MethodDescriptor.Build(t.GetConstructors(BindingFlags.Public | BindingFlags.Instance)));
 
-            foreach (var tuple in TypeConverter.FindBestMatch(constructors, _ => arguments))
+            foreach (var (method, _, _) in TypeConverter.FindBestMatch(_engine, constructors, _ => arguments))
             {
-                var method = tuple.Item1;
                 var retVal = method.Call(Engine, null, arguments);
                 var result = TypeConverter.ToObject(_realm, retVal);
 

+ 1 - 0
Jint/Runtime/Interop/TypeResolver.cs

@@ -14,6 +14,7 @@ namespace Jint.Runtime.Interop
     {
         public static readonly TypeResolver Default = new();
 
+        private readonly record struct ClrPropertyDescriptorFactoriesKey(Type Type, Key PropertyName);
         private Dictionary<ClrPropertyDescriptorFactoriesKey, ReflectionAccessor> _reflectionAccessors = new();
 
         /// <summary>

+ 20 - 15
Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs

@@ -11,12 +11,8 @@ 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 record struct OperatorKey(string OperatorName, Type Left, Type Right);
+        private static readonly ConcurrentDictionary<OperatorKey, MethodDescriptor> _knownOperators = new();
 
         private readonly JintExpression _left;
         private readonly JintExpression _right;
@@ -27,7 +23,12 @@ namespace Jint.Runtime.Interpreter.Expressions
             _right = Build(engine, expression.Right);
         }
 
-        internal static bool TryOperatorOverloading(EvaluationContext context, JsValue leftValue, JsValue rightValue, string clrName, out object result)
+        internal static bool TryOperatorOverloading(
+            EvaluationContext context,
+            JsValue leftValue,
+            JsValue rightValue,
+            string clrName,
+            out object result)
         {
             var left = leftValue.ToObject();
             var right = rightValue.ToObject();
@@ -38,11 +39,7 @@ namespace Jint.Runtime.Interpreter.Expressions
                 var rightType = right.GetType();
                 var arguments = new[] { leftValue, rightValue };
 
-#if NETSTANDARD
-                var key = (clrName, leftType, rightType);
-#else
-                var key = $"{clrName}->{leftType}->{rightType}";
-#endif
+                var key = new OperatorKey(clrName, leftType, rightType);
                 var method = _knownOperators.GetOrAdd(key, _ =>
                 {
                     var leftMethods = leftType.GetOperatorOverloadMethods();
@@ -51,13 +48,21 @@ namespace Jint.Runtime.Interpreter.Expressions
                     var methods = leftMethods.Concat(rightMethods).Where(x => x.Name == clrName && x.GetParameters().Length == 2);
                     var _methods = MethodDescriptor.Build(methods.ToArray());
 
-                    return TypeConverter.FindBestMatch(_methods, _ => arguments).FirstOrDefault()?.Item1;
+                    return TypeConverter.FindBestMatch(context.Engine, _methods, _ => arguments).FirstOrDefault().Method;
                 });
 
                 if (method != null)
                 {
-                    result = method.Call(context.Engine, null, arguments);
-                    return true;
+                    try
+                    {
+                        result = method.Call(context.Engine, null, arguments);
+                        return true;
+                    }
+                    catch
+                    {
+                        result = null;
+                        return false;
+                    }
                 }
             }
 

+ 15 - 15
Jint/Runtime/Interpreter/Expressions/JintUnaryExpression.cs

@@ -1,3 +1,4 @@
+using System;
 using Esprima.Ast;
 using Jint.Extensions;
 using Jint.Native;
@@ -5,18 +6,14 @@ 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 record struct OperatorKey(string OperatorName, Type Operand);
+        private static readonly ConcurrentDictionary<OperatorKey, MethodDescriptor> _knownOperators = new();
 
         private readonly JintExpression _argument;
         private readonly UnaryOperator _operator;
@@ -175,7 +172,7 @@ namespace Jint.Runtime.Interpreter.Expressions
                         v = engine.GetValue(rf, true);
                     }
                     else
-                    { 
+                    {
                         v = (JsValue) result.Value;
                     }
 
@@ -235,15 +232,18 @@ namespace Jint.Runtime.Interpreter.Expressions
                 var operandType = operand.GetType();
                 var arguments = new[] { value };
 
-#if NETSTANDARD
-                var key = (clrName, operandType);
-#else
-                var key = $"{clrName}->{operandType}";
-#endif
+                var key = new OperatorKey(clrName, operandType);
                 var method = _knownOperators.GetOrAdd(key, _ =>
                 {
-                    var foundMethod = operandType.GetOperatorOverloadMethods()
-                        .FirstOrDefault(x => x.Name == clrName && x.GetParameters().Length == 1);
+                    MethodInfo foundMethod = null;
+                    foreach (var x in operandType.GetOperatorOverloadMethods())
+                    {
+                        if (x.Name == clrName && x.GetParameters().Length == 1)
+                        {
+                            foundMethod = x;
+                            break;
+                        }
+                    }
 
                     if (foundMethod != null)
                     {

+ 161 - 76
Jint/Runtime/TypeConverter.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Generic;
 using System.Globalization;
-using System.Linq;
 using System.Runtime.CompilerServices;
 using Esprima.Ast;
 using Jint.Extensions;
@@ -817,26 +816,40 @@ namespace Jint.Runtime
             }
         }
 
-        internal static IEnumerable<Tuple<MethodDescriptor, JsValue[]>> FindBestMatch(
+        internal readonly record struct MethodMatch(MethodDescriptor Method, JsValue[] Arguments, int Score = 0) : IComparable<MethodMatch>
+        {
+            public int CompareTo(MethodMatch other) => Score.CompareTo(other.Score);
+        }
+
+        internal static IEnumerable<MethodMatch> FindBestMatch(
+            Engine engine,
             MethodDescriptor[] methods,
             Func<MethodDescriptor, JsValue[]> argumentProvider)
         {
-            List<Tuple<MethodDescriptor, JsValue[]>> matchingByParameterCount = null;
-            foreach (var m in methods)
+            List<MethodMatch> matchingByParameterCount = null;
+            foreach (var method in methods)
             {
-                var parameterInfos = m.Parameters;
-                var arguments = argumentProvider(m);
+                var parameterInfos = method.Parameters;
+                var arguments = argumentProvider(method);
                 if (arguments.Length <= parameterInfos.Length
-                    && arguments.Length >= parameterInfos.Length - m.ParameterDefaultValuesCount)
+                    && arguments.Length >= parameterInfos.Length - method.ParameterDefaultValuesCount)
                 {
-                    if (methods.Length == 0 && arguments.Length == 0)
+                    var score = CalculateMethodScore(engine, method, arguments);
+                    if (score == 0)
                     {
-                        yield return new Tuple<MethodDescriptor, JsValue[]>(m, arguments);
+                        // perfect match
+                        yield return new MethodMatch(method, arguments);
                         yield break;
                     }
 
-                    matchingByParameterCount ??= new List<Tuple<MethodDescriptor, JsValue[]>>();
-                    matchingByParameterCount.Add(new Tuple<MethodDescriptor, JsValue[]>(m, arguments));
+                    if (score < 0)
+                    {
+                        // discard
+                        continue;
+                    }
+
+                    matchingByParameterCount ??= new List<MethodMatch>();
+                    matchingByParameterCount.Add(new MethodMatch(method, arguments, score));
                 }
             }
 
@@ -845,84 +858,156 @@ namespace Jint.Runtime
                 yield break;
             }
 
-            List<Tuple<int, Tuple<MethodDescriptor, JsValue[]>>> scoredList = null;
+            if (matchingByParameterCount.Count > 1)
+            {
+                matchingByParameterCount.Sort();
+            }
 
-            foreach (var tuple in matchingByParameterCount)
+            foreach (var match in matchingByParameterCount)
             {
-                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))
-                        {
-                            score -= 10000;
-                        }
-                    }
-                    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 &&
-                            jsValue is JsNumber jsNumber
-                            && jsNumber.IsInteger()
-                            && Enum.IsDefined(paramType, jsNumber.AsInteger()))
-                        {
-                            // OK
-                        }
-                        else
-                        {
-                            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;
-                                }
-                            }
-                        }
-                    }
-                }
+                yield return match;
+            }
+        }
+
+        /// <summary>
+        /// Method's match score tells how far away it's from ideal candidate. 0 = ideal, bigger the the number,
+        /// the farther away the candidate is from ideal match. Negative signals impossible match.
+        /// </summary>
+        private static int CalculateMethodScore(Engine engine, MethodDescriptor method, JsValue[] arguments)
+        {
+            if (method.Parameters.Length == 0 && arguments.Length == 0)
+            {
+                // perfect
+                return 0;
+            }
 
-                if (score == 0)
+            var score = 0;
+            for (var i = 0; i < arguments.Length; i++)
+            {
+                var jsValue = arguments[i];
+                var paramType = method.Parameters[i].ParameterType;
+
+                var parameterScore = CalculateMethodParameterScore(engine, jsValue, paramType);
+                if (parameterScore < 0)
                 {
-                    yield return Tuple.Create(tuple.Item1, arguments);
-                    yield break;
+                    return parameterScore;
                 }
-                else
+                score += parameterScore;
+            }
+
+            return score;
+        }
+
+        /// <summary>
+        /// Determines how well parameter type matches target method's type.
+        /// </summary>
+        private static int CalculateMethodParameterScore(
+            Engine engine,
+            JsValue jsValue,
+            Type paramType)
+        {
+            var objectValue = jsValue.ToObject();
+            var objectValueType = objectValue?.GetType();
+
+            if (objectValueType == paramType)
+            {
+                return 0;
+            }
+
+            if (objectValue == null)
+            {
+                if (!TypeIsNullable(paramType))
                 {
-                    scoredList ??= new List<Tuple<int, Tuple<MethodDescriptor, JsValue[]>>>();
-                    scoredList.Add(Tuple.Create(score, tuple));
+                    // this is bad
+                    return -1;
                 }
+
+                return 0;
+            }
+
+            if (paramType == typeof(JsValue))
+            {
+                // JsValue is convertible to. But it is still not a perfect match
+                return 1;
+            }
+
+            if (paramType == typeof(object))
+            {
+                // a catch-all, prefer others over it
+                return 5;
+            }
+
+            if (paramType.IsEnum &&
+                jsValue is JsNumber jsNumber
+                && jsNumber.IsInteger()
+                && Enum.IsDefined(paramType, jsNumber.AsInteger()))
+            {
+                // we can do conversion from int value to enum
+                return 0;
+            }
+
+            if (paramType.IsAssignableFrom(objectValueType))
+            {
+                // is-a-relation
+                return 1;
+            }
+
+            if (jsValue.IsArray() && objectValueType.IsArray)
+            {
+                // we have potential, TODO if we'd know JS array's internal type we could have exact match
+                return 2;
+            }
+
+            if (CanChangeType(objectValue, paramType))
+            {
+                // forcing conversion isn't ideal not ideal, but works, especially for int -> double for example
+                return 1;
             }
 
-            if (scoredList != null)
+            if (engine.Options.Interop.AllowOperatorOverloading)
             {
-                foreach (var item in scoredList.OrderByDescending(x => x.Item1))
+                foreach (var m in objectValueType.GetOperatorOverloadMethods())
                 {
-                    yield return item.Item2;
+                    if (paramType.IsAssignableFrom(m.ReturnType) && m.Name is "op_Implicit" or "op_Explicit")
+                    {
+                        // implicit/explicit operator conversion is OK, but not ideal
+                        return 1;
+                    }
                 }
             }
+
+            if (ReflectionExtensions.TryConvertViaTypeCoercion(paramType, engine.Options.Interop.ValueCoercion, jsValue, out _))
+            {
+                // gray JS zone where we start to do odd things
+                return 10;
+            }
+
+            // will rarely succeed
+            return 100;
+        }
+
+        private static bool CanChangeType(object value, Type targetType)
+        {
+            if (value is null && !targetType.IsValueType)
+            {
+                return true;
+            }
+
+            if (value is not IConvertible)
+            {
+                return false;
+            }
+
+            try
+            {
+                Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
+                return true;
+            }
+            catch
+            {
+                // nope
+                return false;
+            }
         }
 
         internal static bool TypeIsNullable(Type type)