From 5130aaa1bbbfa111cf9d828060d11ee35dbc37d7 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Wed, 16 Dec 2020 23:03:51 +0000 Subject: [PATCH 01/24] Overload operators except for bit shifts and default args. --- src/embed_tests/TestPyMethod.cs | 185 +++++++++++++++++++++++++++++ src/runtime/classmanager.cs | 4 + src/runtime/methodbinder.cs | 54 ++++++++- src/runtime/native/ITypeOffsets.cs | 10 ++ src/runtime/native/TypeOffset.cs | 10 ++ src/runtime/operatormethod.cs | 127 ++++++++++++++++++++ src/runtime/runtime.cs | 2 + src/runtime/typemanager.cs | 1 + 8 files changed, 389 insertions(+), 4 deletions(-) create mode 100644 src/embed_tests/TestPyMethod.cs create mode 100644 src/runtime/operatormethod.cs diff --git a/src/embed_tests/TestPyMethod.cs b/src/embed_tests/TestPyMethod.cs new file mode 100644 index 000000000..0d06e9e1a --- /dev/null +++ b/src/embed_tests/TestPyMethod.cs @@ -0,0 +1,185 @@ +using NUnit.Framework; + +using Python.Runtime; + +using System.Linq; +using System.Reflection; + +namespace Python.EmbeddingTest +{ + public class TestPyMethod + { + [OneTimeSetUp] + public void SetUp() + { + PythonEngine.Initialize(); + } + + [OneTimeTearDown] + public void Dispose() + { + PythonEngine.Shutdown(); + } + + public class SampleClass + { + public int VoidCall() => 10; + + public int Foo(int a, int b = 10) => a + b; + + public int Foo2(int a = 10, params int[] args) + { + return a + args.Sum(); + } + } + + [Test] + public void TestVoidCall() + { + string name = string.Format("{0}.{1}", + typeof(SampleClass).DeclaringType.Name, + typeof(SampleClass).Name); + string module = MethodBase.GetCurrentMethod().DeclaringType.Namespace; + PythonEngine.Exec($@" +from {module} import * +SampleClass = {name} +obj = SampleClass() +assert obj.VoidCall() == 10 +"); + } + + [Test] + public void TestDefaultParameter() + { + string name = string.Format("{0}.{1}", + typeof(SampleClass).DeclaringType.Name, + typeof(SampleClass).Name); + string module = MethodBase.GetCurrentMethod().DeclaringType.Namespace; + + PythonEngine.Exec($@" +from {module} import * +SampleClass = {name} +obj = SampleClass() +assert obj.Foo(10) == 20 +assert obj.Foo(10, 1) == 11 + +assert obj.Foo2() == 10 +assert obj.Foo2(20) == 20 +assert obj.Foo2(20, 30) == 50 +assert obj.Foo2(20, 30, 50) == 100 +"); + } + + public class OperableObject + { + public int Num { get; set; } + + public OperableObject(int num) + { + Num = num; + } + + public static OperableObject operator +(OperableObject a, OperableObject b) + { + return new OperableObject(a.Num + b.Num); + } + + public static OperableObject operator -(OperableObject a, OperableObject b) + { + return new OperableObject(a.Num - b.Num); + } + + public static OperableObject operator *(OperableObject a, OperableObject b) + { + return new OperableObject(a.Num * b.Num); + } + + public static OperableObject operator /(OperableObject a, OperableObject b) + { + return new OperableObject(a.Num / b.Num); + } + + public static OperableObject operator &(OperableObject a, OperableObject b) + { + return new OperableObject(a.Num & b.Num); + } + + public static OperableObject operator |(OperableObject a, OperableObject b) + { + return new OperableObject(a.Num | b.Num); + } + + public static OperableObject operator ^(OperableObject a, OperableObject b) + { + return new OperableObject(a.Num ^ b.Num); + } + + public static OperableObject operator <<(OperableObject a, int offset) + { + return new OperableObject(a.Num << offset); + } + + public static OperableObject operator >>(OperableObject a, int offset) + { + return new OperableObject(a.Num >> offset); + } + } + + [Test] + public void OperatorOverloads() + { + string name = string.Format("{0}.{1}", + typeof(OperableObject).DeclaringType.Name, + typeof(OperableObject).Name); + string module = MethodBase.GetCurrentMethod().DeclaringType.Namespace; + + PythonEngine.Exec($@" +from {module} import * +cls = {name} +a = cls(2) +b = cls(10) +c = a + b +assert c.Num == a.Num + b.Num + +c = a - b +assert c.Num == a.Num - b.Num + +c = a * b +assert c.Num == a.Num * b.Num + +c = a / b +assert c.Num == a.Num // b.Num + +c = a & b +assert c.Num == a.Num & b.Num + +c = a | b +assert c.Num == a.Num | b.Num + +c = a ^ b +assert c.Num == a.Num ^ b.Num +"); + } + [Test] + public void BitOperatorOverloads() + { + string name = string.Format("{0}.{1}", + typeof(OperableObject).DeclaringType.Name, + typeof(OperableObject).Name); + string module = MethodBase.GetCurrentMethod().DeclaringType.Namespace; + + PythonEngine.Exec($@" +from {module} import * +cls = {name} +a = cls(2) +b = cls(10) + +c = a << b.Num +assert c.Num == a.Num << b.Num + +c = a >> b.Num +assert c.Num == a.Num >> b.Num +"); + } + } +} diff --git a/src/runtime/classmanager.cs b/src/runtime/classmanager.cs index c8bed6bc4..de9d43fc4 100644 --- a/src/runtime/classmanager.cs +++ b/src/runtime/classmanager.cs @@ -470,6 +470,10 @@ private static ClassInfo GetClassInfo(Type type) ob = new MethodObject(type, name, mlist); ci.members[name] = ob; + if (OperatorMethod.IsOperatorMethod(name)) + { + ci.members[OperatorMethod.GetPyMethodName(name)] = ob; + } } if (ci.indexer == null && type.IsClass) diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index 2cf548f48..e18a96916 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -342,15 +342,34 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth bool paramsArray; int kwargsMatched; int defaultsNeeded; - + bool isOperator = OperatorMethod.IsOperatorMethod(mi); // e.g. op_Addition is defined for OperableObject if (!MatchesArgumentCount(pynargs, pi, kwargDict, out paramsArray, out defaultArgList, out kwargsMatched, out defaultsNeeded)) { - continue; + if (isOperator) + { + defaultArgList = null; + } + else { continue; } } var outs = 0; + int clrnargs = pi.Length; + isOperator = isOperator && pynargs == clrnargs - 1; // Handle mismatched arg numbers due to Python operator being bound. var margs = TryConvertArguments(pi, paramsArray, args, pynargs, kwargDict, defaultArgList, - needsResolution: _methods.Length > 1, + needsResolution: _methods.Length > 1, // If there's more than one possible match. + isOperator: isOperator, outs: out outs); + if (isOperator) + { + if (inst != IntPtr.Zero) + { + var co = ManagedType.GetManagedObject(inst) as CLRObject; + if (co == null) + { + break; + } + margs[0] = co.inst; + } + } if (margs == null) { @@ -474,6 +493,7 @@ static IntPtr HandleParamsArray(IntPtr args, int arrayStart, int pyArgCount, out /// Dictionary of keyword argument name to python object pointer /// A list of default values for omitted parameters /// true, if overloading resolution is required + /// true, if is operator method /// Returns number of output parameters /// An array of .NET arguments, that can be passed to a method. static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray, @@ -481,6 +501,7 @@ static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray, Dictionary kwargDict, ArrayList defaultArgList, bool needsResolution, + bool isOperator, out int outs) { outs = 0; @@ -519,6 +540,12 @@ static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray, op = Runtime.PyTuple_GetItem(args, paramIndex); } } + if (isOperator && paramIndex == 0) + { + // After we've obtained the first argument from Python, we need to skip the first argument of the CLR + // because operator method is a bound method in Python + paramIndex++; // Leave the first .NET param as null (margs). + } bool isOut; if (!TryConvertArgument(op, parameter.ParameterType, needsResolution, out margs[paramIndex], out isOut)) @@ -543,6 +570,15 @@ static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray, return margs; } + /// + /// Try to convert a Python argument object to a managed CLR type. + /// + /// Pointer to the object at a particular parameter. + /// That parameter's managed type. + /// There are multiple overloading methods that need resolution. + /// Converted argument. + /// Whether the CLR type is passed by reference. + /// static bool TryConvertArgument(IntPtr op, Type parameterType, bool needsResolution, out object arg, out bool isOut) { @@ -633,7 +669,17 @@ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool return clrtype; } - + /// + /// Check whether the number of Python and .NET arguments match, and compute additional arg information. + /// + /// Number of positional args passed from Python. + /// Parameters of the specified .NET method. + /// Keyword args passed from Python. + /// True if the final param of the .NET method is an array (`params` keyword). + /// List of default values for arguments. + /// Number of kwargs from Python that are also present in the .NET method. + /// Number of non-null defaultsArgs. + /// static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] parameters, Dictionary kwargDict, out bool paramsArray, diff --git a/src/runtime/native/ITypeOffsets.cs b/src/runtime/native/ITypeOffsets.cs index 31344c66d..4d18a7c7d 100644 --- a/src/runtime/native/ITypeOffsets.cs +++ b/src/runtime/native/ITypeOffsets.cs @@ -15,6 +15,16 @@ interface ITypeOffsets int mp_subscript { get; } int name { get; } int nb_add { get; } + int nb_subtract { get; } + int nb_multiply { get; } + int nb_true_divide { get; } + int nb_and { get; } + int nb_or { get; } + int nb_xor { get; } + int nb_lshift { get; } + int nb_rshift { get; } + int nb_remainder { get; } + int nb_invert { get; } int nb_inplace_add { get; } int nb_inplace_subtract { get; } int ob_size { get; } diff --git a/src/runtime/native/TypeOffset.cs b/src/runtime/native/TypeOffset.cs index bca191565..3ea23e3fb 100644 --- a/src/runtime/native/TypeOffset.cs +++ b/src/runtime/native/TypeOffset.cs @@ -22,6 +22,16 @@ static partial class TypeOffset internal static int mp_subscript { get; private set; } internal static int name { get; private set; } internal static int nb_add { get; private set; } + internal static int nb_subtract { get; private set; } + internal static int nb_multiply { get; private set; } + internal static int nb_true_divide { get; private set; } + internal static int nb_and { get; private set; } + internal static int nb_or { get; private set; } + internal static int nb_xor { get; private set; } + internal static int nb_lshift { get; private set; } + internal static int nb_rshift { get; private set; } + internal static int nb_remainder { get; private set; } + internal static int nb_invert { get; private set; } internal static int nb_inplace_add { get; private set; } internal static int nb_inplace_subtract { get; private set; } internal static int ob_size { get; private set; } diff --git a/src/runtime/operatormethod.cs b/src/runtime/operatormethod.cs new file mode 100644 index 000000000..5a6a5c1ad --- /dev/null +++ b/src/runtime/operatormethod.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; + +namespace Python.Runtime +{ + internal static class OperatorMethod + { + public static Dictionary> OpMethodMap { get; private set; } + + private static Dictionary _pyOpNames; + private static PyObject _opType; + + static OperatorMethod() + { + // TODO: Rich compare, inplace operator support + OpMethodMap = new Dictionary> + { + ["op_Addition"] = Tuple.Create("__add__", TypeOffset.nb_add), + ["op_Subtraction"] = Tuple.Create("__sub__", TypeOffset.nb_subtract), + ["op_Multiply"] = Tuple.Create("__mul__", TypeOffset.nb_multiply), +#if PYTHON2 + ["op_Division"] = Tuple.Create("__div__", TypeOffset.nb_divide), +#else + ["op_Division"] = Tuple.Create("__truediv__", TypeOffset.nb_true_divide), +#endif + ["op_BitwiseAnd"] = Tuple.Create("__and__", TypeOffset.nb_and), + ["op_BitwiseOr"] = Tuple.Create("__or__", TypeOffset.nb_or), + ["op_ExclusiveOr"] = Tuple.Create("__xor__", TypeOffset.nb_xor), + ["op_LeftShift"] = Tuple.Create("__lshift__", TypeOffset.nb_lshift), + ["op_RightShift"] = Tuple.Create("__rshift__", TypeOffset.nb_rshift), + ["op_Modulus"] = Tuple.Create("__mod__", TypeOffset.nb_remainder), + ["op_OneComplement"] = Tuple.Create("__invert__", TypeOffset.nb_invert) + }; + + _pyOpNames = new Dictionary(); + foreach (string name in OpMethodMap.Keys) + { + _pyOpNames.Add(GetPyMethodName(name), name); + } + } + + public static void Initialize() + { + _opType = GetOperatorType(); + } + + public static void Shutdown() + { + if (_opType != null) + { + _opType.Dispose(); + _opType = null; + } + } + + public static bool IsOperatorMethod(string methodName) + { + return OpMethodMap.ContainsKey(methodName); + } + + public static bool IsPyOperatorMethod(string pyMethodName) + { + return _pyOpNames.ContainsKey(pyMethodName); + } + + public static bool IsOperatorMethod(MethodBase method) + { + if (!method.IsSpecialName) + { + return false; + } + return OpMethodMap.ContainsKey(method.Name); + } + + public static void FixupSlots(IntPtr pyType, Type clrType) + { + // This is not used + IntPtr tp_as_number = Marshal.ReadIntPtr(pyType, TypeOffset.tp_as_number); + + const BindingFlags flags = BindingFlags.Public | BindingFlags.Static; + Debug.Assert(_opType != null); + foreach (var method in clrType.GetMethods(flags)) + { + if (!IsOperatorMethod(method)) + { + continue; + } + var slotdef = OpMethodMap[method.Name]; + int offset = slotdef.Item2; + IntPtr func = Marshal.ReadIntPtr(_opType.Handle, offset); + Marshal.WriteIntPtr(pyType, offset, func); + } + } + + public static string GetPyMethodName(string clrName) + { + return OpMethodMap[clrName].Item1; + } + + private static string GenerateDummyCode() + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("class OperatorMethod(object):"); + foreach (var item in OpMethodMap.Values) + { + string def = string.Format(" def {0}(self, other): pass", item.Item1); + sb.AppendLine(def); + } + return sb.ToString(); + } + + private static PyObject GetOperatorType() + { + using (PyDict locals = new PyDict()) + { + // A hack way for getting typeobject.c::slotdefs + string code = GenerateDummyCode(); + PythonEngine.Exec(code, null, locals.Handle); + return locals.GetItem("OperatorMethod"); + } + } + } +} diff --git a/src/runtime/runtime.cs b/src/runtime/runtime.cs index f80db04b6..1e8db8278 100644 --- a/src/runtime/runtime.cs +++ b/src/runtime/runtime.cs @@ -160,6 +160,7 @@ internal static void Initialize(bool initSigs = false, ShutdownMode mode = Shutd // Initialize modules that depend on the runtime class. AssemblyManager.Initialize(); + OperatorMethod.Initialize(); if (mode == ShutdownMode.Reload && RuntimeData.HasStashData()) { RuntimeData.RestoreRuntimeData(); @@ -345,6 +346,7 @@ internal static void Shutdown(ShutdownMode mode) RuntimeData.Stash(); } AssemblyManager.Shutdown(); + OperatorMethod.Shutdown(); ImportHook.Shutdown(); ClearClrModules(); diff --git a/src/runtime/typemanager.cs b/src/runtime/typemanager.cs index 49a46cb72..e5387600f 100644 --- a/src/runtime/typemanager.cs +++ b/src/runtime/typemanager.cs @@ -281,6 +281,7 @@ internal static IntPtr CreateType(ManagedType impl, Type clrType) { throw new PythonException(); } + OperatorMethod.FixupSlots(type, clrType); IntPtr dict = Marshal.ReadIntPtr(type, TypeOffset.tp_dict); string mn = clrType.Namespace ?? ""; From 0d0ca8716ae2b92de6f44eddf4352c86e9c3bea1 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Thu, 17 Dec 2020 00:28:35 +0000 Subject: [PATCH 02/24] Remove params array tests since they are unrelated to operator overloads. --- src/embed_tests/TestPyMethod.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/embed_tests/TestPyMethod.cs b/src/embed_tests/TestPyMethod.cs index 0d06e9e1a..cb0480c94 100644 --- a/src/embed_tests/TestPyMethod.cs +++ b/src/embed_tests/TestPyMethod.cs @@ -26,11 +26,6 @@ public class SampleClass public int VoidCall() => 10; public int Foo(int a, int b = 10) => a + b; - - public int Foo2(int a = 10, params int[] args) - { - return a + args.Sum(); - } } [Test] @@ -62,11 +57,6 @@ public void TestDefaultParameter() obj = SampleClass() assert obj.Foo(10) == 20 assert obj.Foo(10, 1) == 11 - -assert obj.Foo2() == 10 -assert obj.Foo2(20) == 20 -assert obj.Foo2(20, 30) == 50 -assert obj.Foo2(20, 30, 50) == 100 "); } From 253f9c79909b38a2cf9dddbb857acb7729113e8c Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Thu, 17 Dec 2020 08:26:53 +0000 Subject: [PATCH 03/24] Fix type bug; rename variables pynargs etc. --- src/runtime/methodbinder.cs | 39 +++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index e18a96916..95184931f 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -343,13 +343,9 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth int kwargsMatched; int defaultsNeeded; bool isOperator = OperatorMethod.IsOperatorMethod(mi); // e.g. op_Addition is defined for OperableObject - if (!MatchesArgumentCount(pynargs, pi, kwargDict, out paramsArray, out defaultArgList, out kwargsMatched, out defaultsNeeded)) + if (!MatchesArgumentCount(pynargs, pi, kwargDict, isOperator, out paramsArray, out defaultArgList, out kwargsMatched, out defaultsNeeded)) { - if (isOperator) - { - defaultArgList = null; - } - else { continue; } + continue; } var outs = 0; int clrnargs = pi.Length; @@ -545,6 +541,7 @@ static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray, // After we've obtained the first argument from Python, we need to skip the first argument of the CLR // because operator method is a bound method in Python paramIndex++; // Leave the first .NET param as null (margs). + parameter = pi[paramIndex]; } bool isOut; @@ -672,16 +669,18 @@ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool /// /// Check whether the number of Python and .NET arguments match, and compute additional arg information. /// - /// Number of positional args passed from Python. + /// Number of positional args passed from Python. /// Parameters of the specified .NET method. /// Keyword args passed from Python. + /// True if the parameters' method is an operator. /// True if the final param of the .NET method is an array (`params` keyword). /// List of default values for arguments. /// Number of kwargs from Python that are also present in the .NET method. /// Number of non-null defaultsArgs. /// - static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] parameters, + static bool MatchesArgumentCount(int pynargs, ParameterInfo[] parameters, Dictionary kwargDict, + bool isOperator, out bool paramsArray, out ArrayList defaultArgList, out int kwargsMatched, @@ -689,22 +688,23 @@ static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] pa { defaultArgList = null; var match = false; - paramsArray = parameters.Length > 0 ? Attribute.IsDefined(parameters[parameters.Length - 1], typeof(ParamArrayAttribute)) : false; + int clrnargs = parameters.Length; + paramsArray = clrnargs > 0 ? Attribute.IsDefined(parameters[clrnargs - 1], typeof(ParamArrayAttribute)) : false; var kwargCount = kwargDict.Count; kwargsMatched = 0; defaultsNeeded = 0; - - if (positionalArgumentCount == parameters.Length && kwargDict.Count == 0) + if (pynargs == clrnargs && kwargDict.Count == 0) { match = true; } - else if (positionalArgumentCount < parameters.Length && (!paramsArray || positionalArgumentCount == parameters.Length - 1)) + else if (pynargs < clrnargs && (!paramsArray || pynargs == clrnargs - 1)) { // every parameter past 'positionalArgumentCount' must have either - // a corresponding keyword argument or a default parameter + // a corresponding keyword argument or a default parameter, unless + // the method is an operator or accepts a params array. match = true; defaultArgList = new ArrayList(); - for (var v = positionalArgumentCount; v < parameters.Length; v++) + for (var v = pynargs; v < clrnargs; v++) { if (kwargDict.ContainsKey(parameters[v].Name)) { @@ -723,14 +723,19 @@ static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] pa defaultArgList.Add(parameters[v].GetDefaultValue()); defaultsNeeded++; } - else if (!paramsArray) + else if (!isOperator && !paramsArray) { match = false; } } + if (isOperator && defaultArgList.Count == 0) + { + // If no default arguments were provided for an operable object. + defaultArgList = null; + } } - else if (positionalArgumentCount > parameters.Length && parameters.Length > 0 && - Attribute.IsDefined(parameters[parameters.Length - 1], typeof(ParamArrayAttribute))) + else if (pynargs > clrnargs && clrnargs > 0 && + Attribute.IsDefined(parameters[clrnargs - 1], typeof(ParamArrayAttribute))) { // This is a `foo(params object[] bar)` style method match = true; From 8cdb61cd9a03ed6275140039ca04bf9d2a88a153 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Thu, 17 Dec 2020 12:36:28 +0000 Subject: [PATCH 04/24] Remove unused variables and add comments. --- src/runtime/methodbinder.cs | 6 ++++-- src/runtime/operatormethod.cs | 3 --- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index 95184931f..1f8914775 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -690,7 +690,6 @@ static bool MatchesArgumentCount(int pynargs, ParameterInfo[] parameters, var match = false; int clrnargs = parameters.Length; paramsArray = clrnargs > 0 ? Attribute.IsDefined(parameters[clrnargs - 1], typeof(ParamArrayAttribute)) : false; - var kwargCount = kwargDict.Count; kwargsMatched = 0; defaultsNeeded = 0; if (pynargs == clrnargs && kwargDict.Count == 0) @@ -701,7 +700,8 @@ static bool MatchesArgumentCount(int pynargs, ParameterInfo[] parameters, { // every parameter past 'positionalArgumentCount' must have either // a corresponding keyword argument or a default parameter, unless - // the method is an operator or accepts a params array. + // the method is an operator or accepts a params array (which cannot + // have a default value) match = true; defaultArgList = new ArrayList(); for (var v = pynargs; v < clrnargs; v++) @@ -725,6 +725,8 @@ static bool MatchesArgumentCount(int pynargs, ParameterInfo[] parameters, } else if (!isOperator && !paramsArray) { + // this is separate above because an operator method cannot have + // keyword args, default args, or params arrays (exclusive cases) match = false; } } diff --git a/src/runtime/operatormethod.cs b/src/runtime/operatormethod.cs index 5a6a5c1ad..2706dd9cf 100644 --- a/src/runtime/operatormethod.cs +++ b/src/runtime/operatormethod.cs @@ -78,9 +78,6 @@ public static bool IsOperatorMethod(MethodBase method) public static void FixupSlots(IntPtr pyType, Type clrType) { - // This is not used - IntPtr tp_as_number = Marshal.ReadIntPtr(pyType, TypeOffset.tp_as_number); - const BindingFlags flags = BindingFlags.Public | BindingFlags.Static; Debug.Assert(_opType != null); foreach (var method in clrType.GetMethods(flags)) From c26e5892996cf3463a5cd4512ef5d9fde118740f Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Fri, 18 Dec 2020 09:20:05 +0000 Subject: [PATCH 05/24] Add comments and remove unused in operatormethod.cs Also move fixupslots before pytype_ready. Also apply access violation fix in classderived. --- src/runtime/classderived.cs | 7 ++++++- src/runtime/operatormethod.cs | 34 +++++++++++++++++++--------------- src/runtime/typemanager.cs | 2 +- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/runtime/classderived.cs b/src/runtime/classderived.cs index 4e8e88bf3..d12674d68 100644 --- a/src/runtime/classderived.cs +++ b/src/runtime/classderived.cs @@ -66,7 +66,12 @@ internal ClassDerivedObject(Type tp) : base(tp) public new static void tp_dealloc(IntPtr ob) { var self = (CLRObject)GetManagedObject(ob); - + // Amos's fix https://github.com/pythonnet/pythonnet/pull/1260#issuecomment-726719595 + if (self.tpHandle == IntPtr.Zero) + { + ClassBase.tp_dealloc(ob); + return; + } // don't let the python GC destroy this object Runtime.PyObject_GC_UnTrack(self.pyHandle); diff --git a/src/runtime/operatormethod.cs b/src/runtime/operatormethod.cs index 2706dd9cf..2aa39a04f 100644 --- a/src/runtime/operatormethod.cs +++ b/src/runtime/operatormethod.cs @@ -9,9 +9,11 @@ namespace Python.Runtime { internal static class OperatorMethod { + // Maps the compiled method name in .NET CIL (e.g. op_Addition) to + // the equivalent Python operator (e.g. __add__) as well as the offset + // that identifies that operator's slot (e.g. nb_add) in heap space. public static Dictionary> OpMethodMap { get; private set; } - private static Dictionary _pyOpNames; private static PyObject _opType; static OperatorMethod() @@ -35,12 +37,6 @@ static OperatorMethod() ["op_Modulus"] = Tuple.Create("__mod__", TypeOffset.nb_remainder), ["op_OneComplement"] = Tuple.Create("__invert__", TypeOffset.nb_invert) }; - - _pyOpNames = new Dictionary(); - foreach (string name in OpMethodMap.Keys) - { - _pyOpNames.Add(GetPyMethodName(name), name); - } } public static void Initialize() @@ -62,11 +58,6 @@ public static bool IsOperatorMethod(string methodName) return OpMethodMap.ContainsKey(methodName); } - public static bool IsPyOperatorMethod(string pyMethodName) - { - return _pyOpNames.ContainsKey(pyMethodName); - } - public static bool IsOperatorMethod(MethodBase method) { if (!method.IsSpecialName) @@ -75,7 +66,12 @@ public static bool IsOperatorMethod(MethodBase method) } return OpMethodMap.ContainsKey(method.Name); } - + /// + /// For the operator methods of a CLR type, set the special slots of the + /// corresponding Python type's operator methods. + /// + /// + /// public static void FixupSlots(IntPtr pyType, Type clrType) { const BindingFlags flags = BindingFlags.Public | BindingFlags.Static; @@ -86,10 +82,16 @@ public static void FixupSlots(IntPtr pyType, Type clrType) { continue; } - var slotdef = OpMethodMap[method.Name]; - int offset = slotdef.Item2; + int offset = OpMethodMap[method.Name].Item2; + // Copy the default implementation of e.g. the nb_add slot, + // which simply calls __add__ on the type. IntPtr func = Marshal.ReadIntPtr(_opType.Handle, offset); + // Write the slot definition of the target Python type, so + // that we can later modify __add___ and it will be called + // when used with a Python operator. + // https://tenthousandmeters.com/blog/python-behind-the-scenes-6-how-python-object-system-works/ Marshal.WriteIntPtr(pyType, offset, func); + } } @@ -116,7 +118,9 @@ private static PyObject GetOperatorType() { // A hack way for getting typeobject.c::slotdefs string code = GenerateDummyCode(); + // The resulting OperatorMethod class is stored in a PyDict. PythonEngine.Exec(code, null, locals.Handle); + // Return the class itself, which is a type. return locals.GetItem("OperatorMethod"); } } diff --git a/src/runtime/typemanager.cs b/src/runtime/typemanager.cs index e5387600f..31682c519 100644 --- a/src/runtime/typemanager.cs +++ b/src/runtime/typemanager.cs @@ -273,6 +273,7 @@ internal static IntPtr CreateType(ManagedType impl, Type clrType) | TypeFlags.HaveGC; Util.WriteCLong(type, TypeOffset.tp_flags, flags); + OperatorMethod.FixupSlots(type, clrType); // Leverage followup initialization from the Python runtime. Note // that the type of the new type must PyType_Type at the time we // call this, else PyType_Ready will skip some slot initialization. @@ -281,7 +282,6 @@ internal static IntPtr CreateType(ManagedType impl, Type clrType) { throw new PythonException(); } - OperatorMethod.FixupSlots(type, clrType); IntPtr dict = Marshal.ReadIntPtr(type, TypeOffset.tp_dict); string mn = clrType.Namespace ?? ""; From 3222a543e821034bdf0eb4f3341b056f164745a7 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Sun, 20 Dec 2020 14:04:46 +0000 Subject: [PATCH 06/24] Address review by @lostmsu; Add forward operator test. --- .../{TestPyMethod.cs => TestOperator.cs} | 58 ++++++++----------- src/runtime/classderived.cs | 7 +-- src/runtime/classmanager.cs | 2 +- src/runtime/methodbinder.cs | 8 +-- src/runtime/operatormethod.cs | 52 ++++++++--------- 5 files changed, 56 insertions(+), 71 deletions(-) rename src/embed_tests/{TestPyMethod.cs => TestOperator.cs} (83%) diff --git a/src/embed_tests/TestPyMethod.cs b/src/embed_tests/TestOperator.cs similarity index 83% rename from src/embed_tests/TestPyMethod.cs rename to src/embed_tests/TestOperator.cs index cb0480c94..c46626e9e 100644 --- a/src/embed_tests/TestPyMethod.cs +++ b/src/embed_tests/TestOperator.cs @@ -7,7 +7,7 @@ namespace Python.EmbeddingTest { - public class TestPyMethod + public class TestOperator { [OneTimeSetUp] public void SetUp() @@ -28,38 +28,6 @@ public class SampleClass public int Foo(int a, int b = 10) => a + b; } - [Test] - public void TestVoidCall() - { - string name = string.Format("{0}.{1}", - typeof(SampleClass).DeclaringType.Name, - typeof(SampleClass).Name); - string module = MethodBase.GetCurrentMethod().DeclaringType.Namespace; - PythonEngine.Exec($@" -from {module} import * -SampleClass = {name} -obj = SampleClass() -assert obj.VoidCall() == 10 -"); - } - - [Test] - public void TestDefaultParameter() - { - string name = string.Format("{0}.{1}", - typeof(SampleClass).DeclaringType.Name, - typeof(SampleClass).Name); - string module = MethodBase.GetCurrentMethod().DeclaringType.Namespace; - - PythonEngine.Exec($@" -from {module} import * -SampleClass = {name} -obj = SampleClass() -assert obj.Foo(10) == 20 -assert obj.Foo(10, 1) == 11 -"); - } - public class OperableObject { public int Num { get; set; } @@ -73,6 +41,10 @@ public OperableObject(int num) { return new OperableObject(a.Num + b.Num); } + public static OperableObject operator +(OperableObject a, int b) + { + return new OperableObject(a.Num + b); + } public static OperableObject operator -(OperableObject a, OperableObject b) { @@ -151,7 +123,25 @@ public void OperatorOverloads() "); } [Test] - public void BitOperatorOverloads() + public void ForwardOperatorOverloads() + { + string name = string.Format("{0}.{1}", + typeof(OperableObject).DeclaringType.Name, + typeof(OperableObject).Name); + string module = MethodBase.GetCurrentMethod().DeclaringType.Namespace; + + PythonEngine.Exec($@" +from {module} import * +cls = {name} +a = cls(2) +b = cls(10) + +c = a + b.Num +assert c.Num == a.Num + b.Num +"); + } + [Test] + public void ShiftOperatorOverloads() { string name = string.Format("{0}.{1}", typeof(OperableObject).DeclaringType.Name, diff --git a/src/runtime/classderived.cs b/src/runtime/classderived.cs index d12674d68..4e8e88bf3 100644 --- a/src/runtime/classderived.cs +++ b/src/runtime/classderived.cs @@ -66,12 +66,7 @@ internal ClassDerivedObject(Type tp) : base(tp) public new static void tp_dealloc(IntPtr ob) { var self = (CLRObject)GetManagedObject(ob); - // Amos's fix https://github.com/pythonnet/pythonnet/pull/1260#issuecomment-726719595 - if (self.tpHandle == IntPtr.Zero) - { - ClassBase.tp_dealloc(ob); - return; - } + // don't let the python GC destroy this object Runtime.PyObject_GC_UnTrack(self.pyHandle); diff --git a/src/runtime/classmanager.cs b/src/runtime/classmanager.cs index de9d43fc4..c022553a1 100644 --- a/src/runtime/classmanager.cs +++ b/src/runtime/classmanager.cs @@ -470,7 +470,7 @@ private static ClassInfo GetClassInfo(Type type) ob = new MethodObject(type, name, mlist); ci.members[name] = ob; - if (OperatorMethod.IsOperatorMethod(name)) + if (OperatorMethod.IsOperatorMethod(mlist[0])) { ci.members[OperatorMethod.GetPyMethodName(name)] = ob; } diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index 1f8914775..8e8774795 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -354,6 +354,10 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth needsResolution: _methods.Length > 1, // If there's more than one possible match. isOperator: isOperator, outs: out outs); + if (margs == null) + { + continue; + } if (isOperator) { if (inst != IntPtr.Zero) @@ -367,10 +371,6 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth } } - if (margs == null) - { - continue; - } var matchedMethod = new MatchedMethod(kwargsMatched, defaultsNeeded, margs, outs, mi); argMatchedMethods.Add(matchedMethod); diff --git a/src/runtime/operatormethod.cs b/src/runtime/operatormethod.cs index 2aa39a04f..e77b397e8 100644 --- a/src/runtime/operatormethod.cs +++ b/src/runtime/operatormethod.cs @@ -12,30 +12,35 @@ internal static class OperatorMethod // Maps the compiled method name in .NET CIL (e.g. op_Addition) to // the equivalent Python operator (e.g. __add__) as well as the offset // that identifies that operator's slot (e.g. nb_add) in heap space. - public static Dictionary> OpMethodMap { get; private set; } - + public static Dictionary OpMethodMap { get; private set; } + public readonly struct SlotDefinition + { + public SlotDefinition(string methodName, int typeOffset) + { + MethodName = methodName; + TypeOffset = typeOffset; + } + public string MethodName { get; } + public int TypeOffset { get; } + } private static PyObject _opType; static OperatorMethod() { // TODO: Rich compare, inplace operator support - OpMethodMap = new Dictionary> + OpMethodMap = new Dictionary { - ["op_Addition"] = Tuple.Create("__add__", TypeOffset.nb_add), - ["op_Subtraction"] = Tuple.Create("__sub__", TypeOffset.nb_subtract), - ["op_Multiply"] = Tuple.Create("__mul__", TypeOffset.nb_multiply), -#if PYTHON2 - ["op_Division"] = Tuple.Create("__div__", TypeOffset.nb_divide), -#else - ["op_Division"] = Tuple.Create("__truediv__", TypeOffset.nb_true_divide), -#endif - ["op_BitwiseAnd"] = Tuple.Create("__and__", TypeOffset.nb_and), - ["op_BitwiseOr"] = Tuple.Create("__or__", TypeOffset.nb_or), - ["op_ExclusiveOr"] = Tuple.Create("__xor__", TypeOffset.nb_xor), - ["op_LeftShift"] = Tuple.Create("__lshift__", TypeOffset.nb_lshift), - ["op_RightShift"] = Tuple.Create("__rshift__", TypeOffset.nb_rshift), - ["op_Modulus"] = Tuple.Create("__mod__", TypeOffset.nb_remainder), - ["op_OneComplement"] = Tuple.Create("__invert__", TypeOffset.nb_invert) + ["op_Addition"] = new SlotDefinition("__add__", TypeOffset.nb_add), + ["op_Subtraction"] = new SlotDefinition("__sub__", TypeOffset.nb_subtract), + ["op_Multiply"] = new SlotDefinition("__mul__", TypeOffset.nb_multiply), + ["op_Division"] = new SlotDefinition("__truediv__", TypeOffset.nb_true_divide), + ["op_BitwiseAnd"] = new SlotDefinition("__and__", TypeOffset.nb_and), + ["op_BitwiseOr"] = new SlotDefinition("__or__", TypeOffset.nb_or), + ["op_ExclusiveOr"] = new SlotDefinition("__xor__", TypeOffset.nb_xor), + ["op_LeftShift"] = new SlotDefinition("__lshift__", TypeOffset.nb_lshift), + ["op_RightShift"] = new SlotDefinition("__rshift__", TypeOffset.nb_rshift), + ["op_Modulus"] = new SlotDefinition("__mod__", TypeOffset.nb_remainder), + ["op_OneComplement"] = new SlotDefinition("__invert__", TypeOffset.nb_invert) }; } @@ -53,11 +58,6 @@ public static void Shutdown() } } - public static bool IsOperatorMethod(string methodName) - { - return OpMethodMap.ContainsKey(methodName); - } - public static bool IsOperatorMethod(MethodBase method) { if (!method.IsSpecialName) @@ -82,7 +82,7 @@ public static void FixupSlots(IntPtr pyType, Type clrType) { continue; } - int offset = OpMethodMap[method.Name].Item2; + int offset = OpMethodMap[method.Name].TypeOffset; // Copy the default implementation of e.g. the nb_add slot, // which simply calls __add__ on the type. IntPtr func = Marshal.ReadIntPtr(_opType.Handle, offset); @@ -97,7 +97,7 @@ public static void FixupSlots(IntPtr pyType, Type clrType) public static string GetPyMethodName(string clrName) { - return OpMethodMap[clrName].Item1; + return OpMethodMap[clrName].MethodName; } private static string GenerateDummyCode() @@ -106,7 +106,7 @@ private static string GenerateDummyCode() sb.AppendLine("class OperatorMethod(object):"); foreach (var item in OpMethodMap.Values) { - string def = string.Format(" def {0}(self, other): pass", item.Item1); + string def = string.Format(" def {0}(self, other): pass", item.MethodName); sb.AppendLine(def); } return sb.ToString(); From 550ff31ef7b5bf79565422aa439a35b316539690 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Mon, 21 Dec 2020 08:23:44 +0000 Subject: [PATCH 07/24] Add forward operator tests (OpObj, int). --- src/embed_tests/TestOperator.cs | 50 +++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/src/embed_tests/TestOperator.cs b/src/embed_tests/TestOperator.cs index c46626e9e..30d7d0e45 100644 --- a/src/embed_tests/TestOperator.cs +++ b/src/embed_tests/TestOperator.cs @@ -50,31 +50,58 @@ public OperableObject(int num) { return new OperableObject(a.Num - b.Num); } + public static OperableObject operator -(OperableObject a, int b) + { + return new OperableObject(a.Num - b); + } public static OperableObject operator *(OperableObject a, OperableObject b) { return new OperableObject(a.Num * b.Num); } + public static OperableObject operator *(OperableObject a, int b) + { + return new OperableObject(a.Num * b); + } public static OperableObject operator /(OperableObject a, OperableObject b) { return new OperableObject(a.Num / b.Num); } + public static OperableObject operator /(OperableObject a, int b) + { + return new OperableObject(a.Num / b); + } + public static OperableObject operator &(OperableObject a, OperableObject b) { return new OperableObject(a.Num & b.Num); } + public static OperableObject operator &(OperableObject a, int b) + { + return new OperableObject(a.Num & b); + } + public static OperableObject operator |(OperableObject a, OperableObject b) { return new OperableObject(a.Num | b.Num); } + public static OperableObject operator |(OperableObject a, int b) + { + return new OperableObject(a.Num | b); + } + public static OperableObject operator ^(OperableObject a, OperableObject b) { return new OperableObject(a.Num ^ b.Num); } + public static OperableObject operator ^(OperableObject a, int b) + { + return new OperableObject(a.Num ^ b); + } public static OperableObject operator <<(OperableObject a, int offset) { @@ -134,10 +161,27 @@ public void ForwardOperatorOverloads() from {module} import * cls = {name} a = cls(2) -b = cls(10) +b = 10 +c = a + b +assert c.Num == a.Num + b -c = a + b.Num -assert c.Num == a.Num + b.Num +c = a - b +assert c.Num == a.Num - b + +c = a * b +assert c.Num == a.Num * b + +c = a / b +assert c.Num == a.Num // b + +c = a & b +assert c.Num == a.Num & b + +c = a | b +assert c.Num == a.Num | b + +c = a ^ b +assert c.Num == a.Num ^ b "); } [Test] From eab8edcc4e470b1ad15358fd7a7ae38c24e499ab Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Mon, 21 Dec 2020 11:09:49 +0000 Subject: [PATCH 08/24] Address @amos402's comment on raising branch. --- src/runtime/methodbinder.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index 8e8774795..fe538925a 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -698,11 +698,18 @@ static bool MatchesArgumentCount(int pynargs, ParameterInfo[] parameters, } else if (pynargs < clrnargs && (!paramsArray || pynargs == clrnargs - 1)) { - // every parameter past 'positionalArgumentCount' must have either - // a corresponding keyword argument or a default parameter, unless - // the method is an operator or accepts a params array (which cannot - // have a default value) match = true; + // operator methods will have 2 CLR args but only one Python arg, + // since Python operator methods are bound + if (isOperator) + { + // return early since a C# operator method cannot have + // keyword args, default args, or params arrays (exclusive cases) + return match; + } + // every parameter past 'positionalArgumentCount' must have either + // a corresponding keyword arg or a default param, unless the method + // method accepts a params array (which cannot have a default value) defaultArgList = new ArrayList(); for (var v = pynargs; v < clrnargs; v++) { @@ -723,18 +730,11 @@ static bool MatchesArgumentCount(int pynargs, ParameterInfo[] parameters, defaultArgList.Add(parameters[v].GetDefaultValue()); defaultsNeeded++; } - else if (!isOperator && !paramsArray) + else if (!paramsArray) { - // this is separate above because an operator method cannot have - // keyword args, default args, or params arrays (exclusive cases) match = false; } } - if (isOperator && defaultArgList.Count == 0) - { - // If no default arguments were provided for an operable object. - defaultArgList = null; - } } else if (pynargs > clrnargs && clrnargs > 0 && Attribute.IsDefined(parameters[clrnargs - 1], typeof(ParamArrayAttribute))) From c2be3f130fd8620ce10e1457d2193c6bd706c48b Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Mon, 21 Dec 2020 19:32:03 +0000 Subject: [PATCH 09/24] Add operator overload and rename pynargs, clrnargs --- src/embed_tests/TestOperator.cs | 29 +++++++++++++++++++++++++++++ src/runtime/methodbinder.cs | 28 ++++++++++++++-------------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/embed_tests/TestOperator.cs b/src/embed_tests/TestOperator.cs index 30d7d0e45..a309010a2 100644 --- a/src/embed_tests/TestOperator.cs +++ b/src/embed_tests/TestOperator.cs @@ -37,6 +37,10 @@ public OperableObject(int num) Num = num; } + public static OperableObject operator +(int a, OperableObject b) + { + return new OperableObject(a + b.Num); + } public static OperableObject operator +(OperableObject a, OperableObject b) { return new OperableObject(a.Num + b.Num); @@ -46,6 +50,10 @@ public OperableObject(int num) return new OperableObject(a.Num + b); } + public static OperableObject operator -(int a, OperableObject b) + { + return new OperableObject(a - b.Num); + } public static OperableObject operator -(OperableObject a, OperableObject b) { return new OperableObject(a.Num - b.Num); @@ -55,6 +63,11 @@ public OperableObject(int num) return new OperableObject(a.Num - b); } + + public static OperableObject operator *(int a, OperableObject b) + { + return new OperableObject(a * b.Num); + } public static OperableObject operator *(OperableObject a, OperableObject b) { return new OperableObject(a.Num * b.Num); @@ -64,6 +77,10 @@ public OperableObject(int num) return new OperableObject(a.Num * b); } + public static OperableObject operator /(int a, OperableObject b) + { + return new OperableObject(a / b.Num); + } public static OperableObject operator /(OperableObject a, OperableObject b) { return new OperableObject(a.Num / b.Num); @@ -74,6 +91,10 @@ public OperableObject(int num) } + public static OperableObject operator &(int a, OperableObject b) + { + return new OperableObject(a & b.Num); + } public static OperableObject operator &(OperableObject a, OperableObject b) { return new OperableObject(a.Num & b.Num); @@ -84,6 +105,10 @@ public OperableObject(int num) } + public static OperableObject operator |(int a, OperableObject b) + { + return new OperableObject(a | b.Num); + } public static OperableObject operator |(OperableObject a, OperableObject b) { return new OperableObject(a.Num | b.Num); @@ -94,6 +119,10 @@ public OperableObject(int num) } + public static OperableObject operator ^(int a, OperableObject b) + { + return new OperableObject(a ^ b.Num); + } public static OperableObject operator ^(OperableObject a, OperableObject b) { return new OperableObject(a.Num ^ b.Num); diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index fe538925a..4bc1b6baf 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -316,7 +316,7 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth Runtime.XDecref(valueList); } - var pynargs = (int)Runtime.PyTuple_Size(args); + var numPyArgs = (int)Runtime.PyTuple_Size(args); var isGeneric = false; if (info != null) { @@ -343,14 +343,14 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth int kwargsMatched; int defaultsNeeded; bool isOperator = OperatorMethod.IsOperatorMethod(mi); // e.g. op_Addition is defined for OperableObject - if (!MatchesArgumentCount(pynargs, pi, kwargDict, isOperator, out paramsArray, out defaultArgList, out kwargsMatched, out defaultsNeeded)) + if (!MatchesArgumentCount(numPyArgs, pi, kwargDict, isOperator, out paramsArray, out defaultArgList, out kwargsMatched, out defaultsNeeded)) { continue; } var outs = 0; - int clrnargs = pi.Length; - isOperator = isOperator && pynargs == clrnargs - 1; // Handle mismatched arg numbers due to Python operator being bound. - var margs = TryConvertArguments(pi, paramsArray, args, pynargs, kwargDict, defaultArgList, + int numClrArgs = pi.Length; + isOperator = isOperator && numPyArgs == numClrArgs - 1; // Handle mismatched arg numbers due to Python operator being bound. + var margs = TryConvertArguments(pi, paramsArray, args, numPyArgs, kwargDict, defaultArgList, needsResolution: _methods.Length > 1, // If there's more than one possible match. isOperator: isOperator, outs: out outs); @@ -669,7 +669,7 @@ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool /// /// Check whether the number of Python and .NET arguments match, and compute additional arg information. /// - /// Number of positional args passed from Python. + /// Number of positional args passed from Python. /// Parameters of the specified .NET method. /// Keyword args passed from Python. /// True if the parameters' method is an operator. @@ -678,7 +678,7 @@ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool /// Number of kwargs from Python that are also present in the .NET method. /// Number of non-null defaultsArgs. /// - static bool MatchesArgumentCount(int pynargs, ParameterInfo[] parameters, + static bool MatchesArgumentCount(int numPyArgs, ParameterInfo[] parameters, Dictionary kwargDict, bool isOperator, out bool paramsArray, @@ -688,15 +688,15 @@ static bool MatchesArgumentCount(int pynargs, ParameterInfo[] parameters, { defaultArgList = null; var match = false; - int clrnargs = parameters.Length; - paramsArray = clrnargs > 0 ? Attribute.IsDefined(parameters[clrnargs - 1], typeof(ParamArrayAttribute)) : false; + int numClrArgs = parameters.Length; + paramsArray = numClrArgs > 0 ? Attribute.IsDefined(parameters[numClrArgs - 1], typeof(ParamArrayAttribute)) : false; kwargsMatched = 0; defaultsNeeded = 0; - if (pynargs == clrnargs && kwargDict.Count == 0) + if (numPyArgs == numClrArgs && kwargDict.Count == 0) { match = true; } - else if (pynargs < clrnargs && (!paramsArray || pynargs == clrnargs - 1)) + else if (numPyArgs < numClrArgs && (!paramsArray || numPyArgs == numClrArgs - 1)) { match = true; // operator methods will have 2 CLR args but only one Python arg, @@ -711,7 +711,7 @@ static bool MatchesArgumentCount(int pynargs, ParameterInfo[] parameters, // a corresponding keyword arg or a default param, unless the method // method accepts a params array (which cannot have a default value) defaultArgList = new ArrayList(); - for (var v = pynargs; v < clrnargs; v++) + for (var v = numPyArgs; v < numClrArgs; v++) { if (kwargDict.ContainsKey(parameters[v].Name)) { @@ -736,8 +736,8 @@ static bool MatchesArgumentCount(int pynargs, ParameterInfo[] parameters, } } } - else if (pynargs > clrnargs && clrnargs > 0 && - Attribute.IsDefined(parameters[clrnargs - 1], typeof(ParamArrayAttribute))) + else if (numPyArgs > numClrArgs && numClrArgs > 0 && + Attribute.IsDefined(parameters[numClrArgs - 1], typeof(ParamArrayAttribute))) { // This is a `foo(params object[] bar)` style method match = true; From 581a0474027385f758638dfa24dd9ff22b017576 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Tue, 22 Dec 2020 08:36:10 +0000 Subject: [PATCH 10/24] Revert variable renames --- src/runtime/methodbinder.cs | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index 4bc1b6baf..c2106ece9 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -316,7 +316,7 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth Runtime.XDecref(valueList); } - var numPyArgs = (int)Runtime.PyTuple_Size(args); + var pynargs = (int)Runtime.PyTuple_Size(args); var isGeneric = false; if (info != null) { @@ -343,14 +343,19 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth int kwargsMatched; int defaultsNeeded; bool isOperator = OperatorMethod.IsOperatorMethod(mi); // e.g. op_Addition is defined for OperableObject - if (!MatchesArgumentCount(numPyArgs, pi, kwargDict, isOperator, out paramsArray, out defaultArgList, out kwargsMatched, out defaultsNeeded)) + if (isOperator ) + { + var a = 0; + a++; + } + if (!MatchesArgumentCount(pynargs, pi, kwargDict, isOperator, out paramsArray, out defaultArgList, out kwargsMatched, out defaultsNeeded)) { continue; } var outs = 0; - int numClrArgs = pi.Length; - isOperator = isOperator && numPyArgs == numClrArgs - 1; // Handle mismatched arg numbers due to Python operator being bound. - var margs = TryConvertArguments(pi, paramsArray, args, numPyArgs, kwargDict, defaultArgList, + int clrnargs = pi.Length; + isOperator = isOperator && pynargs == clrnargs - 1; // Handle mismatched arg numbers due to Python operator being bound. + var margs = TryConvertArguments(pi, paramsArray, args, pynargs, kwargDict, defaultArgList, needsResolution: _methods.Length > 1, // If there's more than one possible match. isOperator: isOperator, outs: out outs); @@ -669,7 +674,7 @@ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool /// /// Check whether the number of Python and .NET arguments match, and compute additional arg information. /// - /// Number of positional args passed from Python. + /// Number of positional args passed from Python. /// Parameters of the specified .NET method. /// Keyword args passed from Python. /// True if the parameters' method is an operator. @@ -678,7 +683,7 @@ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool /// Number of kwargs from Python that are also present in the .NET method. /// Number of non-null defaultsArgs. /// - static bool MatchesArgumentCount(int numPyArgs, ParameterInfo[] parameters, + static bool MatchesArgumentCount(int pynargs, ParameterInfo[] parameters, Dictionary kwargDict, bool isOperator, out bool paramsArray, @@ -688,15 +693,14 @@ static bool MatchesArgumentCount(int numPyArgs, ParameterInfo[] parameters, { defaultArgList = null; var match = false; - int numClrArgs = parameters.Length; - paramsArray = numClrArgs > 0 ? Attribute.IsDefined(parameters[numClrArgs - 1], typeof(ParamArrayAttribute)) : false; + paramsArray = parameters.Length > 0 ? Attribute.IsDefined(parameters[parameters.Length - 1], typeof(ParamArrayAttribute)) : false; kwargsMatched = 0; defaultsNeeded = 0; - if (numPyArgs == numClrArgs && kwargDict.Count == 0) + if (pynargs == parameters.Length && kwargDict.Count == 0) { match = true; } - else if (numPyArgs < numClrArgs && (!paramsArray || numPyArgs == numClrArgs - 1)) + else if (pynargs < parameters.Length && (!paramsArray || pynargs == parameters.Length - 1)) { match = true; // operator methods will have 2 CLR args but only one Python arg, @@ -711,7 +715,7 @@ static bool MatchesArgumentCount(int numPyArgs, ParameterInfo[] parameters, // a corresponding keyword arg or a default param, unless the method // method accepts a params array (which cannot have a default value) defaultArgList = new ArrayList(); - for (var v = numPyArgs; v < numClrArgs; v++) + for (var v = pynargs; v < parameters.Length; v++) { if (kwargDict.ContainsKey(parameters[v].Name)) { @@ -736,8 +740,8 @@ static bool MatchesArgumentCount(int numPyArgs, ParameterInfo[] parameters, } } } - else if (numPyArgs > numClrArgs && numClrArgs > 0 && - Attribute.IsDefined(parameters[numClrArgs - 1], typeof(ParamArrayAttribute))) + else if (pynargs > parameters.Length && parameters.Length > 0 && + Attribute.IsDefined(parameters[parameters.Length - 1], typeof(ParamArrayAttribute))) { // This is a `foo(params object[] bar)` style method match = true; From e11327f6e56a42f71f09283734a1dcb32be283c3 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Tue, 22 Dec 2020 09:02:46 +0000 Subject: [PATCH 11/24] Update AUTHORS and CHANGELOG --- AUTHORS.md | 1 + CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/AUTHORS.md b/AUTHORS.md index 69e7b5c4a..167fd496c 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -24,6 +24,7 @@ - BenoƮt Hudson ([@benoithudson](https://github.com/benoithudson)) - Bradley Friedman ([@leith-bartrich](https://github.com/leith-bartrich)) - Callum Noble ([@callumnoble](https://github.com/callumnoble)) +- Christabella Irwanto([@christabella](https://github.com/christabella)) - Christian Heimes ([@tiran](https://github.com/tiran)) - Christoph Gohlke ([@cgohlke](https://github.com/cgohlke)) - Christopher Bremner ([@chrisjbremner](https://github.com/chrisjbremner)) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1442075ef..87e4009ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ details about the cause of the failure - Made it possible to call `ToString`, `GetHashCode`, and `GetType` on inteface objects - Fixed objects returned by enumerating `PyObject` being disposed too soon - Incorrectly using a non-generic type with type parameters now produces a helpful Python error instead of throwing NullReferenceException +- Python operator method will call C# operator method when left operand is the bound type. ### Removed From 35be4bcfa3e9ba5a2d98981194b37a4e2852a145 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Wed, 23 Dec 2020 08:37:18 +0000 Subject: [PATCH 12/24] Revert rename to pynargs --- src/runtime/methodbinder.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index c2106ece9..30e40fae7 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -674,7 +674,7 @@ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool /// /// Check whether the number of Python and .NET arguments match, and compute additional arg information. /// - /// Number of positional args passed from Python. + /// Number of positional args passed from Python. /// Parameters of the specified .NET method. /// Keyword args passed from Python. /// True if the parameters' method is an operator. @@ -683,7 +683,7 @@ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool /// Number of kwargs from Python that are also present in the .NET method. /// Number of non-null defaultsArgs. /// - static bool MatchesArgumentCount(int pynargs, ParameterInfo[] parameters, + static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] parameters, Dictionary kwargDict, bool isOperator, out bool paramsArray, @@ -696,11 +696,11 @@ static bool MatchesArgumentCount(int pynargs, ParameterInfo[] parameters, paramsArray = parameters.Length > 0 ? Attribute.IsDefined(parameters[parameters.Length - 1], typeof(ParamArrayAttribute)) : false; kwargsMatched = 0; defaultsNeeded = 0; - if (pynargs == parameters.Length && kwargDict.Count == 0) + if (positionalArgumentCount == parameters.Length && kwargDict.Count == 0) { match = true; } - else if (pynargs < parameters.Length && (!paramsArray || pynargs == parameters.Length - 1)) + else if (positionalArgumentCount < parameters.Length && (!paramsArray || positionalArgumentCount == parameters.Length - 1)) { match = true; // operator methods will have 2 CLR args but only one Python arg, @@ -715,7 +715,7 @@ static bool MatchesArgumentCount(int pynargs, ParameterInfo[] parameters, // a corresponding keyword arg or a default param, unless the method // method accepts a params array (which cannot have a default value) defaultArgList = new ArrayList(); - for (var v = pynargs; v < parameters.Length; v++) + for (var v = positionalArgumentCount; v < parameters.Length; v++) { if (kwargDict.ContainsKey(parameters[v].Name)) { @@ -740,7 +740,7 @@ static bool MatchesArgumentCount(int pynargs, ParameterInfo[] parameters, } } } - else if (pynargs > parameters.Length && parameters.Length > 0 && + else if (positionalArgumentCount > parameters.Length && parameters.Length > 0 && Attribute.IsDefined(parameters[parameters.Length - 1], typeof(ParamArrayAttribute))) { // This is a `foo(params object[] bar)` style method From f19c2816218523489edb9da557b89ce1771ef054 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Wed, 23 Dec 2020 09:14:56 +0000 Subject: [PATCH 13/24] Address @tminka's comments. --- src/embed_tests/TestOperator.cs | 7 ------- src/runtime/methodbinder.cs | 5 ----- src/runtime/operatormethod.cs | 12 +++++++++--- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/embed_tests/TestOperator.cs b/src/embed_tests/TestOperator.cs index a309010a2..9e048b101 100644 --- a/src/embed_tests/TestOperator.cs +++ b/src/embed_tests/TestOperator.cs @@ -21,13 +21,6 @@ public void Dispose() PythonEngine.Shutdown(); } - public class SampleClass - { - public int VoidCall() => 10; - - public int Foo(int a, int b = 10) => a + b; - } - public class OperableObject { public int Num { get; set; } diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index 30e40fae7..2797f329c 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -343,11 +343,6 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth int kwargsMatched; int defaultsNeeded; bool isOperator = OperatorMethod.IsOperatorMethod(mi); // e.g. op_Addition is defined for OperableObject - if (isOperator ) - { - var a = 0; - a++; - } if (!MatchesArgumentCount(pynargs, pi, kwargDict, isOperator, out paramsArray, out defaultArgList, out kwargsMatched, out defaultsNeeded)) { continue; diff --git a/src/runtime/operatormethod.cs b/src/runtime/operatormethod.cs index e77b397e8..407a3a45b 100644 --- a/src/runtime/operatormethod.cs +++ b/src/runtime/operatormethod.cs @@ -9,9 +9,11 @@ namespace Python.Runtime { internal static class OperatorMethod { - // Maps the compiled method name in .NET CIL (e.g. op_Addition) to - // the equivalent Python operator (e.g. __add__) as well as the offset - // that identifies that operator's slot (e.g. nb_add) in heap space. + /// + /// Maps the compiled method name in .NET CIL (e.g. op_Addition) to + /// the equivalent Python operator (e.g. __add__) as well as the offset + /// that identifies that operator's slot (e.g. nb_add) in heap space. + /// public static Dictionary OpMethodMap { get; private set; } public readonly struct SlotDefinition { @@ -27,6 +29,10 @@ public SlotDefinition(string methodName, int typeOffset) static OperatorMethod() { + // .NET operator method names are documented at: + // https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/operator-overloads + // Python operator methods and slots are documented at: + // https://docs.python.org/3/c-api/typeobj.html // TODO: Rich compare, inplace operator support OpMethodMap = new Dictionary { From d7f52d2fd8cdd7038e5f0efd0901a2722b031754 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Wed, 23 Dec 2020 16:04:31 +0000 Subject: [PATCH 14/24] Remove whitespace --- src/embed_tests/TestOperator.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/embed_tests/TestOperator.cs b/src/embed_tests/TestOperator.cs index 9e048b101..9e4605db3 100644 --- a/src/embed_tests/TestOperator.cs +++ b/src/embed_tests/TestOperator.cs @@ -56,7 +56,6 @@ public OperableObject(int num) return new OperableObject(a.Num - b); } - public static OperableObject operator *(int a, OperableObject b) { return new OperableObject(a * b.Num); @@ -83,7 +82,6 @@ public OperableObject(int num) return new OperableObject(a.Num / b); } - public static OperableObject operator &(int a, OperableObject b) { return new OperableObject(a & b.Num); @@ -97,7 +95,6 @@ public OperableObject(int num) return new OperableObject(a.Num & b); } - public static OperableObject operator |(int a, OperableObject b) { return new OperableObject(a | b.Num); @@ -111,7 +108,6 @@ public OperableObject(int num) return new OperableObject(a.Num | b); } - public static OperableObject operator ^(int a, OperableObject b) { return new OperableObject(a ^ b.Num); From 5855a1ba9e826a581ab26c2cc9c26007a0c15b3c Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Thu, 24 Dec 2020 11:46:05 +0000 Subject: [PATCH 15/24] Fix nits --- src/runtime/classmanager.cs | 2 +- src/runtime/methodbinder.cs | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/runtime/classmanager.cs b/src/runtime/classmanager.cs index c022553a1..b52a30d23 100644 --- a/src/runtime/classmanager.cs +++ b/src/runtime/classmanager.cs @@ -470,7 +470,7 @@ private static ClassInfo GetClassInfo(Type type) ob = new MethodObject(type, name, mlist); ci.members[name] = ob; - if (OperatorMethod.IsOperatorMethod(mlist[0])) + if (mlist.Any(OperatorMethod.IsOperatorMethod)) { ci.members[OperatorMethod.GetPyMethodName(name)] = ob; } diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index 2797f329c..6787e13d7 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -342,7 +342,7 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth bool paramsArray; int kwargsMatched; int defaultsNeeded; - bool isOperator = OperatorMethod.IsOperatorMethod(mi); // e.g. op_Addition is defined for OperableObject + bool isOperator = OperatorMethod.IsOperatorMethod(mi); if (!MatchesArgumentCount(pynargs, pi, kwargDict, isOperator, out paramsArray, out defaultArgList, out kwargsMatched, out defaultsNeeded)) { continue; @@ -362,12 +362,11 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth { if (inst != IntPtr.Zero) { - var co = ManagedType.GetManagedObject(inst) as CLRObject; - if (co == null) + if (ManagedType.GetManagedObject(inst) is CLRObject co) { - break; + margs[0] = co.inst; } - margs[0] = co.inst; + else { break; } } } From e7da0bcc451856db03e901d29c64ffb2246cbbe9 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Mon, 28 Dec 2020 11:37:45 +0000 Subject: [PATCH 16/24] Support reverse binary operations --- src/embed_tests/TestOperator.cs | 36 +++++++++++++++++++++++++++++++++ src/runtime/classmanager.cs | 9 ++++++++- src/runtime/methodbinder.cs | 32 +++++++++++++++++++---------- src/runtime/operatormethod.cs | 36 +++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 12 deletions(-) diff --git a/src/embed_tests/TestOperator.cs b/src/embed_tests/TestOperator.cs index 9e4605db3..10d05dd2f 100644 --- a/src/embed_tests/TestOperator.cs +++ b/src/embed_tests/TestOperator.cs @@ -202,6 +202,42 @@ public void ForwardOperatorOverloads() assert c.Num == a.Num ^ b "); } + + + [Test] + public void ReverseOperatorOverloads() + { + string name = string.Format("{0}.{1}", + typeof(OperableObject).DeclaringType.Name, + typeof(OperableObject).Name); + string module = MethodBase.GetCurrentMethod().DeclaringType.Namespace; + + PythonEngine.Exec($@" +from {module} import * +cls = {name} +a = 2 +b = cls(10) + +c = a + b +assert c.Num == a + b.Num + +c = a - b +assert c.Num == a - b.Num + +c = a * b +assert c.Num == a * b.Num + +c = a & b +assert c.Num == a & b.Num + +c = a | b +assert c.Num == a | b.Num + +c = a ^ b +assert c.Num == a ^ b.Num +"); + + } [Test] public void ShiftOperatorOverloads() { diff --git a/src/runtime/classmanager.cs b/src/runtime/classmanager.cs index b52a30d23..b21716bd7 100644 --- a/src/runtime/classmanager.cs +++ b/src/runtime/classmanager.cs @@ -472,7 +472,14 @@ private static ClassInfo GetClassInfo(Type type) ci.members[name] = ob; if (mlist.Any(OperatorMethod.IsOperatorMethod)) { - ci.members[OperatorMethod.GetPyMethodName(name)] = ob; + string pyName = OperatorMethod.GetPyMethodName(name); + string pyNameReverse = pyName.Insert(2, "r"); + MethodInfo[] forwardMethods, reverseMethods; + OperatorMethod.FilterMethods(mlist, out forwardMethods, out reverseMethods); + // Only methods where the left operand is the declaring type. + ci.members[pyName] = new MethodObject(type, name, forwardMethods); + // Only methods where only the right operand is the declaring type. + ci.members[pyNameReverse] = new MethodObject(type, name, reverseMethods); } } diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index 6787e13d7..602695f9f 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -350,9 +350,20 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth var outs = 0; int clrnargs = pi.Length; isOperator = isOperator && pynargs == clrnargs - 1; // Handle mismatched arg numbers due to Python operator being bound. + // Preprocessing pi to remove either the first or second argument. + bool isForward = isOperator && OperatorMethod.IsForward((MethodInfo)mi); // Only cast if isOperator. + if (isOperator && isForward) { + // The first Python arg is the right operand, while the bound instance is the left. + // We need to skip the first (left operand) CLR argument. + pi = pi.Skip(1).Take(1).ToArray(); + } + else if (isOperator && !isForward) { + // The first Python arg is the left operand. + // We need to take the first CLR argument. + pi = pi.Take(1).ToArray(); + } var margs = TryConvertArguments(pi, paramsArray, args, pynargs, kwargDict, defaultArgList, needsResolution: _methods.Length > 1, // If there's more than one possible match. - isOperator: isOperator, outs: out outs); if (margs == null) { @@ -364,7 +375,15 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth { if (ManagedType.GetManagedObject(inst) is CLRObject co) { - margs[0] = co.inst; + // Postprocessing to extend margs. + var margsTemp = new object[2]; + // If forward, the bound instance is the left operand. + int boundOperandIndex = isForward ? 0 : 1; + // If forward, the passed instance is the right operand. + int passedOperandIndex = isForward ? 1 : 0; + margsTemp[boundOperandIndex] = co.inst; + margsTemp[passedOperandIndex] = margs[0]; + margs = margsTemp; } else { break; } } @@ -488,7 +507,6 @@ static IntPtr HandleParamsArray(IntPtr args, int arrayStart, int pyArgCount, out /// Dictionary of keyword argument name to python object pointer /// A list of default values for omitted parameters /// true, if overloading resolution is required - /// true, if is operator method /// Returns number of output parameters /// An array of .NET arguments, that can be passed to a method. static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray, @@ -496,7 +514,6 @@ static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray, Dictionary kwargDict, ArrayList defaultArgList, bool needsResolution, - bool isOperator, out int outs) { outs = 0; @@ -535,13 +552,6 @@ static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray, op = Runtime.PyTuple_GetItem(args, paramIndex); } } - if (isOperator && paramIndex == 0) - { - // After we've obtained the first argument from Python, we need to skip the first argument of the CLR - // because operator method is a bound method in Python - paramIndex++; // Leave the first .NET param as null (margs). - parameter = pi[paramIndex]; - } bool isOut; if (!TryConvertArgument(op, parameter.ParameterType, needsResolution, out margs[paramIndex], out isOut)) diff --git a/src/runtime/operatormethod.cs b/src/runtime/operatormethod.cs index 407a3a45b..b0498c8f2 100644 --- a/src/runtime/operatormethod.cs +++ b/src/runtime/operatormethod.cs @@ -130,5 +130,41 @@ private static PyObject GetOperatorType() return locals.GetItem("OperatorMethod"); } } + + public static string ReversePyMethodName(string pyName) + { + return pyName.Insert(2, "r"); + } + + /// + /// Check if the method is performing a forward or reverse operation. + /// + /// The operator method. + /// + public static bool IsForward(MethodInfo method) + { + Type declaringType = method.DeclaringType; + Type leftOperandType = method.GetParameters()[0].ParameterType; + return leftOperandType == declaringType; + } + + public static void FilterMethods(MethodInfo[] methods, out MethodInfo[] forwardMethods, out MethodInfo[] reverseMethods) + { + List forwardMethodsList = new List(); + List reverseMethodsList = new List(); + foreach (var method in methods) + { + if (IsForward(method)) + { + forwardMethodsList.Add(method); + } else + { + reverseMethodsList.Add(method); + } + + } + forwardMethods = forwardMethodsList.ToArray(); + reverseMethods = reverseMethodsList.ToArray(); + } } } From a376838947915c47b77cbd32b7e0e797a5f69f1d Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Tue, 29 Dec 2020 13:17:27 +0000 Subject: [PATCH 17/24] Use reverse instead of forward (semantics) --- src/runtime/methodbinder.cs | 14 +++++++------- src/runtime/operatormethod.cs | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index 602695f9f..185c9df4d 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -351,13 +351,13 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth int clrnargs = pi.Length; isOperator = isOperator && pynargs == clrnargs - 1; // Handle mismatched arg numbers due to Python operator being bound. // Preprocessing pi to remove either the first or second argument. - bool isForward = isOperator && OperatorMethod.IsForward((MethodInfo)mi); // Only cast if isOperator. - if (isOperator && isForward) { + bool isReverse = isOperator && OperatorMethod.IsReverse((MethodInfo)mi); // Only cast if isOperator. + if (isOperator && !isReverse) { // The first Python arg is the right operand, while the bound instance is the left. // We need to skip the first (left operand) CLR argument. pi = pi.Skip(1).Take(1).ToArray(); } - else if (isOperator && !isForward) { + else if (isOperator && isReverse) { // The first Python arg is the left operand. // We need to take the first CLR argument. pi = pi.Take(1).ToArray(); @@ -377,10 +377,10 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth { // Postprocessing to extend margs. var margsTemp = new object[2]; - // If forward, the bound instance is the left operand. - int boundOperandIndex = isForward ? 0 : 1; - // If forward, the passed instance is the right operand. - int passedOperandIndex = isForward ? 1 : 0; + // If reverse, the passed instance is the left operand. + int passedOperandIndex= isReverse ? 0 : 1; + // If reverse, the bound instance is the right operand. + int boundOperandIndex = isReverse ? 1 : 0; margsTemp[boundOperandIndex] = co.inst; margsTemp[passedOperandIndex] = margs[0]; margs = margsTemp; diff --git a/src/runtime/operatormethod.cs b/src/runtime/operatormethod.cs index b0498c8f2..529548e7d 100644 --- a/src/runtime/operatormethod.cs +++ b/src/runtime/operatormethod.cs @@ -137,15 +137,15 @@ public static string ReversePyMethodName(string pyName) } /// - /// Check if the method is performing a forward or reverse operation. + /// Check if the method is performing a reverse operation. /// /// The operator method. /// - public static bool IsForward(MethodInfo method) + public static bool IsReverse(MethodInfo method) { Type declaringType = method.DeclaringType; Type leftOperandType = method.GetParameters()[0].ParameterType; - return leftOperandType == declaringType; + return leftOperandType != declaringType; } public static void FilterMethods(MethodInfo[] methods, out MethodInfo[] forwardMethods, out MethodInfo[] reverseMethods) @@ -154,12 +154,12 @@ public static void FilterMethods(MethodInfo[] methods, out MethodInfo[] forwardM List reverseMethodsList = new List(); foreach (var method in methods) { - if (IsForward(method)) + if (IsReverse(method)) { - forwardMethodsList.Add(method); + reverseMethodsList.Add(method); } else { - reverseMethodsList.Add(method); + forwardMethodsList.Add(method); } } From 6923a780c1a02b980c8c92c98f9163cd82d5d138 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Wed, 30 Dec 2020 14:43:30 +0000 Subject: [PATCH 18/24] Address @tminka's comments --- src/embed_tests/TestOperator.cs | 19 +++++++++++++++++++ src/runtime/classmanager.cs | 2 +- src/runtime/methodbinder.cs | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/embed_tests/TestOperator.cs b/src/embed_tests/TestOperator.cs index 10d05dd2f..4e87235db 100644 --- a/src/embed_tests/TestOperator.cs +++ b/src/embed_tests/TestOperator.cs @@ -167,6 +167,25 @@ public void OperatorOverloads() assert c.Num == a.Num ^ b.Num "); } + + [Test] + public void OperatorOverloadMissingArgument() + { + string name = string.Format("{0}.{1}", + typeof(OperableObject).DeclaringType.Name, + typeof(OperableObject).Name); + string module = MethodBase.GetCurrentMethod().DeclaringType.Namespace; + + Assert.Throws(() => + PythonEngine.Exec($@" +from {module} import * +cls = {name} +a = cls(2) +b = cls(10) +a.op_Addition() +")); + } + [Test] public void ForwardOperatorOverloads() { diff --git a/src/runtime/classmanager.cs b/src/runtime/classmanager.cs index b21716bd7..760c794a8 100644 --- a/src/runtime/classmanager.cs +++ b/src/runtime/classmanager.cs @@ -473,7 +473,7 @@ private static ClassInfo GetClassInfo(Type type) if (mlist.Any(OperatorMethod.IsOperatorMethod)) { string pyName = OperatorMethod.GetPyMethodName(name); - string pyNameReverse = pyName.Insert(2, "r"); + string pyNameReverse = OperatorMethod.ReversePyMethodName(pyName); MethodInfo[] forwardMethods, reverseMethods; OperatorMethod.FilterMethods(mlist, out forwardMethods, out reverseMethods); // Only methods where the left operand is the declaring type. diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index 185c9df4d..8f40c7a0e 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -709,7 +709,7 @@ static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] pa match = true; // operator methods will have 2 CLR args but only one Python arg, // since Python operator methods are bound - if (isOperator) + if (isOperator && positionalArgumentCount == parameters.Length - 1) { // return early since a C# operator method cannot have // keyword args, default args, or params arrays (exclusive cases) From 4c992d821f587dafa88a7847d9e299cb8a39d378 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Wed, 30 Dec 2020 15:24:58 +0000 Subject: [PATCH 19/24] Support unary neg and pos operators --- src/embed_tests/TestOperator.cs | 19 ++++++++++++++++++- src/runtime/methodbinder.cs | 14 +++++++++----- src/runtime/native/ITypeOffsets.cs | 2 ++ src/runtime/native/TypeOffset.cs | 2 ++ src/runtime/operatormethod.cs | 4 +++- 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/embed_tests/TestOperator.cs b/src/embed_tests/TestOperator.cs index 4e87235db..f5da83616 100644 --- a/src/embed_tests/TestOperator.cs +++ b/src/embed_tests/TestOperator.cs @@ -30,6 +30,16 @@ public OperableObject(int num) Num = num; } + public static OperableObject operator +(OperableObject a) + { + return new OperableObject(+a.Num); + } + + public static OperableObject operator -(OperableObject a) + { + return new OperableObject(-a.Num); + } + public static OperableObject operator +(int a, OperableObject b) { return new OperableObject(a + b.Num); @@ -143,8 +153,15 @@ public void OperatorOverloads() PythonEngine.Exec($@" from {module} import * cls = {name} -a = cls(2) +a = cls(-2) b = cls(10) +c = +a +assert c.Num == +a.Num + +a = cls(2) +c = -a +assert c.Num == -a.Num + c = a + b assert c.Num == a.Num + b.Num diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index 8f40c7a0e..0daf524ae 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -355,7 +355,7 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth if (isOperator && !isReverse) { // The first Python arg is the right operand, while the bound instance is the left. // We need to skip the first (left operand) CLR argument. - pi = pi.Skip(1).Take(1).ToArray(); + pi = pi.Skip(1).ToArray(); } else if (isOperator && isReverse) { // The first Python arg is the left operand. @@ -375,14 +375,18 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth { if (ManagedType.GetManagedObject(inst) is CLRObject co) { + bool isUnary = pynargs == 0; // Postprocessing to extend margs. - var margsTemp = new object[2]; - // If reverse, the passed instance is the left operand. - int passedOperandIndex= isReverse ? 0 : 1; + var margsTemp = isUnary ? new object[1] : new object[2]; // If reverse, the bound instance is the right operand. int boundOperandIndex = isReverse ? 1 : 0; + // If reverse, the passed instance is the left operand. + int passedOperandIndex = isReverse ? 0 : 1; margsTemp[boundOperandIndex] = co.inst; - margsTemp[passedOperandIndex] = margs[0]; + if (!isUnary) + { + margsTemp[passedOperandIndex] = margs[0]; + } margs = margsTemp; } else { break; } diff --git a/src/runtime/native/ITypeOffsets.cs b/src/runtime/native/ITypeOffsets.cs index 4d18a7c7d..485c041f8 100644 --- a/src/runtime/native/ITypeOffsets.cs +++ b/src/runtime/native/ITypeOffsets.cs @@ -14,6 +14,8 @@ interface ITypeOffsets int mp_length { get; } int mp_subscript { get; } int name { get; } + int nb_positive { get; } + int nb_negative { get; } int nb_add { get; } int nb_subtract { get; } int nb_multiply { get; } diff --git a/src/runtime/native/TypeOffset.cs b/src/runtime/native/TypeOffset.cs index 3ea23e3fb..4c1bcefa0 100644 --- a/src/runtime/native/TypeOffset.cs +++ b/src/runtime/native/TypeOffset.cs @@ -21,6 +21,8 @@ static partial class TypeOffset internal static int mp_length { get; private set; } internal static int mp_subscript { get; private set; } internal static int name { get; private set; } + internal static int nb_positive { get; private set; } + internal static int nb_negative { get; private set; } internal static int nb_add { get; private set; } internal static int nb_subtract { get; private set; } internal static int nb_multiply { get; private set; } diff --git a/src/runtime/operatormethod.cs b/src/runtime/operatormethod.cs index 529548e7d..d161ec39c 100644 --- a/src/runtime/operatormethod.cs +++ b/src/runtime/operatormethod.cs @@ -46,7 +46,9 @@ static OperatorMethod() ["op_LeftShift"] = new SlotDefinition("__lshift__", TypeOffset.nb_lshift), ["op_RightShift"] = new SlotDefinition("__rshift__", TypeOffset.nb_rshift), ["op_Modulus"] = new SlotDefinition("__mod__", TypeOffset.nb_remainder), - ["op_OneComplement"] = new SlotDefinition("__invert__", TypeOffset.nb_invert) + ["op_OneComplement"] = new SlotDefinition("__invert__", TypeOffset.nb_invert), + ["op_UnaryNegation"] = new SlotDefinition("__neg__", TypeOffset.nb_negative), + ["op_UnaryPlus"] = new SlotDefinition("__pos__", TypeOffset.nb_positive), }; } From 8cce61dd5def72485e599a071e9fd3bfc12346c6 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Mon, 4 Jan 2021 10:40:54 +0200 Subject: [PATCH 20/24] Remove isOperator from MatchesArgumentCount (simplify), add test. --- src/embed_tests/TestOperator.cs | 3 +++ src/runtime/methodbinder.cs | 18 +++++++----------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/embed_tests/TestOperator.cs b/src/embed_tests/TestOperator.cs index f5da83616..d732dfbbe 100644 --- a/src/embed_tests/TestOperator.cs +++ b/src/embed_tests/TestOperator.cs @@ -263,6 +263,9 @@ public void ReverseOperatorOverloads() c = a * b assert c.Num == a * b.Num +c = a / b +assert c.Num == a // b.Num + c = a & b assert c.Num == a & b.Num diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index 0daf524ae..e5653e372 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -343,13 +343,14 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth int kwargsMatched; int defaultsNeeded; bool isOperator = OperatorMethod.IsOperatorMethod(mi); - if (!MatchesArgumentCount(pynargs, pi, kwargDict, isOperator, out paramsArray, out defaultArgList, out kwargsMatched, out defaultsNeeded)) + int clrnargs = pi.Length; + // Binary operator methods will have 2 CLR args but only one Python arg + // (unary operators will have 1 less each), since Python operator methods are bound. + isOperator = isOperator && pynargs == clrnargs - 1; + if (!MatchesArgumentCount(pynargs, pi, kwargDict, out paramsArray, out defaultArgList, out kwargsMatched, out defaultsNeeded) && !isOperator) { continue; } - var outs = 0; - int clrnargs = pi.Length; - isOperator = isOperator && pynargs == clrnargs - 1; // Handle mismatched arg numbers due to Python operator being bound. // Preprocessing pi to remove either the first or second argument. bool isReverse = isOperator && OperatorMethod.IsReverse((MethodInfo)mi); // Only cast if isOperator. if (isOperator && !isReverse) { @@ -362,6 +363,7 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth // We need to take the first CLR argument. pi = pi.Take(1).ToArray(); } + var outs = 0; var margs = TryConvertArguments(pi, paramsArray, args, pynargs, kwargDict, defaultArgList, needsResolution: _methods.Length > 1, // If there's more than one possible match. outs: out outs); @@ -685,7 +687,6 @@ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool /// Number of positional args passed from Python. /// Parameters of the specified .NET method. /// Keyword args passed from Python. - /// True if the parameters' method is an operator. /// True if the final param of the .NET method is an array (`params` keyword). /// List of default values for arguments. /// Number of kwargs from Python that are also present in the .NET method. @@ -693,7 +694,6 @@ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool /// static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] parameters, Dictionary kwargDict, - bool isOperator, out bool paramsArray, out ArrayList defaultArgList, out int kwargsMatched, @@ -711,12 +711,8 @@ static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] pa else if (positionalArgumentCount < parameters.Length && (!paramsArray || positionalArgumentCount == parameters.Length - 1)) { match = true; - // operator methods will have 2 CLR args but only one Python arg, - // since Python operator methods are bound - if (isOperator && positionalArgumentCount == parameters.Length - 1) + if (positionalArgumentCount == parameters.Length - 1) { - // return early since a C# operator method cannot have - // keyword args, default args, or params arrays (exclusive cases) return match; } // every parameter past 'positionalArgumentCount' must have either From 09a2047f502fd6869744aae2234bbe474f9aaa94 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Mon, 4 Jan 2021 11:56:09 +0200 Subject: [PATCH 21/24] Revert "Remove isOperator from MatchesArgumentCount (simplify)" This partially reverts commit 8cce61dd5def72485e599a071e9fd3bfc12346c6. Due to breaking change --- src/runtime/classmanager.cs | 6 ++++-- src/runtime/methodbinder.cs | 18 +++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/runtime/classmanager.cs b/src/runtime/classmanager.cs index 760c794a8..db4146722 100644 --- a/src/runtime/classmanager.cs +++ b/src/runtime/classmanager.cs @@ -477,9 +477,11 @@ private static ClassInfo GetClassInfo(Type type) MethodInfo[] forwardMethods, reverseMethods; OperatorMethod.FilterMethods(mlist, out forwardMethods, out reverseMethods); // Only methods where the left operand is the declaring type. - ci.members[pyName] = new MethodObject(type, name, forwardMethods); + if (forwardMethods.Length > 0) + ci.members[pyName] = new MethodObject(type, name, forwardMethods); // Only methods where only the right operand is the declaring type. - ci.members[pyNameReverse] = new MethodObject(type, name, reverseMethods); + if (reverseMethods.Length > 0) + ci.members[pyNameReverse] = new MethodObject(type, name, reverseMethods); } } diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index e5653e372..0daf524ae 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -343,14 +343,13 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth int kwargsMatched; int defaultsNeeded; bool isOperator = OperatorMethod.IsOperatorMethod(mi); - int clrnargs = pi.Length; - // Binary operator methods will have 2 CLR args but only one Python arg - // (unary operators will have 1 less each), since Python operator methods are bound. - isOperator = isOperator && pynargs == clrnargs - 1; - if (!MatchesArgumentCount(pynargs, pi, kwargDict, out paramsArray, out defaultArgList, out kwargsMatched, out defaultsNeeded) && !isOperator) + if (!MatchesArgumentCount(pynargs, pi, kwargDict, isOperator, out paramsArray, out defaultArgList, out kwargsMatched, out defaultsNeeded)) { continue; } + var outs = 0; + int clrnargs = pi.Length; + isOperator = isOperator && pynargs == clrnargs - 1; // Handle mismatched arg numbers due to Python operator being bound. // Preprocessing pi to remove either the first or second argument. bool isReverse = isOperator && OperatorMethod.IsReverse((MethodInfo)mi); // Only cast if isOperator. if (isOperator && !isReverse) { @@ -363,7 +362,6 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth // We need to take the first CLR argument. pi = pi.Take(1).ToArray(); } - var outs = 0; var margs = TryConvertArguments(pi, paramsArray, args, pynargs, kwargDict, defaultArgList, needsResolution: _methods.Length > 1, // If there's more than one possible match. outs: out outs); @@ -687,6 +685,7 @@ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool /// Number of positional args passed from Python. /// Parameters of the specified .NET method. /// Keyword args passed from Python. + /// True if the parameters' method is an operator. /// True if the final param of the .NET method is an array (`params` keyword). /// List of default values for arguments. /// Number of kwargs from Python that are also present in the .NET method. @@ -694,6 +693,7 @@ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool /// static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] parameters, Dictionary kwargDict, + bool isOperator, out bool paramsArray, out ArrayList defaultArgList, out int kwargsMatched, @@ -711,8 +711,12 @@ static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] pa else if (positionalArgumentCount < parameters.Length && (!paramsArray || positionalArgumentCount == parameters.Length - 1)) { match = true; - if (positionalArgumentCount == parameters.Length - 1) + // operator methods will have 2 CLR args but only one Python arg, + // since Python operator methods are bound + if (isOperator && positionalArgumentCount == parameters.Length - 1) { + // return early since a C# operator method cannot have + // keyword args, default args, or params arrays (exclusive cases) return match; } // every parameter past 'positionalArgumentCount' must have either From 41bd07fe3ad2ae53162f81f961f1ec8b402753d1 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Mon, 4 Jan 2021 16:31:23 +0200 Subject: [PATCH 22/24] Properly remove isOperator from MatchesArgumentCount (@tminka comment) --- src/runtime/methodbinder.cs | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index 0daf524ae..47883f0e6 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -343,13 +343,14 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth int kwargsMatched; int defaultsNeeded; bool isOperator = OperatorMethod.IsOperatorMethod(mi); - if (!MatchesArgumentCount(pynargs, pi, kwargDict, isOperator, out paramsArray, out defaultArgList, out kwargsMatched, out defaultsNeeded)) + int clrnargs = pi.Length; + // Binary operator methods will have 2 CLR args but only one Python arg + // (unary operators will have 1 less each), since Python operator methods are bound. + isOperator = isOperator && pynargs == clrnargs - 1; + if (!MatchesArgumentCount(pynargs, pi, kwargDict, out paramsArray, out defaultArgList, out kwargsMatched, out defaultsNeeded) && !isOperator) { continue; } - var outs = 0; - int clrnargs = pi.Length; - isOperator = isOperator && pynargs == clrnargs - 1; // Handle mismatched arg numbers due to Python operator being bound. // Preprocessing pi to remove either the first or second argument. bool isReverse = isOperator && OperatorMethod.IsReverse((MethodInfo)mi); // Only cast if isOperator. if (isOperator && !isReverse) { @@ -362,6 +363,7 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth // We need to take the first CLR argument. pi = pi.Take(1).ToArray(); } + var outs = 0; var margs = TryConvertArguments(pi, paramsArray, args, pynargs, kwargDict, defaultArgList, needsResolution: _methods.Length > 1, // If there's more than one possible match. outs: out outs); @@ -685,7 +687,6 @@ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool /// Number of positional args passed from Python. /// Parameters of the specified .NET method. /// Keyword args passed from Python. - /// True if the parameters' method is an operator. /// True if the final param of the .NET method is an array (`params` keyword). /// List of default values for arguments. /// Number of kwargs from Python that are also present in the .NET method. @@ -693,7 +694,6 @@ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool /// static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] parameters, Dictionary kwargDict, - bool isOperator, out bool paramsArray, out ArrayList defaultArgList, out int kwargsMatched, @@ -711,14 +711,6 @@ static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] pa else if (positionalArgumentCount < parameters.Length && (!paramsArray || positionalArgumentCount == parameters.Length - 1)) { match = true; - // operator methods will have 2 CLR args but only one Python arg, - // since Python operator methods are bound - if (isOperator && positionalArgumentCount == parameters.Length - 1) - { - // return early since a C# operator method cannot have - // keyword args, default args, or params arrays (exclusive cases) - return match; - } // every parameter past 'positionalArgumentCount' must have either // a corresponding keyword arg or a default param, unless the method // method accepts a params array (which cannot have a default value) From 10ccf1e121346f5cbc010b2fa0e4fb1b9fd489b5 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Mon, 4 Jan 2021 17:07:11 +0200 Subject: [PATCH 23/24] Update changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87e4009ad..920703054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,7 @@ details about the cause of the failure - Made it possible to call `ToString`, `GetHashCode`, and `GetType` on inteface objects - Fixed objects returned by enumerating `PyObject` being disposed too soon - Incorrectly using a non-generic type with type parameters now produces a helpful Python error instead of throwing NullReferenceException -- Python operator method will call C# operator method when left operand is the bound type. +- Python operator method will call C# operator method for supported binary and unary operators ([#1324][p1324]). ### Removed From 5682e0ce635dcb5e1f4f5d23c7c280ceb76cbce2 Mon Sep 17 00:00:00 2001 From: Christabella Irwanto Date: Mon, 4 Jan 2021 20:23:55 +0200 Subject: [PATCH 24/24] Add ones complement and modulo tests (@tminka comment) --- src/embed_tests/TestOperator.cs | 30 ++++++++++++++++++++++++++++++ src/runtime/operatormethod.cs | 4 ++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/embed_tests/TestOperator.cs b/src/embed_tests/TestOperator.cs index d732dfbbe..ecdb0c1dc 100644 --- a/src/embed_tests/TestOperator.cs +++ b/src/embed_tests/TestOperator.cs @@ -30,6 +30,11 @@ public OperableObject(int num) Num = num; } + public static OperableObject operator ~(OperableObject a) + { + return new OperableObject(~a.Num); + } + public static OperableObject operator +(OperableObject a) { return new OperableObject(+a.Num); @@ -92,6 +97,19 @@ public OperableObject(int num) return new OperableObject(a.Num / b); } + public static OperableObject operator %(int a, OperableObject b) + { + return new OperableObject(a % b.Num); + } + public static OperableObject operator %(OperableObject a, OperableObject b) + { + return new OperableObject(a.Num % b.Num); + } + public static OperableObject operator %(OperableObject a, int b) + { + return new OperableObject(a.Num % b); + } + public static OperableObject operator &(int a, OperableObject b) { return new OperableObject(a & b.Num); @@ -155,6 +173,9 @@ public void OperatorOverloads() cls = {name} a = cls(-2) b = cls(10) +c = ~a +assert c.Num == ~a.Num + c = +a assert c.Num == +a.Num @@ -174,6 +195,9 @@ public void OperatorOverloads() c = a / b assert c.Num == a.Num // b.Num +c = a % b +assert c.Num == a.Num % b.Num + c = a & b assert c.Num == a.Num & b.Num @@ -228,6 +252,9 @@ public void ForwardOperatorOverloads() c = a / b assert c.Num == a.Num // b +c = a % b +assert c.Num == a.Num % b + c = a & b assert c.Num == a.Num & b @@ -266,6 +293,9 @@ public void ReverseOperatorOverloads() c = a / b assert c.Num == a // b.Num +c = a % b +assert c.Num == a % b.Num + c = a & b assert c.Num == a & b.Num diff --git a/src/runtime/operatormethod.cs b/src/runtime/operatormethod.cs index d161ec39c..1e0244510 100644 --- a/src/runtime/operatormethod.cs +++ b/src/runtime/operatormethod.cs @@ -40,13 +40,13 @@ static OperatorMethod() ["op_Subtraction"] = new SlotDefinition("__sub__", TypeOffset.nb_subtract), ["op_Multiply"] = new SlotDefinition("__mul__", TypeOffset.nb_multiply), ["op_Division"] = new SlotDefinition("__truediv__", TypeOffset.nb_true_divide), + ["op_Modulus"] = new SlotDefinition("__mod__", TypeOffset.nb_remainder), ["op_BitwiseAnd"] = new SlotDefinition("__and__", TypeOffset.nb_and), ["op_BitwiseOr"] = new SlotDefinition("__or__", TypeOffset.nb_or), ["op_ExclusiveOr"] = new SlotDefinition("__xor__", TypeOffset.nb_xor), ["op_LeftShift"] = new SlotDefinition("__lshift__", TypeOffset.nb_lshift), ["op_RightShift"] = new SlotDefinition("__rshift__", TypeOffset.nb_rshift), - ["op_Modulus"] = new SlotDefinition("__mod__", TypeOffset.nb_remainder), - ["op_OneComplement"] = new SlotDefinition("__invert__", TypeOffset.nb_invert), + ["op_OnesComplement"] = new SlotDefinition("__invert__", TypeOffset.nb_invert), ["op_UnaryNegation"] = new SlotDefinition("__neg__", TypeOffset.nb_negative), ["op_UnaryPlus"] = new SlotDefinition("__pos__", TypeOffset.nb_positive), };