From 7d365055aa0005a64e1cecf5c864d9a9e8c248a8 Mon Sep 17 00:00:00 2001 From: Aaron Jomy Date: Fri, 28 Aug 2026 18:07:11 +0200 Subject: [PATCH] [cpyrt] Detect enum arguments by type in overload resolution GetPriority() deprioritizes enum arguments so that a competing integer overload wins, since C++ has no implicit int->enum conversion, but it detected them with IsEnumScope(GetScope(name)). GetScope does not resolve an enum name to a scope, so the penalty never applied. Query the argument type with IsEnumType as well, per the existing FIXME. The priority is argument-agnostic, so on its own it also sends an enum instance to the integer overload. Give by-value enum parameters a converter that accepts an instance of the same enum as an exact match in the first resolution round, and have the integer converters defer enum instances to the implicit round, as they already do for bools. --- src/cpyrt/CPPMethod.cxx | 4 ++- src/cpyrt/Converters.cxx | 66 ++++++++++++++++++++++++++++++++++- src/cpyrt/DeclareConverters.h | 21 +++++++++++ test/test_regression.py | 28 +++++++++++++++ 4 files changed, 117 insertions(+), 2 deletions(-) diff --git a/src/cpyrt/CPPMethod.cxx b/src/cpyrt/CPPMethod.cxx index e8cccc57..33364f1e 100644 --- a/src/cpyrt/CPPMethod.cxx +++ b/src/cpyrt/CPPMethod.cxx @@ -581,7 +581,9 @@ int cpyrt::CPPMethod::GetPriority() { if (scope) priority += static_cast(interop::GetNumBasesLongestBranch(scope)); - if (interop::IsEnumScope(scope)) + // GetScope does not resolve enum names, so also detect enums by type + if (interop::IsEnumScope(scope) || + interop::IsEnumType(interop::GetMethodArgType(fMethod, iarg))) priority -= 100; // a couple of special cases as explained above diff --git a/src/cpyrt/Converters.cxx b/src/cpyrt/Converters.cxx index da07033f..4c4510fc 100644 --- a/src/cpyrt/Converters.cxx +++ b/src/cpyrt/Converters.cxx @@ -344,9 +344,20 @@ GetCppInstance(PyObject* pyobject, } //- custom helpers to check ranges ------------------------------------------- +static inline bool IsCPPEnumInstance(PyObject* pyobject) { + // cppjit enums are int subclasses created with __underlying on the class + if (!PyLong_Check(pyobject) || PyLong_CheckExact(pyobject) || + PyBool_Check(pyobject)) + return false; + return PyObject_HasAttr((PyObject*)Py_TYPE(pyobject), + cppjit::cpyrt::PyStrings::gUnderlying); +} + +// bools and enum instances match integer parameters in the implicit round only static inline bool ImplicitBool(PyObject* pyobject, cpyrt::CallContext* ctxt) { using namespace cppjit::cpyrt; - if (!AllowImplicit(ctxt) && PyBool_Check(pyobject)) { + if (!AllowImplicit(ctxt) && + (PyBool_Check(pyobject) || IsCPPEnumInstance(pyobject))) { if (!NoImplicit(ctxt)) ctxt->fFlags |= CallContext::kHaveImplicit; return false; @@ -1259,6 +1270,49 @@ bool cpyrt::ULLongConverter::ToMemory(PyObject* value, void* address, return true; } +//---------------------------------------------------------------------------- +cpyrt::EnumConverter::~EnumConverter() { DestroyConverter(fBase); } + +bool cpyrt::EnumConverter::SetArg(PyObject* pyobject, Parameter& para, + CallContext* ctxt) { + // an instance of the same enum is an exact match; anything else, plain + // integers included, converts in the implicit round only, as in C++ + if (ctxt && !AllowImplicit(ctxt)) { + bool exact = false; + if (IsCPPEnumInstance(pyobject)) { + PyObject* pycppname = + PyObject_GetAttr((PyObject*)Py_TYPE(pyobject), PyStrings::gCppName); + if (pycppname) { + exact = (fEnumName == cpyrt_PyText_AsString(pycppname)); + Py_DECREF(pycppname); + } else + PyErr_Clear(); + } + if (!exact) { + if (!NoImplicit(ctxt)) + ctxt->fFlags |= CallContext::kHaveImplicit; + return false; + } + // the base integer converter defers enum instances, so lift the gate + CallContextRAII allow(ctxt); + return fBase->SetArg(pyobject, para, ctxt); + } + return fBase->SetArg(pyobject, para, ctxt); +} + +PyObject* cpyrt::EnumConverter::FromMemory(void* address) { + return fBase->FromMemory(address); +} + +bool cpyrt::EnumConverter::ToMemory(PyObject* value, void* address, + PyObject* ctxt) { + return fBase->ToMemory(value, address, ctxt); +} + +std::string cpyrt::EnumConverter::GetFailureMsg() { + return fBase->GetFailureMsg(); +} + //---------------------------------------------------------------------------- bool cpyrt::CStringConverter::SetArg(PyObject* pyobject, Parameter& para, CallContext* ctxt) { @@ -3628,6 +3682,16 @@ cppjit::cpyrt::CreateConverter(interop::TCppType_t type, cdims_t dims) { interop::TCppType_t resolvedType = interop::ResolveType(type); const std::string& resolvedTypeStr = interop::GetTypeAsString(resolvedType); + // a by-value enum parameter exact-matches instances of the same enum; the + // resolved type is its underlying integer, which carries the conversion + if (interop::IsEnumType(type) && fullType != "std::byte") { + h = gConvFactories.find(resolvedTypeStr); + if (h != gConvFactories.end()) + return new EnumConverter( + (h->second)(dims), + interop::GetScopedFinalName(interop::GetScopeFromType(type))); + } + // a full, qualified matching converter is preferred if (resolvedTypeStr != fullType) { h = gConvFactories.find(resolvedTypeStr); diff --git a/src/cpyrt/DeclareConverters.h b/src/cpyrt/DeclareConverters.h index 537a8d00..b1b655fd 100644 --- a/src/cpyrt/DeclareConverters.h +++ b/src/cpyrt/DeclareConverters.h @@ -136,6 +136,27 @@ CPPJIT_DECLARE_REFCONVERTER(Float); CPPJIT_DECLARE_REFCONVERTER(Double); CPPJIT_DECLARE_REFCONVERTER(LDouble); +// by-value enum parameters: exact-match instances of the same enum in the +// first resolution round, delegate storage to the underlying integer converter +class EnumConverter : public Converter { +public: + EnumConverter(Converter* base, const std::string& enum_name) + : fBase(base), fEnumName(enum_name) {} + ~EnumConverter() override; + +public: + bool SetArg(PyObject*, Parameter&, CallContext* = nullptr) override; + PyObject* FromMemory(void* address) override; + bool ToMemory(PyObject* value, void* address, + PyObject* ctxt = nullptr) override; + bool HasState() override { return true; } + std::string GetFailureMsg() override; + +private: + Converter* fBase; + std::string fEnumName; +}; + class VoidConverter : public Converter { public: bool SetArg(PyObject*, Parameter&, CallContext* = nullptr) override; diff --git a/test/test_regression.py b/test/test_regression.py index c82d8084..980cbf39 100644 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -1658,3 +1658,31 @@ def test53_str_fallback_without_ostream_insertion(self): # degrade to the generic repr assert str(bare) == repr(bare) assert "Bare object at" in str(bare) + + def test54_enum_arg_overload_priority(self): + """Enum overloads resolve like C++ for both int and enum arguments""" + + import cppjit + + cppjit.cppdef("""\ + namespace EnumArgPriority { + enum Color { Red = 0, Green = 1, Blue = 2 }; + int pick(Color) { return 1; } + int pick(unsigned int) { return 2; } + int only_enum(Color c) { return 10 + (int)c; } + int only_uint(unsigned int v) { return 20 + (int)v; } + }""") + + ns = cppjit.gbl.EnumArgPriority + + # C++ has no implicit int -> enum conversion + assert ns.pick(2) == 2 + + # an enum instance is an exact match for its own enum overload + assert ns.pick(ns.Color.Green) == 1 + + # an enum parameter is only deprioritized, never unusable + assert ns.only_enum(2) == 12 + + # an enum instance still converts to an integer parameter + assert ns.only_uint(ns.Color.Blue) == 22