Browse Source

Generic Methods: better generic method support. (#1106)

* Generic Methods: better generic method support.  covariance + contravariance - where generic arguments are derived from base generic argument type

* generic extension methods can't be bound to an array of Object - otherwise generic extension methods that aren't covariant/contravariant won't work with alternate types

Co-authored-by: Tim Cassidy <[email protected]>
source-transformer 3 years ago
parent
commit
0e315f41df

+ 113 - 21
Jint.Tests/Runtime/ExtensionMethods/ExtensionMethodsTest.cs

@@ -186,35 +186,127 @@ namespace Jint.Tests.Runtime.ExtensionMethods
             Assert.Equal("foobar", observable.Last);
         }
 
+        [Fact]
+        public void GenericExtensionMethodOnClosedGenericType()
+        {
+            var options = new Options();
+            options.AddExtensionMethods(typeof(ObservableExtensions));
+
+            var engine = new Engine(options);
+
+            engine.SetValue("log", new System.Action<object>(System.Console.WriteLine));
 
-        private class Converter : TextWriter
+            NameObservable observable = new NameObservable();
+            engine.SetValue("observable", observable);
+            var result = engine.Evaluate(@"
+                log('before calling Select');
+                var result = observable.Select('some text');
+                log('result: ' + result);
+                return result;
+            ");
+
+            //System.Console.WriteLine("GenericExtensionMethodOnGenericType: result: " + result + " result.ToString(): " + result.ToString());
+
+            Assert.Equal("some text", result);
+        }
+
+        [Fact]
+        public void GenericExtensionMethodOnClosedGenericType2()
         {
-            ITestOutputHelper _output;
+            var options = new Options();
+            options.AddExtensionMethods(typeof(ObservableExtensions));
 
-            public Converter(ITestOutputHelper output)
-            {
-                _output = output;
-            }
+            var engine = new Engine(options);
 
-            public override Encoding Encoding
+            NameObservable observable = new NameObservable();
+            observable.Where((text) =>
             {
-                get { return Encoding.ASCII; }
-            }
+                System.Console.WriteLine("GenericExtensionMethodOnClosedGenericType2: NameObservable: Where: text: " + text);
+                return true;
+            });
+            engine.SetValue("observable", observable);
+            var result = engine.Evaluate(@"
+                var result = observable.Where(function(text){
+                    return true;
+                });
 
-            public override void WriteLine(string message)
-            {
-                _output.WriteLine(message);
-            }
+                observable.UpdateName('testing yo');
+                observable.CommitName();
+                return result;
+            ");
 
-            public override void WriteLine(string format, params object[] args)
-            {
-                _output.WriteLine(format, args);
-            }
+            var nameObservableResult = result.ToObject() as NameObservable;
+            Assert.NotNull(nameObservableResult);
+            Assert.Equal("testing yo", nameObservableResult.Last);
+        }
 
-            public override void Write(char value)
+        [Fact]
+        public void GenericExtensionMethodOnOpenGenericType()
+        {
+            var options = new Options();
+            options.AddExtensionMethods(typeof(ObservableExtensions));
+
+            var engine = new Engine(options);
+
+            BaseObservable<string> observable = new BaseObservable<string>();
+            observable.Where((text) =>
             {
-                throw new System.Exception("This text writer only supports WriteLine(string) and WriteLine(string, params object[]).");
-            }
+                System.Console.WriteLine("GenericExtensionMethodOnOpenGenericType: BaseObservable: Where: text: " + text);
+                return true;
+            });
+            engine.SetValue("observable", observable);
+            var result = engine.Evaluate(@"
+                var result = observable.Where(function(text){
+                    return true;
+                });
+
+                observable.Update('testing yo');
+                observable.BroadcastCompleted();
+
+                return result;
+            ");
+
+            System.Console.WriteLine("GenericExtensionMethodOnOpenGenericType: result: " + result + " result.ToString(): " + result.ToString());
+            var baseObservableResult = result.ToObject() as BaseObservable<string>;
+
+            System.Console.WriteLine("GenericExtensionMethodOnOpenGenericType: baseObservableResult: " + baseObservableResult);
+            Assert.NotNull(baseObservableResult);
+            Assert.Equal("testing yo", baseObservableResult.Last);
+        }
+
+        [Fact]
+        public void GenericExtensionMethodOnGenericTypeInstantiatedInJs()
+        {
+            var options = new Options();
+            options.AddExtensionMethods(typeof(ObservableExtensions));
+
+            var engine = new Engine(options);
+
+            engine.SetValue("BaseObservable", typeof(BaseObservable<>));
+            engine.SetValue("ObservableFactory", typeof(ObservableFactory));
+
+            var result = engine.Evaluate(@"
+
+                // you can't instantiate generic types in JS (without providing the types as arguments to the constructor) - i.e. not compatible with transpiled typescript
+                //const observable = new BaseObservable();
+                //const observable = BaseObservable.GetBoolBaseObservable();
+                const observable = ObservableFactory.GetBoolBaseObservable();
+
+                var result = observable.Where(function(someBool){
+                    return true;
+                });
+                observable.Update(false);
+                observable.BroadcastCompleted();
+
+                return result;
+            ");
+
+            var baseObservableResult = result.ToObject() as BaseObservable<bool>;
+
+            System.Console.WriteLine("GenericExtensionMethodOnOpenGenericType: baseObservableResult: " + baseObservableResult);
+            Assert.NotNull(baseObservableResult);
+            Assert.Equal(false, baseObservableResult.Last);
         }
+
     }
-}
+}

+ 57 - 13
Jint.Tests/Runtime/ExtensionMethods/ObservableExtensions.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Collections.Generic;
 
 namespace Jint.Tests.Runtime.ExtensionMethods
@@ -45,15 +45,34 @@ namespace Jint.Tests.Runtime.ExtensionMethods
             var subs = new Subscribe<T>(onNext, null, null);
             source.Subscribe(subs);
         }
+
+        public static TResult Select<T, TResult>(this IObservable<T> source, TResult result)
+        {
+            return result;
+        }
+
+        public static IObservable<T> Where<T>(this IObservable<T> source, Func<T, bool> predicate)
+        {
+            T t = default;
+            predicate(t);
+            return source;
+        }
+
+        public static IObservable<T> Where<T>(this IObservable<T> source, Func<T, int, bool> predicate)
+        {
+            T t = default;
+            bool result = predicate(t, 42);
+            return source;
+        }
     }
 
-    public class NameObservable : IObservable<string>
+    public class BaseObservable<T> : IObservable<T>
     {
-        private List<IObserver<string>> observers = new List<IObserver<string>>();
+        private List<IObserver<T>> observers = new List<IObserver<T>>();
 
-        public string Last { get; private set; }
+        public T Last { get; private set; }
 
-        public IDisposable Subscribe(IObserver<string> observer)
+        public IDisposable Subscribe(IObserver<T> observer)
         {
             if (!observers.Contains(observer))
                 observers.Add(observer);
@@ -62,10 +81,10 @@ namespace Jint.Tests.Runtime.ExtensionMethods
 
         private class Unsubscriber : IDisposable
         {
-            private List<IObserver<string>> _observers;
-            private IObserver<string> _observer;
+            private List<IObserver<T>> _observers;
+            private IObserver<T> _observer;
 
-            public Unsubscriber(List<IObserver<string>> observers, IObserver<string> observer)
+            public Unsubscriber(List<IObserver<T>> observers, IObserver<T> observer)
             {
                 this._observers = observers;
                 this._observer = observer;
@@ -78,23 +97,48 @@ namespace Jint.Tests.Runtime.ExtensionMethods
             }
         }
 
-        public void UpdateName(string name)
+        protected void BroadcastUpdate(T t)
         {
-            Last = name;
             foreach (var observer in observers)
             {
-                observer.OnNext(name);
+                observer.OnNext(t);
             }
         }
 
-        public void CommitName()
+        public void Update(T t)
+        {
+            Last = t;
+            BroadcastUpdate(t);
+        }
+
+        public void BroadcastCompleted()
         {
             foreach (var observer in observers.ToArray())
             {
                 observer.OnCompleted();
             }
-
             observers.Clear();
         }
     }
+
+    public class ObservableFactory
+    {
+        public static BaseObservable<bool> GetBoolBaseObservable()
+        {
+            return new BaseObservable<bool>();
+        }
+    }
+
+    public class NameObservable : BaseObservable<string>
+    {
+        public void UpdateName(string name)
+        {
+            Update(name);
+        }
+
+        public void CommitName()
+        {
+            BroadcastCompleted();
+        }
+    }
 }

+ 135 - 7
Jint.Tests/Runtime/GenericMethodTests.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using Xunit;
 
 namespace Jint.Tests.Runtime;
@@ -8,7 +8,7 @@ public class GenericMethodTests
     [Fact]
     public void TestGeneric()
     {
-        var engine = new Engine(cfg => cfg.AllowOperatorOverloading());
+        var engine = new Engine();
         engine.SetValue("TestGenericBaseClass", typeof(TestGenericBaseClass<>));
         engine.SetValue("TestGenericClass", typeof(TestGenericClass));
 
@@ -26,7 +26,7 @@ public class GenericMethodTests
     [Fact]
     public void TestGeneric2()
     {
-        var engine = new Engine(cfg => cfg.AllowOperatorOverloading());
+        var engine = new Engine();
         var testGenericObj = new TestGenericClass();
         engine.SetValue("testGenericObj", testGenericObj);
 
@@ -42,7 +42,7 @@ public class GenericMethodTests
     [Fact]
     public void TestFancyGenericPass()
     {
-        var engine = new Engine(cfg => cfg.AllowOperatorOverloading());
+        var engine = new Engine();
         var testGenericObj = new TestGenericClass();
         engine.SetValue("testGenericObj", testGenericObj);
 
@@ -56,18 +56,88 @@ public class GenericMethodTests
     [Fact]
     public void TestFancyGenericFail()
     {
-        var engine = new Engine(cfg => cfg.AllowOperatorOverloading());
+        var engine = new Engine();
         var testGenericObj = new TestGenericClass();
         engine.SetValue("testGenericObj", testGenericObj);
 
-        var argException = Assert.Throws<ArgumentException>(() =>
+        var argException = Assert.Throws<Jint.Runtime.JavaScriptException>(() =>
         {
             engine.Execute(@"
                     testGenericObj.Fancy('test', 'foo', 42);
                 ");
         });
 
-        Assert.Equal("Object of type 'System.String' cannot be converted to type 'System.Double'.", argException.Message);
+        Assert.Equal("No public methods with the specified arguments were found.", argException.Message);
+    }
+
+    // TPC: TODO: tldr; typescript transpiled to javascript does not include the types in the constructors - JINT should allow you to use generics without specifying type
+    // The following doesn't work because JINT currently requires generic classes to be instantiated in a way that doesn't comply with typescript transpile of javascript
+    // i.e. we shouldn't have to specify the type of the generic class when we instantiate it.  Since typescript takes the following:
+    // const someGeneric = new Foo.Bar.MeGeneric<string>()
+    // and it becomes the following javascript (thru transpile):
+    // const someGeneric = new Foo.Bar.MeGeneric();
+    // we _may_ be able to address this by simply instantiating generic types using System.Object for the generic arguments
+    // This test currently generates the following error:
+    // No public methods with the specified arguments were found.
+    public void TestGenericClassDeriveFromGenericInterface()
+    {
+        var engine = new Engine(cfg => cfg.AllowClr(typeof(OpenGenericTest<>).Assembly));
+
+        engine.SetValue("ClosedGenericTest", typeof(ClosedGenericTest));
+        engine.SetValue("OpenGenericTest", typeof(OpenGenericTest<>));
+        engine.SetValue("log", new System.Action<object>(System.Console.WriteLine));
+        engine.Execute(@"
+            const closedGenericTest = new ClosedGenericTest();
+            closedGenericTest.Foo(42);
+            const temp = new OpenGenericTest(System.String);
+        ");
+    }
+
+    [Fact]
+    public void TestGenericMethodUsingCovarianceOrContraviance()
+    {
+        var engine = new Engine(cfg => cfg.AllowClr(typeof(PlayerChoiceManager).Assembly));
+
+        engine.SetValue("PlayerChoiceManager", typeof(PlayerChoiceManager));
+        engine.SetValue("TestSelectorWithoutProps", typeof(TestSelectorWithoutProps));
+
+        engine.SetValue("TestGenericClass", typeof(TestGenericClass));
+
+        // TPC: the following is the C# equivalent
+        /*
+        PlayerChoiceManager playerChoiceManager = new PlayerChoiceManager();
+        var testSelectorWithoutProps = new TestSelectorWithoutProps();
+        var result = playerChoiceManager.Store.Select(testSelectorWithoutProps);
+        */
+        engine.Execute(@"
+            const playerChoiceManager = new PlayerChoiceManager();
+            const testSelectorWithoutProps = new TestSelectorWithoutProps();
+            const result = playerChoiceManager.Store.Select(testSelectorWithoutProps);
+        ");
+
+        Assert.Equal(true, ReduxStore<PlayerChoiceState>.SelectInvoked);
+    }
+
+
+    public interface IGenericTest<T>
+    {
+        void Foo<U>(U u);
+    }
+
+    public class OpenGenericTest<T> : IGenericTest<T>
+    {
+        public void Foo<U>(U u)
+        {
+            Console.WriteLine("OpenGenericTest: u: " + u);
+        }
+    }
+
+    public class ClosedGenericTest : IGenericTest<string>
+    {
+        public void Foo<U>(U u)
+        {
+            Console.WriteLine("ClosedGenericTest: u: " + u);
+        }
     }
 
     public class TestGenericBaseClass<T>
@@ -118,4 +188,62 @@ public class GenericMethodTests
             FancyInvoked = true;
         }
     }
+
+    public interface ISelector<in TInput, out TOutput>
+    {
+    }
+
+    public interface ISelectorWithoutProps<in TInput, out TOutput> : ISelector<TInput, TOutput>
+    {
+        IObservable<TOutput> Apply(TInput input);
+    }
+
+    public sealed partial class ReduxStore<TState> where TState : class, new()
+    {
+        public static bool SelectInvoked
+        {
+            get;
+            private set;
+        }
+
+        TState _stateSubject;
+
+        public ReduxStore()
+        {
+            SelectInvoked = false;
+        }
+
+        public IObservable<TResult> Select<TResult>(ISelectorWithoutProps<TState, TResult> selector, string? optionsStr = null)
+        {
+            SelectInvoked = true;
+            return selector.Apply(_stateSubject);
+        }
+    }
+
+    public class ManagerWithStore<Klass, TState> where Klass : ManagerWithStore<Klass, TState>, new() where TState : class, new()
+    {
+        public ReduxStore<TState> Store { get; private set; } = null;
+
+        public ManagerWithStore()
+        {
+            Store = new ReduxStore<TState>();
+        }
+    }
+
+    public class PlayerChoiceState
+    {
+    }
+
+    public class TestSelectorWithoutProps : ISelectorWithoutProps<PlayerChoiceState, string>
+    {
+        public IObservable<string> Apply(PlayerChoiceState input)
+        {
+            return null;
+        }
+    }
+
+    public class PlayerChoiceManager : ManagerWithStore<PlayerChoiceManager, PlayerChoiceState>
+    {
+    }
 }
+

+ 10 - 1
Jint/Runtime/Interop/DefaultTypeConverter.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Collections.Concurrent;
 using System.Collections.ObjectModel;
 using System.Linq;
@@ -53,6 +53,15 @@ namespace Jint.Runtime.Interop
                 return value;
             }
 
+            if (type.IsGenericType)
+            {
+                var result = TypeConverter.IsAssignableToGenericType(value.GetType(), type);
+                if (result.IsAssignable)
+                {
+                    return value;
+                }
+            }
+
             if (type.IsNullable())
             {
                 type = Nullable.GetUnderlyingType(type);

+ 115 - 27
Jint/Runtime/Interop/MethodInfoFunctionInstance.cs

@@ -27,10 +27,111 @@ namespace Jint.Runtime.Interop
             _fallbackClrFunctionInstance = fallbackClrFunctionInstance;
         }
 
-        private static bool IsAssignableToGenericType(Type givenType, Type genericType)
+        private bool IsGenericParameter(object argObj, Type parameterType, int parameterIndex)
         {
-            var result = TypeConverter.IsAssignableToGenericType(givenType, genericType);
-            return (result >= 0);
+            var result = TypeConverter.IsAssignableToGenericType(argObj?.GetType(), parameterType);
+            if (result.Score < 0)
+            {
+                return false;
+            }
+
+            if ((parameterType.IsGenericParameter) || (parameterType.IsGenericType))
+            {
+                return true;
+            }
+            return false;
+        }
+
+        private bool HandleGenericParameter(object argObj, Type parameterType, int parameterIndex, Type[] genericArgTypes)
+        {
+            var result = TypeConverter.IsAssignableToGenericType(argObj?.GetType(), parameterType);
+            if (result.Score < 0)
+            {
+                return false;
+            }
+
+            if (parameterType.IsGenericParameter)
+            {
+                var genericParamPosition = parameterType.GenericParameterPosition;
+                if (genericParamPosition >= 0)
+                {
+                    genericArgTypes[genericParamPosition] = argObj.GetType();
+                }
+            }
+            else if (parameterType.IsGenericType)
+            {
+                // TPC: maybe we can pull the generic parameters from the arguments?
+                var genericArgs = parameterType.GetGenericArguments();
+                for (int j = 0; j < genericArgs.Length; ++j)
+                {
+                    var genericArg = genericArgs[j];
+                    if (genericArg.IsGenericParameter)
+                    {
+                        var genericParamPosition = genericArg.GenericParameterPosition;
+                        if (genericParamPosition >= 0)
+                        {
+                            var givenTypeGenericArgs = result.MatchingGivenType.GetGenericArguments();
+                            genericArgTypes[genericParamPosition] = givenTypeGenericArgs[j];
+                        }
+                    }
+                }
+            }
+            else
+            {
+                return false;
+            }
+            return true;
+        }
+
+        private MethodBase ResolveMethod(MethodBase method, ParameterInfo[] methodParameters, object thisObj, JsValue[] arguments)
+        {
+            if (!method.IsGenericMethod)
+            {
+                return method;
+            }
+            if (!method.IsGenericMethodDefinition)
+            {
+                return method;
+            }
+            MethodInfo methodInfo = method as MethodInfo;
+            if (methodInfo == null)
+            {
+                // probably should issue at least a warning here
+                return method;
+            }
+
+            // TPC: we could also && "(method.Method.IsGenericMethodDefinition)" because we won't create a generic method if that isn't the case
+            var methodGenericArgs = method.GetGenericArguments();
+            var originalGenericArgTypes = methodGenericArgs;
+            var genericArgTypes = new Type[methodGenericArgs.Length];
+
+            for (int i = 0; i < methodParameters.Length; ++i)
+            {
+                var methodParameter = methodParameters[i];
+                var parameterType = methodParameter.ParameterType;
+                object argObj;
+                if (i < arguments.Length)
+                {
+                    argObj = arguments[i].ToObject();
+                }
+                else
+                {
+                    argObj = typeof(object);
+                }
+                var handled = HandleGenericParameter(argObj, parameterType, i, genericArgTypes);
+            }
+
+            for (int i = 0; i < genericArgTypes.Length; ++i)
+            {
+                if (genericArgTypes[i] == null)
+                {
+                    // this is how we're dealing with things like "void" return types - you can't use "void" as a type:
+                    genericArgTypes[i] = typeof(object);
+                }
+            }
+
+            var genericMethodInfo = methodInfo.MakeGenericMethod(genericArgTypes);
+            return genericMethodInfo;
         }
 
         public override JsValue Call(JsValue thisObject, JsValue[] jsArguments)
@@ -53,25 +154,21 @@ namespace Jint.Runtime.Interop
             }
 
             var converter = Engine.ClrTypeConverter;
-
+            var thisObj = thisObject.ToObject();
+            var thisObjType = thisObj?.GetType();
             object[] parameters = null;
             foreach (var (method, arguments, _) in TypeConverter.FindBestMatch(_engine, _methods, ArgumentProvider))
             {
                 var methodParameters = method.Parameters;
-
                 if (parameters == null || parameters.Length != methodParameters.Length)
                 {
                     parameters = new object[methodParameters.Length];
                 }
 
                 var argumentsMatch = true;
-                Type[] genericArgTypes = null;
-                if (method.Method.IsGenericMethod)
-                {
-                    var methodGenericArgs = method.Method.GetGenericArguments();
-                    genericArgTypes = new Type[methodGenericArgs.Length];
-                }
-
+                var resolvedMethod = ResolveMethod(method.Method, methodParameters, thisObj, arguments);
+                // TPC: if we're concerned about cost of MethodInfo.GetParameters() - we could only invoke it if this ends up being a generic method (i.e. they will be different in that scenario)
+                methodParameters = resolvedMethod.GetParameters();
                 for (var i = 0; i < parameters.Length; i++)
                 {
                     var methodParameter = methodParameters[i];
@@ -82,25 +179,18 @@ namespace Jint.Runtime.Interop
                     {
                         parameters[i] = argument;
                     }
-                    else if ((parameterType.IsGenericParameter) && (IsAssignableToGenericType(argument.ToObject()?.GetType(), parameterType)))
-                    {
-                        var argObj = argument.ToObject();
-                        if (parameterType.GenericParameterPosition >= 0)
-                        {
-                            genericArgTypes[parameterType.GenericParameterPosition] = argObj.GetType();
-                        }
-
-                        parameters[i] = argObj;
-                    }
                     else if (argument is null)
                     {
                         // optional
                         parameters[i] = System.Type.Missing;
                     }
+                    else if (IsGenericParameter(argument.ToObject(), parameterType, i)) // don't think we need the condition preface of (argument == null) because of earlier condition
+                    {
+                        parameters[i] = argument.ToObject();
+                    }
                     else if (parameterType == typeof(JsValue[]) && argument.IsArray())
                     {
                         // Handle specific case of F(params JsValue[])
-
                         var arrayInstance = argument.AsArray();
                         var len = TypeConverter.ToInt32(arrayInstance.Get(CommonProperties.Length, this));
                         var result = new JsValue[len];
@@ -137,15 +227,13 @@ namespace Jint.Runtime.Interop
                 {
                     if ((method.Method.IsGenericMethodDefinition) && (method.Method is MethodInfo methodInfo))
                     {
-                        var declaringType = methodInfo.DeclaringType;
-                        var genericMethodInfo = methodInfo.MakeGenericMethod(genericArgTypes);
-                        var thisObj = thisObject.ToObject();
+                        var genericMethodInfo = resolvedMethod;
                         var result = genericMethodInfo.Invoke(thisObj, parameters);
                         return FromObject(Engine, result);
                     }
                     else
                     {
-                        return FromObject(Engine, method.Method.Invoke(thisObject.ToObject(), parameters));
+                        return FromObject(Engine, method.Method.Invoke(thisObj, parameters));
                     }
                 }
                 catch (TargetInvocationException exception)

+ 3 - 23
Jint/Runtime/Interop/Reflection/ExtensionMethodCache.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Reflection;
@@ -48,27 +48,6 @@ namespace Jint.Runtime.Interop.Reflection
 
         public bool HasMethods => _allExtensionMethods.Count > 0;
 
-        private MethodInfo BindMethodGenericParameters(MethodInfo method)
-        {
-            if (method.IsGenericMethodDefinition && method.ContainsGenericParameters)
-            {
-                var methodGenerics = method.GetGenericArguments();
-                var parameterList = Enumerable.Repeat(typeof(object), methodGenerics.Length).ToArray();
-
-                try
-                {
-                    return method.MakeGenericMethod(parameterList);
-                }
-                catch
-                {
-                    // Generic parameter constraints failed probably.
-                    // If it does not work, let it be. We don't need to do anything.
-                }
-            }
-            return method;
-        }
-
-
         public bool TryGetExtensionMethods(Type objectType, out MethodInfo[] methods)
         {
             var methodLookup = _extensionMethods;
@@ -92,7 +71,8 @@ namespace Jint.Runtime.Interop.Reflection
                 }
             }
 
-            methods = results.Select(BindMethodGenericParameters).ToArray();
+			// don't create generic methods bound to an array of object - as this will prevent value types and other generics that don't support covariants/contravariants
+            methods = results.ToArray();
 
             // racy, we don't care, worst case we'll catch up later
             Interlocked.CompareExchange(ref _extensionMethods, new Dictionary<Type, MethodInfo[]>(methodLookup)

+ 31 - 10
Jint/Runtime/TypeConverter.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Numerics;
@@ -1124,6 +1124,19 @@ namespace Jint.Runtime
             return score;
         }
 
+        internal class AssignableResult
+        {
+            public int Score = -1;
+            public bool IsAssignable { get { return Score >= 0; } }
+            public Type MatchingGivenType;
+
+            public AssignableResult(int score, Type matchingGivenType)
+            {
+                Score = score;
+                MatchingGivenType = matchingGivenType;
+            }
+        }
+
         /// <summary>
         /// resources:
         /// https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/how-to-examine-and-instantiate-generic-types-with-reflection
@@ -1137,7 +1150,7 @@ namespace Jint.Runtime
         /// <param name="givenType"></param>
         /// <param name="genericType"></param>
         /// <returns></returns>
-        internal static int IsAssignableToGenericType(Type givenType, Type genericType)
+        internal static AssignableResult IsAssignableToGenericType(Type givenType, Type genericType)
         {
             if (!genericType.IsConstructedGenericType)
             {
@@ -1145,28 +1158,36 @@ namespace Jint.Runtime
                 // https://docs.microsoft.com/en-us/dotnet/api/system.type.isconstructedgenerictype?view=net-6.0
                 // this effectively means this generic type is open (i.e. not closed) - so any type is "possible" - without looking at the code in the method we don't know
                 // whether any operations are being applied that "don't work"
-                return 2;
+                return new AssignableResult(2, givenType);
             }
 
             var interfaceTypes = givenType.GetInterfaces();
-
             foreach (var it in interfaceTypes)
             {
-                if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType)
+                if (it.IsGenericType)
                 {
-                    return 0;
+                    var givenTypeGenericDef = it.GetGenericTypeDefinition();
+                    if (givenTypeGenericDef == genericType)
+                    {
+                        return new AssignableResult(0, it);
+                    }
+                    else if (genericType.IsGenericType && (givenTypeGenericDef == genericType.GetGenericTypeDefinition()))
+                    {
+                        return new AssignableResult(0, it);
+                    }
+                    // TPC: we could also add a loop to recurse and iterate thru the iterfaces of generic type - because of covariance/contravariance
                 }
             }
 
             if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType)
             {
-                return 0;
+                return new AssignableResult(0, givenType);
             }
 
             Type baseType = givenType.BaseType;
             if (baseType == null)
             {
-                return -1;
+                return new AssignableResult(-1, givenType);
             }
 
             var result = IsAssignableToGenericType(baseType, genericType);
@@ -1248,9 +1269,9 @@ namespace Jint.Runtime
             if (paramType.IsGenericParameter)
             {
                 var genericTypeAssignmentScore = IsAssignableToGenericType(objectValueType, paramType);
-                if (genericTypeAssignmentScore != -1)
+                if (genericTypeAssignmentScore.Score != -1)
                 {
-                    return genericTypeAssignmentScore;
+                    return genericTypeAssignmentScore.Score;
                 }
             }