From 2cdcc7322e18e5cc1ec3a8d7c6cbf3c766d42b06 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Tue, 13 Oct 2026 19:15:51 +0530 Subject: [PATCH 01/71] [cppinterop] Add `IsPackExpansionType`, `IsOperator` and `IsConversionOperator` (#1125) - `IsPackExpansionType` checks whether a type is a pack expansion (e.g. the type `Ts...` of a function parameter pack). - `IsOperator` checks whether a function or function template is an overloaded operator (e.g. `operator+`). - `IsConversionOperator` checks whether a function or function template is a conversion operator (e.g. `operator int()`). Like the other function predicates, `IsOperator` and `IsConversionOperator` look through using-shadow declarations and function templates. (cherry picked from commit e2aca65) --- .../CppInterOp/lib/CppInterOp/CppInterOp.cpp | 25 ++++++ .../CppInterOp/lib/CppInterOp/CppInterOp.td | 22 +++++ .../CppInterOp/FunctionReflectionTest.cpp | 86 +++++++++++++++++++ .../CppInterOp/TypeReflectionTest.cpp | 17 ++++ 4 files changed, 150 insertions(+) diff --git a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp index 30119c4282838..06e0fe70fa61d 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp +++ b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp @@ -2665,6 +2665,13 @@ bool IsTemplateParmType(ConstTypeRef TyRef) { return INTEROP_RETURN(QT->isTemplateTypeParmType()); } +bool IsPackExpansionType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + clang::QualType QT = clang::QualType::getFromOpaquePtr(TyRef.data); + return INTEROP_RETURN( + llvm::isa_and_nonnull(QT.getTypePtrOrNull())); +} + std::string GetFunctionSignature(ConstFuncRef func) { INTEROP_TRACE(func); if (!func) @@ -3069,6 +3076,24 @@ bool IsExplicit(ConstFuncRef method) { return INTEROP_RETURN(false); } +bool IsOperator(ConstFuncRef func) { + INTEROP_TRACE(func); + const auto* D = UnwrapUsingShadowToFunction(unwrap(func)); + if (const auto* FTD = llvm::dyn_cast_or_null(D)) + D = FTD->getTemplatedDecl(); + if (const auto* FD = llvm::dyn_cast_or_null(D)) + return INTEROP_RETURN(FD->isOverloadedOperator()); + return INTEROP_RETURN(false); +} + +bool IsConversionOperator(ConstFuncRef func) { + INTEROP_TRACE(func); + const auto* D = UnwrapUsingShadowToFunction(unwrap(func)); + if (const auto* FTD = llvm::dyn_cast_or_null(D)) + D = FTD->getTemplatedDecl(); + return INTEROP_RETURN(llvm::isa_and_nonnull(D)); +} + void* GetFunctionAddress(const char* mangled_name) { INTEROP_TRACE(mangled_name); auto& I = getInterp(); diff --git a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td index 6aba48d089136..72d6b860d72c1 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td +++ b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td @@ -1503,6 +1503,14 @@ def IsTemplateParmType : CppInterOpAPI { ]; } +def IsPackExpansionType : CppInterOpAPI { + let Doc = "Checks if the given type is a pack expansion type (e.g. `Ts...`)."; + let ReturnType = "bool"; + let Args = [ + Arg<"ConstTypeRef", "TyRef"> + ]; +} + def IsFunction : CppInterOpAPI { let Doc = "Checks if the scope is a function."; let ReturnType = "bool"; @@ -1521,6 +1529,20 @@ def IsMethod : CppInterOpAPI { let Args = [Arg<"ConstFuncRef", "method">]; } +def IsOperator : CppInterOpAPI { + let Doc = [{Checks if the provided function (or function template) is an +overloaded operator.}]; + let ReturnType = "bool"; + let Args = [Arg<"ConstFuncRef", "func">]; +} + +def IsConversionOperator : CppInterOpAPI { + let Doc = [{Checks if the provided function (or function template) is a +conversion operator, e.g. `operator int()`.}]; + let ReturnType = "bool"; + let Args = [Arg<"ConstFuncRef", "func">]; +} + def IsPODType : CppInterOpAPI { let Doc = "Checks if the provided parameter is a Plain Old Data Type (POD)."; let ReturnType = "bool"; diff --git a/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp index 80fdec03f0f12..2fcfd6f085c86 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp @@ -4858,6 +4858,92 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_IsExplicitDeductionGuide) { "Wrapper"); } +TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_IsOperator) { + std::vector Decls, BaseDecls, DerivedDecls; + std::string code = R"( + struct OpBase { + OpBase& operator+=(const OpBase&) { return *this; } + operator int() const { return 0; } + void method() {} + }; + struct OpDerived : OpBase { + using OpBase::operator+=; + }; + bool operator==(const OpBase&, const OpBase&) { return true; } + template bool operator<(const T&, const OpBase&) { + return true; + } + template void free_template(T) {} + void free_function() {} + )"; + + GetAllTopLevelDecls(code, Decls); + ASSERT_EQ(Decls.size(), 6U); + GetAllSubDecls(Decls[0], BaseDecls, /*filter_implicitGenerated=*/true); + ASSERT_EQ(BaseDecls.size(), 3U); + GetAllSubDecls(Decls[1], DerivedDecls); + + EXPECT_TRUE(Cpp::IsOperator(BaseDecls[0])); // operator+= + EXPECT_FALSE(Cpp::IsOperator(BaseDecls[1])); // operator int + EXPECT_FALSE(Cpp::IsOperator(BaseDecls[2])); // method + + // using OpBase::operator+=; + Decl* Shadow = nullptr; + for (auto* D : DerivedDecls) + if (llvm::isa(D)) + Shadow = D; + ASSERT_TRUE(Shadow); + EXPECT_TRUE(Cpp::IsOperator(Shadow)); + + EXPECT_FALSE(Cpp::IsOperator(Decls[0])); // not a function + EXPECT_TRUE(Cpp::IsOperator(Decls[2])); // operator== + EXPECT_TRUE(Cpp::IsOperator(Decls[3])); // template operator< + EXPECT_FALSE(Cpp::IsOperator(Decls[4])); // free_template + EXPECT_FALSE(Cpp::IsOperator(Decls[5])); // free_function + EXPECT_FALSE(Cpp::IsOperator(nullptr)); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_IsConversionOperator) { + std::vector Decls, BaseDecls, DerivedDecls; + std::string code = R"( + struct ConvBase { + operator int() const { return 0; } + explicit operator bool() const { return true; } + template operator T*() const { return nullptr; } + bool operator!() const { return false; } + void method() {} + }; + struct ConvDerived : ConvBase { + using ConvBase::operator int; + }; + void free_function() {} + )"; + + GetAllTopLevelDecls(code, Decls); + ASSERT_EQ(Decls.size(), 3U); + GetAllSubDecls(Decls[0], BaseDecls, /*filter_implicitGenerated=*/true); + ASSERT_EQ(BaseDecls.size(), 5U); + GetAllSubDecls(Decls[1], DerivedDecls); + + EXPECT_TRUE(Cpp::IsConversionOperator(BaseDecls[0])); // operator int + EXPECT_TRUE(Cpp::IsConversionOperator(BaseDecls[1])); // operator bool + EXPECT_TRUE(Cpp::IsConversionOperator(BaseDecls[2])); // operator T* + EXPECT_FALSE(Cpp::IsConversionOperator(BaseDecls[3])); // operator! + EXPECT_FALSE(Cpp::IsConversionOperator(BaseDecls[4])); // method + + // using ConvBase::operator int; + Decl* Shadow = nullptr; + for (auto* D : DerivedDecls) + if (llvm::isa(D)) + Shadow = D; + ASSERT_TRUE(Shadow); + EXPECT_TRUE(Cpp::IsConversionOperator(Shadow)); + + EXPECT_FALSE(Cpp::IsConversionOperator(Decls[0])); // not a function + EXPECT_FALSE(Cpp::IsConversionOperator(Decls[2])); // free_function + EXPECT_FALSE(Cpp::IsConversionOperator(nullptr)); +} + // C++23 "deducing this" (explicit object parameters, P0847R7): the object // parameter binds to the receiver, not the argument list. TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_DeducingThisIntrospection) { diff --git a/interpreter/CppInterOp/unittests/CppInterOp/TypeReflectionTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/TypeReflectionTest.cpp index 3fa88696c7d71..c6800106f39fd 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/TypeReflectionTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/TypeReflectionTest.cpp @@ -592,6 +592,23 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, TypeReflection_IsTemplateParmType) { EXPECT_FALSE(Cpp::IsTemplateParmType(Cpp::GetFunctionArgType(Decls[0], 2))); } +TYPED_TEST(CPPINTEROP_TEST_MODE, TypeReflection_IsPackExpansionType) { + std::vector Decls; + + std::string code = R"( + template void f(T t, Ts... ts) {} + template void g(const Ts&... ts) {} + )"; + + GetAllTopLevelDecls(code, Decls); + + EXPECT_FALSE(Cpp::IsPackExpansionType(Cpp::GetFunctionArgType(Decls[0], 0))); + EXPECT_TRUE(Cpp::IsPackExpansionType(Cpp::GetFunctionArgType(Decls[0], 1))); + EXPECT_TRUE(Cpp::IsPackExpansionType(Cpp::GetFunctionArgType(Decls[1], 0))); + EXPECT_FALSE(Cpp::IsPackExpansionType(Cpp::GetType("int"))); + EXPECT_FALSE(Cpp::IsPackExpansionType(nullptr)); +} + TYPED_TEST(CPPINTEROP_TEST_MODE, TypeReflection_IsSmartPtrType) { #if CLANG_VERSION_MAJOR == 20 && defined(CPPINTEROP_USE_CLING) && defined(_WIN32) GTEST_SKIP() << "Test fails with Cling on Windows"; From ec249a78595a4f5e4c9f66bf527594734765b0ef Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Tue, 13 Oct 2026 11:30:23 +0530 Subject: [PATCH 02/71] [cppinterop] Rework `BestOverloadFunctionMatch` for methods, operators and ambiguity - Non-static member functions and member function templates are added as method candidates. The object type is taken from the first entry of `arg_types`, and the remaining entries are the call arguments. This replaces the synthesized receiver that was only used for explicit object member functions. - New `is_operator` parameter (default `false`) resolves the candidates as an operator call (`CSK_Operator`). - New `ambiguous_candidates` out-parameter: when the call is ambiguous, the best viable candidates are appended to it and `nullptr` is returned. - A candidate brought in by a using-declaration (a UsingShadowDecl, as returned by e.g. GetClassMethods for `using Base::f;`) was silently ignored. It is now added with the UsingShadowDecl as its found decl, so Sema treats it as a member of the class the using-declaration is in, as C++ requires ([over.match.funcs]). - Add a test covering constructors, static member functions, their templates, using-declarations, non-function candidates, calls without an object and calls without a viable candidate. (cherry picked from commit eb14c12) --- .../CppInterOp/lib/CppInterOp/CppInterOp.cpp | 65 ++-- .../CppInterOp/lib/CppInterOp/CppInterOp.td | 17 +- .../CppInterOp/FunctionReflectionTest.cpp | 317 +++++++++++++----- 3 files changed, 294 insertions(+), 105 deletions(-) diff --git a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp index 06e0fe70fa61d..f5d96432c9d06 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp +++ b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp @@ -2857,8 +2857,11 @@ bool GetClassTemplatedMethods(const std::string& name, ConstDeclRef parent, FuncRef BestOverloadFunctionMatch(const std::vector& candidates, const std::vector& explicit_types, - const std::vector& arg_types) { - INTEROP_TRACE(candidates, explicit_types, arg_types); + const std::vector& arg_types, + std::vector& ambiguous_candidates, + bool is_operator) { + INTEROP_TRACE(candidates, explicit_types, arg_types, + INTEROP_OUT(ambiguous_candidates), is_operator); auto& S = getSema(); auto& C = S.getASTContext(); @@ -2874,6 +2877,7 @@ BestOverloadFunctionMatch(const std::vector& candidates, }; auto* Exprs = new WrapperExpr[arg_types.size()]; llvm::SmallVector Args; + Args.reserve(arg_types.size()); size_t idx = 0; for (auto i : arg_types) { @@ -2916,25 +2920,34 @@ BestOverloadFunctionMatch(const std::vector& candidates, Sema::SFINAETrap Trap(S, /*ForValidityCheck=*/true); OverloadCandidateSet Overloads( - Loc, OverloadCandidateSet::CandidateSetKind::CSK_Normal); + Loc, is_operator ? OverloadCandidateSet::CandidateSetKind::CSK_Operator + : OverloadCandidateSet::CandidateSetKind::CSK_Normal); + + // check if this function requires the this/self argument + auto IsCalledOnObject = [&Args](const CXXMethodDecl* CXXMD) { + return CXXMD && !Cpp::IsConstructor(CXXMD) && !CXXMD->isStatic() && + !Args.empty(); + }; for (auto i : candidates) { - auto* D = const_cast(unwrap(i)); - if (auto* FD = dyn_cast(D)) { - S.AddOverloadCandidate(FD, DeclAccessPair::make(FD, FD->getAccess()), - Args, Overloads); + auto* Found = const_cast(unwrap(i)); + auto FoundDeclAccessPair = + DeclAccessPair::make(cast(Found), Found->getAccess()); + auto* D = UnwrapUsingShadowToFunction(Found); + if (auto* CXXMD = dyn_cast(D); IsCalledOnObject(CXXMD)) { + S.AddMethodCandidate(FoundDeclAccessPair, Args.front()->getType(), + Args.front()->Classify(C), + llvm::ArrayRef(Args).slice(1), Overloads); + } else if (auto* FD = dyn_cast(D)) { + S.AddOverloadCandidate(FD, FoundDeclAccessPair, Args, Overloads); } else if (auto* FTD = dyn_cast(D)) { - auto* MD = dyn_cast(FTD->getTemplatedDecl()); - if (MD && MD->isExplicitObjectMemberFunction()) { - // The explicit object parameter isn't in Args, so deduce it via the - // method-template path with a synthesized receiver of the record type. - CXXRecordDecl* RD = MD->getParent(); - QualType ObjectType = compat::GetTypeFromDecl(RD); - OpaqueValueExpr ObjectExpr(SourceLocation::getFromRawEncoding(1), - ObjectType, ExprValueKind::VK_LValue); + if (auto* CXXMD = dyn_cast(FTD->getTemplatedDecl()); + IsCalledOnObject(CXXMD)) { S.AddMethodTemplateCandidate( - FTD, DeclAccessPair::make(FTD, FTD->getAccess()), RD, - &ExplicitTemplateArgs, ObjectType, ObjectExpr.Classify(C), Args, + FTD, FoundDeclAccessPair, + Args.front()->getType()->getAsCXXRecordDecl(), + &ExplicitTemplateArgs, Args.front()->getType(), + Args.front()->Classify(C), llvm::ArrayRef(Args).slice(1), Overloads); } else { // AddTemplateOverloadCandidate is causing a memory leak @@ -2942,17 +2955,23 @@ BestOverloadFunctionMatch(const std::vector& candidates, // call stack: AddTemplateOverloadCandidate -> MakeDeductionFailureInfo // source: // https://github.com/llvm/llvm-project/blob/release/19.x/clang/lib/Sema/SemaOverload.cpp#L731-L756 - S.AddTemplateOverloadCandidate( - FTD, DeclAccessPair::make(FTD, FTD->getAccess()), - &ExplicitTemplateArgs, Args, Overloads); + S.AddTemplateOverloadCandidate(FTD, FoundDeclAccessPair, + &ExplicitTemplateArgs, Args, Overloads); } } } OverloadCandidateSet::iterator Best; - Overloads.BestViableFunction(S, Loc, Best); - - FunctionDecl* Result = Best != Overloads.end() ? Best->Function : nullptr; + auto res = Overloads.BestViableFunction(S, Loc, Best); + FunctionDecl* Result = nullptr; + if (res == clang::OR_Success) { + Result = Best->Function; + } else if (res == clang::OR_Ambiguous) { + for (const auto& i : Overloads) { + if (i.Best) + ambiguous_candidates.emplace_back(i.Function); + } + } // If the winner is a not-yet-instantiated template specialization, force // the instantiation of its body now, with diagnostics suppressed, and diff --git a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td index 72d6b860d72c1..d1a78998037d8 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td +++ b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td @@ -1211,13 +1211,24 @@ def BeginStdStreamCapture : CppInterOpAPI { } def BestOverloadFunctionMatch : CppInterOpAPI { - let Doc = [{Finds best overload match based on explicit template parameters -(if any) and argument types.}]; + let Doc = [{Finds the best overload match among \c candidates based on the +explicit template arguments (if any) and the argument types. + +For a non-static member function candidate, the first entry of \c arg_types +is the type of the object the function is called on, and the remaining +entries are the call arguments. With \c is_operator set, the call is +resolved as an operator call. + +\returns the best match, or nullptr if there is none. If the call is +ambiguous, the best viable candidates are appended to +\c ambiguous_candidates.}]; let ReturnType = "FuncRef"; let Args = [ Arg<"const std::vector&", "candidates">, Arg<"const std::vector&", "explicit_types">, - Arg<"const std::vector&", "arg_types"> + Arg<"const std::vector&", "arg_types">, + Arg<"std::vector&", "ambiguous_candidates">, + Arg<"bool", "is_operator", "false"> ]; } diff --git a/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp index 2fcfd6f085c86..3ee8e4b403586 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp @@ -869,11 +869,12 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionReturnType) { std::vector args = {C.IntTy.getAsOpaquePtr(), C.DoubleTy.getAsOpaquePtr()}; std::vector explicit_args; + std::vector ambiguous_candidates; std::vector candidates = {Decls[14]}; - EXPECT_EQ( - Cpp::GetTypeAsString(Cpp::GetFunctionReturnType( - Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args))), - "RTTest_TemplatedList"); + EXPECT_EQ(Cpp::GetTypeAsString( + Cpp::GetFunctionReturnType(Cpp::BestOverloadFunctionMatch( + candidates, explicit_args, args, ambiguous_candidates))), + "RTTest_TemplatedList"); std::vector args2 = {C.DoubleTy.getAsOpaquePtr()}; EXPECT_EQ( @@ -2510,6 +2511,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, GetAllTopLevelDecls(code, Decls); EXPECT_EQ(Decls.size(), 1); + std::vector ambiguous_candidates; std::vector candidates; candidates.reserve(Decls.size()); for (auto* i : Decls) @@ -2530,15 +2532,16 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, C.IntTy.getAsOpaquePtr(), }; - Cpp::FuncRef fn0 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args0, args0); + Cpp::FuncRef fn0 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args0, args0, ambiguous_candidates); EXPECT_TRUE(fn0); - Cpp::FuncRef fn = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args1, args0); + Cpp::FuncRef fn = Cpp::BestOverloadFunctionMatch(candidates, explicit_args1, + args0, ambiguous_candidates); EXPECT_EQ(fn, fn0); - fn = Cpp::BestOverloadFunctionMatch(candidates, explicit_args2, args0); + fn = Cpp::BestOverloadFunctionMatch(candidates, explicit_args2, args0, + ambiguous_candidates); EXPECT_EQ(fn, fn0); fn = Cpp::FuncRef{Cpp::InstantiateTemplate(Decls[0], explicit_args1).data}; @@ -2588,6 +2591,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, )"; GetAllTopLevelDecls(code, Decls); + std::vector ambiguous_candidates; std::vector candidates; for (auto decl : Decls) @@ -2596,27 +2600,35 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ASTContext& C = Interp->getCI()->getASTContext(); - std::vector args0; + Cpp::DeclRef ScopeKlass = Cpp::GetNamed("MyTemplatedMethodClass"); + EXPECT_TRUE(ScopeKlass); + + Cpp::TypeRef TypeKlass = Cpp::GetTypeFromScope(ScopeKlass); + EXPECT_TRUE(TypeKlass); + + std::vector args0 = {TypeKlass.data}; std::vector args1 = { - C.getLValueReferenceType(C.IntTy).getAsOpaquePtr()}; - std::vector args2 = {C.CharTy.getAsOpaquePtr(), C.FloatTy.getAsOpaquePtr()}; - std::vector args3 = {C.FloatTy.getAsOpaquePtr()}; + TypeKlass.data, C.getLValueReferenceType(C.IntTy).getAsOpaquePtr()}; + std::vector args2 = { + TypeKlass.data, C.CharTy.getAsOpaquePtr(), C.FloatTy.getAsOpaquePtr()}; + std::vector args3 = {TypeKlass.data, + C.FloatTy.getAsOpaquePtr()}; std::vector explicit_args0; std::vector explicit_args1 = {C.IntTy.getAsOpaquePtr()}; std::vector explicit_args2 = { {C.IntTy.getAsOpaquePtr(), "1"}, C.IntTy.getAsOpaquePtr()}; - Cpp::FuncRef func1 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args0, args1); - Cpp::FuncRef func2 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args1, args0); - Cpp::FuncRef func3 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args0, args2); - Cpp::FuncRef func4 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args1, args3); - Cpp::FuncRef func5 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args2, args3); + Cpp::FuncRef func1 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args0, args1, ambiguous_candidates); + Cpp::FuncRef func2 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args1, args0, ambiguous_candidates); + Cpp::FuncRef func3 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args0, args2, ambiguous_candidates); + Cpp::FuncRef func4 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args1, args3, ambiguous_candidates); + Cpp::FuncRef func5 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args2, args3, ambiguous_candidates); EXPECT_EQ(Cpp::GetFunctionSignature(func1), "template<> long MyTemplatedMethodClass::get_size(int &)"); @@ -2656,6 +2668,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, GetAllTopLevelDecls(code, Decls); std::vector candidates; + std::vector ambiguous_candidates; for (auto decl : Decls) if (Cpp::IsFunction(decl) || Cpp::IsTemplatedFunction(decl)) @@ -2678,16 +2691,16 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, std::vector explicit_args; - Cpp::FuncRef func1 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args1); - Cpp::FuncRef func2 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args2); - Cpp::FuncRef func3 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args3); - Cpp::FuncRef func4 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args4); - Cpp::FuncRef func5 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args5); + Cpp::FuncRef func1 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args, args1, ambiguous_candidates); + Cpp::FuncRef func2 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args, args2, ambiguous_candidates); + Cpp::FuncRef func3 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args, args3, ambiguous_candidates); + Cpp::FuncRef func4 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args, args4, ambiguous_candidates); + Cpp::FuncRef func5 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args, args5, ambiguous_candidates); EXPECT_EQ(Cpp::GetFunctionSignature(func1), "template<> void somefunc(int arg)"); @@ -2730,6 +2743,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, GetAllTopLevelDecls(code, Decls); std::vector candidates; + std::vector ambiguous_candidates; for (auto decl : Decls) if (Cpp::IsTemplatedFunction(decl)) @@ -2749,12 +2763,12 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, C.DoubleTy.getAsOpaquePtr()}; std::vector explicit_args; - Cpp::FuncRef func1 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args1); - Cpp::FuncRef func2 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args2); - Cpp::FuncRef func3 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args3); + Cpp::FuncRef func1 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args, args1, ambiguous_candidates, true); + Cpp::FuncRef func2 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args, args2, ambiguous_candidates, true); + Cpp::FuncRef func3 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args, args3, ambiguous_candidates, true); candidates.clear(); Cpp::GetOperator( @@ -2764,10 +2778,11 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, EXPECT_EQ(candidates.size(), 1); std::vector args4 = { + Cpp::GetVariableType(Cpp::GetNamed("a")).data, Cpp::GetVariableType(Cpp::GetNamed("a")).data}; - Cpp::FuncRef func4 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args4); + Cpp::FuncRef func4 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args, args4, ambiguous_candidates, true); EXPECT_EQ(Cpp::GetFunctionSignature(func1), "template<> A operator+(A lhs, A rhs)"); @@ -2838,6 +2853,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, GetAllTopLevelDecls(code, Decls); GetAllSubDecls(Decls[1], SubDecls); + std::vector ambiguous_candidates; std::vector candidates; for (auto i : SubDecls) { if ((Cpp::IsFunction(i) || Cpp::IsTemplatedFunction(i)) && @@ -2849,31 +2865,38 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ASTContext& C = Interp->getCI()->getASTContext(); - std::vector args1 = {}; - std::vector args2 = {C.IntTy.getAsOpaquePtr()}; + Cpp::DeclRef ScopeB = Cpp::GetNamed("B"); + EXPECT_TRUE(ScopeB); + + Cpp::TypeRef TypeB = Cpp::GetTypeFromScope(ScopeB); + EXPECT_TRUE(TypeB); + + std::vector args1 = {TypeB.data}; + std::vector args2 = {TypeB.data, + C.IntTy.getAsOpaquePtr()}; std::vector args3 = { - Cpp::GetVariableType(Cpp::GetNamed("a")).data}; + TypeB.data, Cpp::GetVariableType(Cpp::GetNamed("a")).data}; std::vector args4 = { - Cpp::GetVariableType(Cpp::GetNamed("a")).data, + TypeB.data, Cpp::GetVariableType(Cpp::GetNamed("a")).data, Cpp::GetVariableType(Cpp::GetNamed("b")).data}; std::vector args5 = { - Cpp::GetVariableType(Cpp::GetNamed("a")).data, + TypeB.data, Cpp::GetVariableType(Cpp::GetNamed("a")).data, Cpp::GetVariableType(Cpp::GetNamed("a")).data}; std::vector explicit_args1; std::vector explicit_args2 = {C.IntTy.getAsOpaquePtr(), C.IntTy.getAsOpaquePtr()}; - Cpp::FuncRef func1 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args1, args1); - Cpp::FuncRef func2 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args1, args2); - Cpp::FuncRef func3 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args1, args3); - Cpp::FuncRef func4 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args1, args4); - Cpp::FuncRef func5 = - Cpp::BestOverloadFunctionMatch(candidates, explicit_args2, args5); + Cpp::FuncRef func1 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args1, args1, ambiguous_candidates); + Cpp::FuncRef func2 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args1, args2, ambiguous_candidates); + Cpp::FuncRef func3 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args1, args3, ambiguous_candidates); + Cpp::FuncRef func4 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args1, args4, ambiguous_candidates); + Cpp::FuncRef func5 = Cpp::BestOverloadFunctionMatch( + candidates, explicit_args2, args5, ambiguous_candidates); EXPECT_EQ(Cpp::GetFunctionSignature(func1), "void B::fn()"); EXPECT_EQ(Cpp::GetFunctionSignature(func2), @@ -2902,6 +2925,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, EXPECT_EQ(Decls.size(), 2); std::vector candidates; + std::vector ambiguous_candidates; candidates.push_back(Decls[1]); ASTContext& C = Interp->getCI()->getASTContext(); @@ -2919,8 +2943,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, C.getLValueReferenceType(C.IntTy).getAsOpaquePtr(), }; - Cpp::FuncRef callback = - Cpp::BestOverloadFunctionMatch(candidates, empty_templ_args, arg_types); + Cpp::FuncRef callback = Cpp::BestOverloadFunctionMatch( + candidates, empty_templ_args, arg_types, ambiguous_candidates); EXPECT_TRUE(callback); EXPECT_EQ(Cpp::GetFunctionSignature(callback), @@ -2928,6 +2952,127 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, "&>>(void (*callable)(double, int), double &args, int &args)"); } +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_BestOverloadFunctionMatch6) { + std::vector Decls; + std::string code = R"( + struct A0 { + A0(int) {} + }; + + struct B0 { + B0(int) {} + }; + + struct C0 {}; + + void consume(A0) {} + void consume(B0) {} + void consume(C0) {} + )"; + GetAllTopLevelDecls(code, Decls); + EXPECT_EQ(Decls.size(), 6); + + std::vector ambiguous_candidates; + std::vector candidates; + for (auto i : Decls) { + if (Cpp::IsFunction(i)) + candidates.push_back(i); + } + EXPECT_EQ(candidates.size(), 3); + + Cpp::TypeRef TypeA = Cpp::GetTypeFromScope(Cpp::GetScope("A0")); + EXPECT_TRUE(TypeA); + Cpp::TypeRef TypeB = Cpp::GetTypeFromScope(Cpp::GetScope("B0")); + EXPECT_TRUE(TypeB); + + Cpp::FuncRef meth = Cpp::BestOverloadFunctionMatch( + candidates, {}, {Cpp::GetType("int").data}, ambiguous_candidates); + EXPECT_FALSE(meth); + EXPECT_EQ(ambiguous_candidates.size(), 2); + + // the following might fail due to ordering + EXPECT_EQ(Cpp::GetFunctionSignature(ambiguous_candidates[0]), + "void consume(A0)"); + EXPECT_EQ(Cpp::GetFunctionSignature(ambiguous_candidates[1]), + "void consume(B0)"); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_BestOverloadFunctionMatch_CandidateKinds) { + std::vector Decls, SubDecls, DerivedDecls; + std::string code = R"( + struct BOKinds { + BOKinds(int) {} + template BOKinds(T*) {} + static int s(int) { return 0; } + template static T st(T t) { return t; } + int m(int) { return 0; } + int get() { return 0; } + }; + struct BOKindsDerived : BOKinds { + using BOKinds::m; + int m(double) { return 1; } + }; + int bo_var = 0; + )"; + GetAllTopLevelDecls(code, Decls); + ASSERT_EQ(Decls.size(), 3U); + GetAllSubDecls(Decls[0], SubDecls, /*filter_implicitGenerated=*/true); + ASSERT_EQ(SubDecls.size(), 6U); + GetAllSubDecls(Decls[1], DerivedDecls); + + Cpp::TypeRef Int = Cpp::GetType("int"); + Cpp::TypeRef Double = Cpp::GetType("double"); + Cpp::TypeRef KindsTy = Cpp::GetTypeFromScope(Decls[0]); + Cpp::TypeRef DerivedTy = Cpp::GetTypeFromScope(Decls[1]); + std::vector ambiguous_candidates; + auto Match = [&](std::vector candidates, + std::vector arg_types) { + return Cpp::GetFunctionSignature(Cpp::BestOverloadFunctionMatch( + candidates, {}, arg_types, ambiguous_candidates)); + }; + + // Constructors and static member functions take no object argument. + std::vector ctors = {SubDecls[0], SubDecls[1]}; + EXPECT_EQ(Match(ctors, {Int.data}), "BOKinds::BOKinds(int)"); + EXPECT_EQ(Match(ctors, {Cpp::GetPointerType(Double).data}), + "template<> BOKinds::BOKinds(double *)"); + std::vector statics = {SubDecls[2], SubDecls[3]}; + EXPECT_EQ(Match(statics, {Int.data}), "static int BOKinds::s(int)"); + EXPECT_EQ(Match(statics, {Double.data}), + "template<> static double BOKinds::st(double t)"); + + // No viable candidate. + EXPECT_FALSE(Cpp::BestOverloadFunctionMatch({SubDecls[2]}, {}, {KindsTy.data}, + ambiguous_candidates)); + EXPECT_TRUE(ambiguous_candidates.empty()); + + // A method brought in by a using-declaration is a candidate too. + Decl* Shadow = nullptr; + Decl* DerivedM = nullptr; + for (auto* D : DerivedDecls) { + if (llvm::isa(D)) + Shadow = D; + else if (Cpp::IsMethod(D) && !D->isImplicit()) + DerivedM = D; + } + ASSERT_TRUE(Shadow); + ASSERT_TRUE(DerivedM); + EXPECT_EQ(Match({Shadow, DerivedM}, {DerivedTy.data, Int.data}), + "int BOKinds::m(int)"); + EXPECT_EQ(Match({Shadow, DerivedM}, {DerivedTy.data, Double.data}), + "int BOKindsDerived::m(double)"); + + // Candidates that are not functions are ignored. + EXPECT_EQ(Match({Decls[2], SubDecls[2]}, {Int.data}), + "static int BOKinds::s(int)"); + + // Without argument types there is no object to call a method on; the + // method is resolved with an implicit object argument. + EXPECT_EQ(Match({SubDecls[5]}, {}), "int BOKinds::get()"); +} + TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_IsPublicMethod) { std::vector Decls, SubDecls; std::string code = R"( @@ -3504,6 +3649,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { FCI_TOperatorCtor.Invoke((void*)&toperator); EXPECT_TRUE(toperator); + std::vector ambiguous_candidates; std::vector operators; Cpp::GetOperator(TOperator, Cpp::Operator::OP_Less, operators); EXPECT_EQ(operators.size(), 1); @@ -3553,7 +3699,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { Cpp::Operator::OP_Plus, operators); EXPECT_EQ(operators.size(), 1); Cpp::FuncRef kop = Cpp::BestOverloadFunctionMatch(operators, empty_templ_args, - {K1.data, K2.data}); + {K1.data, K2.data}, + ambiguous_candidates, true); auto chrono_op_fn_callable = Cpp::MakeFunctionCallable(kop); EXPECT_EQ(chrono_op_fn_callable.getKind(), Cpp::JitCall::kGenericCall); @@ -3632,8 +3779,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { EXPECT_TRUE(p); Cpp::FuncRef fn = Cpp::BestOverloadFunctionMatch( - unresolved_candidate_methods, {{Cpp::GetType("int").data, "0"}}, - {p.data}); + unresolved_candidate_methods, {{Cpp::GetType("int").data, "0"}}, {p.data}, + ambiguous_candidates); EXPECT_TRUE(fn); auto fn_callable = Cpp::MakeFunctionCallable(fn); @@ -3653,7 +3800,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { Cpp::FuncRef call_move = Cpp::BestOverloadFunctionMatch( unresolved_candidate_methods, {}, - {Cpp::GetReferencedType(Cpp::GetType("int"), true).data}); + {Cpp::GetReferencedType(Cpp::GetType("int"), true).data}, + ambiguous_candidates); EXPECT_TRUE(call_move); auto call_move_callable = Cpp::MakeFunctionCallable(call_move); @@ -3668,7 +3816,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { EXPECT_EQ(unresolved_candidate_methods.size(), 1); Cpp::FuncRef instantiation_in_host = Cpp::BestOverloadFunctionMatch( - unresolved_candidate_methods, {Cpp::GetType("int").data}, {}); + unresolved_candidate_methods, {Cpp::GetType("int").data}, {}, + ambiguous_candidates); EXPECT_TRUE(instantiation_in_host); Cpp::JitCall instantiation_in_host_callable = @@ -3677,7 +3826,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { Cpp::JitCall::kGenericCall); instantiation_in_host = Cpp::BestOverloadFunctionMatch( - unresolved_candidate_methods, {Cpp::GetType("double").data}, {}); + unresolved_candidate_methods, {Cpp::GetType("double").data}, {}, + ambiguous_candidates); EXPECT_TRUE(instantiation_in_host); Cpp::BeginStdStreamCapture(Cpp::CaptureStreamKind::kStdErr); @@ -3707,7 +3857,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { Cpp::FuncRef tuple_tuple = Cpp::BestOverloadFunctionMatch( unresolved_candidate_methods, {}, {Cpp::GetVariableType(Cpp::GetNamed("tuple_one")).data, - Cpp::GetVariableType(Cpp::GetNamed("tuple_two")).data}); + Cpp::GetVariableType(Cpp::GetNamed("tuple_two")).data}, + ambiguous_candidates); EXPECT_TRUE(tuple_tuple); auto tuple_tuple_callable = Cpp::MakeFunctionCallable(tuple_tuple); @@ -3747,7 +3898,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { Cpp::FuncRef consume = Cpp::BestOverloadFunctionMatch( unresolved_candidate_methods, {}, - {Cpp::GetVariableType(Cpp::GetNamed("consumable")).data}); + {Cpp::GetVariableType(Cpp::GetNamed("consumable")).data}, + ambiguous_candidates); EXPECT_TRUE(consume); auto consume_callable = Cpp::MakeFunctionCallable(consume); @@ -3783,11 +3935,14 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { Cpp::GetOperator(KlassProduct_int, Cpp::Operator::OP_Star, operators); EXPECT_EQ(operators.size(), 2); - Cpp::FuncRef op2 = Cpp::BestOverloadFunctionMatch( - operators, {}, {{Cpp::GetTypeFromScope(KlassProduct_float).data}}); - EXPECT_TRUE(op2); + Cpp::FuncRef op_fn = Cpp::BestOverloadFunctionMatch( + operators, {}, + {Cpp::GetTypeFromScope(KlassProduct_int).data, + Cpp::GetTypeFromScope(KlassProduct_float).data}, + ambiguous_candidates, true); + EXPECT_TRUE(op_fn); - auto op_callable = Cpp::MakeFunctionCallable(op2); + auto op_callable = Cpp::MakeFunctionCallable(op_fn); EXPECT_EQ(op_callable.getKind(), Cpp::JitCall::kGenericCall); Cpp::Declare(R"( @@ -3861,9 +4016,11 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { unresolved_candidate_methods); EXPECT_EQ(unresolved_candidate_methods.size(), 1); + ambiguous_candidates.clear(); Cpp::FuncRef callback_func = Cpp::BestOverloadFunctionMatch( unresolved_candidate_methods, {}, - {Cpp::GetVariableType(Cpp::GetNamed("fn_ptr")).data}); + {Cpp::GetVariableType(Cpp::GetNamed("fn_ptr")).data}, + ambiguous_candidates); EXPECT_TRUE(callback_func); auto callback_callable = Cpp::MakeFunctionCallable(callback_func); @@ -5100,17 +5257,17 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, GetAllSubDecls(Decls[0], SubDecls, /*filter_implicitGenerated=*/true); std::vector candidates; + std::vector ambiguous_candidates; for (auto* decl : SubDecls) if (Cpp::IsTemplatedFunction(decl)) candidates.push_back((Cpp::FuncRef)decl); ASSERT_EQ(candidates.size(), (size_t)1); - // No explicit template/call args: `Self` deduces from the synthesized - // receiver (only the AddMethodTemplateCandidate path can do this). std::vector no_explicit_args; - std::vector no_args; - Cpp::FuncRef matched = - Cpp::BestOverloadFunctionMatch(candidates, no_explicit_args, no_args); + Cpp::TypeRef widget_type = Cpp::GetTypeFromScope(Cpp::GetScope("TWidget")); + std::vector args = {widget_type.data}; + Cpp::FuncRef matched = Cpp::BestOverloadFunctionMatch( + candidates, no_explicit_args, args, ambiguous_candidates); ASSERT_TRUE(matched); EXPECT_NE(Cpp::GetFunctionSignature(matched).find("via"), std::string::npos); } @@ -5134,15 +5291,17 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, GetAllSubDecls(Decls[0], SubDecls, /*filter_implicitGenerated=*/true); std::vector candidates; + std::vector ambiguous_candidates; for (auto* decl : SubDecls) if (Cpp::IsTemplatedFunction(decl)) candidates.push_back((Cpp::FuncRef)decl); ASSERT_EQ(candidates.size(), (size_t)1); std::vector no_explicit_args; - std::vector no_args; - Cpp::FuncRef matched = - Cpp::BestOverloadFunctionMatch(candidates, no_explicit_args, no_args); + Cpp::TypeRef aawidget_type = Cpp::GetTypeFromScope(Cpp::GetScope("AAWidget")); + std::vector args = {aawidget_type.data}; + Cpp::FuncRef matched = Cpp::BestOverloadFunctionMatch( + candidates, no_explicit_args, args, ambiguous_candidates); ASSERT_TRUE(matched); EXPECT_NE(Cpp::GetFunctionSignature(matched).find("get"), std::string::npos); } From 7831d60670af884890b851ee264c22bc50162cdf Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Wed, 14 Oct 2026 12:04:41 +0530 Subject: [PATCH 03/71] [cppinterop] Refactor according to code review (cherry picked from commit 76c198c) --- .../CppInterOp/lib/CppInterOp/CppInterOp.cpp | 4 ++-- .../unittests/CppInterOp/FunctionReflectionTest.cpp | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp index f5d96432c9d06..97fb7db70b212 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp +++ b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp @@ -2859,7 +2859,7 @@ BestOverloadFunctionMatch(const std::vector& candidates, const std::vector& explicit_types, const std::vector& arg_types, std::vector& ambiguous_candidates, - bool is_operator) { + bool is_operator /*= false*/) { INTEROP_TRACE(candidates, explicit_types, arg_types, INTEROP_OUT(ambiguous_candidates), is_operator); auto& S = getSema(); @@ -2930,7 +2930,7 @@ BestOverloadFunctionMatch(const std::vector& candidates, }; for (auto i : candidates) { - auto* Found = const_cast(unwrap(i)); + auto* Found = unwrap(i); auto FoundDeclAccessPair = DeclAccessPair::make(cast(Found), Found->getAccess()); auto* D = UnwrapUsingShadowToFunction(Found); diff --git a/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp index 3ee8e4b403586..bbba8027274ad 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include "gtest/gtest.h" @@ -2975,9 +2976,9 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, std::vector ambiguous_candidates; std::vector candidates; - for (auto i : Decls) { + for (auto* i : Decls) { if (Cpp::IsFunction(i)) - candidates.push_back(i); + candidates.emplace_back(i); } EXPECT_EQ(candidates.size(), 3); @@ -3000,7 +3001,9 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_BestOverloadFunctionMatch_CandidateKinds) { - std::vector Decls, SubDecls, DerivedDecls; + std::vector Decls; + std::vector SubDecls; + std::vector DerivedDecls; std::string code = R"( struct BOKinds { BOKinds(int) {} @@ -3027,8 +3030,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, Cpp::TypeRef KindsTy = Cpp::GetTypeFromScope(Decls[0]); Cpp::TypeRef DerivedTy = Cpp::GetTypeFromScope(Decls[1]); std::vector ambiguous_candidates; - auto Match = [&](std::vector candidates, - std::vector arg_types) { + auto Match = [&](const std::vector& candidates, + const std::vector& arg_types) { return Cpp::GetFunctionSignature(Cpp::BestOverloadFunctionMatch( candidates, {}, arg_types, ambiguous_candidates)); }; From 3890823c838e89ac41d8e0bfaf057faf5d7f48ed Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Wed, 7 Oct 2026 18:28:08 +0200 Subject: [PATCH 04/71] [cppjit] [cpyrt] Generate the PyStrings cache from a single X-macro list (compiler-ressearch/cppjit#141) Each cached string was spelled out four times: the extern declaration, the definition, the interning in CreatePyStrings() and the release in DestroyPyStrings(). The lists had drifted: gAssign, gCastCpp, gRepr, gCppRepr, gStr and gCppStr were never released. List every string once in CPYRT_PYSTRINGS and expand the declarations, definitions, creation and cleanup from it. Call sites are unchanged. gEmptyString is now interned like the others, which yields the same object since CPython keeps a single empty string. (cherry picked from commit 13f4c58) --- .../pyroot/cppjit/src/cpyrt/PyStrings.cxx | 290 +----------------- bindings/pyroot/cppjit/src/cpyrt/PyStrings.h | 155 +++++----- 2 files changed, 92 insertions(+), 353 deletions(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/PyStrings.cxx b/bindings/pyroot/cppjit/src/cpyrt/PyStrings.cxx index a0e957f3787f9..9745acaec32d0 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/PyStrings.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/PyStrings.cxx @@ -5,164 +5,20 @@ using namespace cppjit; #include "PyStrings.h" //- data _____________________________________________________________________ -PyObject* cpyrt::PyStrings::gAssign = nullptr; -PyObject* cpyrt::PyStrings::gBases = nullptr; -PyObject* cpyrt::PyStrings::gBase = nullptr; -PyObject* cpyrt::PyStrings::gContains = nullptr; -PyObject* cpyrt::PyStrings::gCopy = nullptr; -PyObject* cpyrt::PyStrings::gCppBool = nullptr; -PyObject* cpyrt::PyStrings::gCppName = nullptr; -PyObject* cpyrt::PyStrings::gAnnotations = nullptr; -PyObject* cpyrt::PyStrings::gCastCpp = nullptr; -PyObject* cpyrt::PyStrings::gCType = nullptr; -PyObject* cpyrt::PyStrings::gDeref = nullptr; -PyObject* cpyrt::PyStrings::gPreInc = nullptr; -PyObject* cpyrt::PyStrings::gPostInc = nullptr; -PyObject* cpyrt::PyStrings::gDict = nullptr; -PyObject* cpyrt::PyStrings::gEmptyString = nullptr; -PyObject* cpyrt::PyStrings::gEq = nullptr; -PyObject* cpyrt::PyStrings::gFollow = nullptr; -PyObject* cpyrt::PyStrings::gHasValue = nullptr; -PyObject* cpyrt::PyStrings::gGetItem = nullptr; -PyObject* cpyrt::PyStrings::gGetNoCheck = nullptr; -PyObject* cpyrt::PyStrings::gSetItem = nullptr; -PyObject* cpyrt::PyStrings::gInit = nullptr; -PyObject* cpyrt::PyStrings::gIter = nullptr; -PyObject* cpyrt::PyStrings::gLen = nullptr; -PyObject* cpyrt::PyStrings::gLifeLine = nullptr; -PyObject* cpyrt::PyStrings::gModule = nullptr; -PyObject* cpyrt::PyStrings::gMRO = nullptr; -PyObject* cpyrt::PyStrings::gName = nullptr; -PyObject* cpyrt::PyStrings::gNe = nullptr; -PyObject* cpyrt::PyStrings::gRepr = nullptr; -PyObject* cpyrt::PyStrings::gCppRepr = nullptr; -PyObject* cpyrt::PyStrings::gStr = nullptr; -PyObject* cpyrt::PyStrings::gCppStr = nullptr; -PyObject* cpyrt::PyStrings::gTypeCode = nullptr; -PyObject* cpyrt::PyStrings::gCTypesType = nullptr; - -PyObject* cpyrt::PyStrings::gUnderlying = nullptr; -PyObject* cpyrt::PyStrings::gRealInit = nullptr; - -PyObject* cpyrt::PyStrings::gAdd = nullptr; -PyObject* cpyrt::PyStrings::gSub = nullptr; -PyObject* cpyrt::PyStrings::gMul = nullptr; -PyObject* cpyrt::PyStrings::gDiv = nullptr; - -PyObject* cpyrt::PyStrings::gLShift = nullptr; -PyObject* cpyrt::PyStrings::gLShiftC = nullptr; - -PyObject* cpyrt::PyStrings::gAt = nullptr; -PyObject* cpyrt::PyStrings::gBegin = nullptr; -PyObject* cpyrt::PyStrings::gEnd = nullptr; -PyObject* cpyrt::PyStrings::gFirst = nullptr; -PyObject* cpyrt::PyStrings::gSecond = nullptr; -PyObject* cpyrt::PyStrings::gSize = nullptr; -PyObject* cpyrt::PyStrings::gTemplate = nullptr; -PyObject* cpyrt::PyStrings::gVectorAt = nullptr; -PyObject* cpyrt::PyStrings::gInsert = nullptr; -PyObject* cpyrt::PyStrings::gValueType = nullptr; -PyObject* cpyrt::PyStrings::gValueTypePtr = nullptr; -PyObject* cpyrt::PyStrings::gValueSize = nullptr; - -PyObject* cpyrt::PyStrings::gCppReal = nullptr; -PyObject* cpyrt::PyStrings::gCppImag = nullptr; - -PyObject* cpyrt::PyStrings::gThisModule = nullptr; - -PyObject* cpyrt::PyStrings::gDispInit = nullptr; -PyObject* cpyrt::PyStrings::gDispGet = nullptr; - -PyObject* cpyrt::PyStrings::gExPythonize = nullptr; -PyObject* cpyrt::PyStrings::gPythonize = nullptr; - -PyObject* cpyrt::PyStrings::gArray = nullptr; -PyObject* cpyrt::PyStrings::gDType = nullptr; -PyObject* cpyrt::PyStrings::gFromBuffer = nullptr; +#define CPYRT_DEFINE_PYSTRING(var, str) \ + PyObject* cpyrt::PyStrings::var = nullptr; +CPYRT_PYSTRINGS(CPYRT_DEFINE_PYSTRING) +#undef CPYRT_DEFINE_PYSTRING //----------------------------------------------------------------------------- -#define CPPJIT_INITIALIZE_STRING(var, str) \ - if (!(PyStrings::var = PyUnicode_InternFromString((char*)#str))) \ - return false - bool cpyrt::CreatePyStrings() { // Build cache of commonly used python strings (the cache is python intern, so // all strings are shared python-wide, not just in cppjit). - CPPJIT_INITIALIZE_STRING(gAssign, __assign__); - CPPJIT_INITIALIZE_STRING(gBases, __bases__); - CPPJIT_INITIALIZE_STRING(gBase, __base__); - CPPJIT_INITIALIZE_STRING(gContains, contains); - CPPJIT_INITIALIZE_STRING(gCopy, copy); - CPPJIT_INITIALIZE_STRING(gCppBool, __cpp_bool__); - CPPJIT_INITIALIZE_STRING(gCppName, __cpp_name__); - CPPJIT_INITIALIZE_STRING(gAnnotations, __annotations__); - CPPJIT_INITIALIZE_STRING(gCastCpp, __cast_cpp__); - CPPJIT_INITIALIZE_STRING(gCType, __ctype__); - CPPJIT_INITIALIZE_STRING(gDeref, __deref__); - CPPJIT_INITIALIZE_STRING(gPreInc, __preinc__); - CPPJIT_INITIALIZE_STRING(gPostInc, __postinc__); - CPPJIT_INITIALIZE_STRING(gDict, __dict__); - if (!(PyStrings::gEmptyString = PyUnicode_FromString((char*)""))) +#define CPYRT_CREATE_PYSTRING(var, str) \ + if (!(PyStrings::var = PyUnicode_InternFromString(str))) \ return false; - CPPJIT_INITIALIZE_STRING(gEq, __eq__); - CPPJIT_INITIALIZE_STRING(gFollow, __follow__); - CPPJIT_INITIALIZE_STRING(gHasValue, has_value); - CPPJIT_INITIALIZE_STRING(gGetItem, __getitem__); - CPPJIT_INITIALIZE_STRING(gGetNoCheck, _getitem__unchecked); - CPPJIT_INITIALIZE_STRING(gSetItem, __setitem__); - CPPJIT_INITIALIZE_STRING(gInit, __init__); - CPPJIT_INITIALIZE_STRING(gIter, __iter__); - CPPJIT_INITIALIZE_STRING(gLen, __len__); - CPPJIT_INITIALIZE_STRING(gLifeLine, __lifeline); - CPPJIT_INITIALIZE_STRING(gModule, __module__); - CPPJIT_INITIALIZE_STRING(gMRO, __mro__); - CPPJIT_INITIALIZE_STRING(gName, __name__); - CPPJIT_INITIALIZE_STRING(gNe, __ne__); - CPPJIT_INITIALIZE_STRING(gRepr, __repr__); - CPPJIT_INITIALIZE_STRING(gCppRepr, __cpp_repr); - CPPJIT_INITIALIZE_STRING(gStr, __str__); - CPPJIT_INITIALIZE_STRING(gCppStr, __cpp_str); - CPPJIT_INITIALIZE_STRING(gTypeCode, typecode); - CPPJIT_INITIALIZE_STRING(gCTypesType, _type_); - - CPPJIT_INITIALIZE_STRING(gUnderlying, __underlying); - CPPJIT_INITIALIZE_STRING(gRealInit, __real_init); - - CPPJIT_INITIALIZE_STRING(gAdd, __add__); - CPPJIT_INITIALIZE_STRING(gSub, __sub__); - CPPJIT_INITIALIZE_STRING(gMul, __mul__); - CPPJIT_INITIALIZE_STRING(gDiv, __truediv__); - - CPPJIT_INITIALIZE_STRING(gLShift, __lshift__); - CPPJIT_INITIALIZE_STRING(gLShiftC, __lshiftc__); - - CPPJIT_INITIALIZE_STRING(gAt, at); - CPPJIT_INITIALIZE_STRING(gBegin, begin); - CPPJIT_INITIALIZE_STRING(gEnd, end); - CPPJIT_INITIALIZE_STRING(gFirst, first); - CPPJIT_INITIALIZE_STRING(gSecond, second); - CPPJIT_INITIALIZE_STRING(gSize, size); - CPPJIT_INITIALIZE_STRING(gTemplate, Template); - CPPJIT_INITIALIZE_STRING(gVectorAt, _vector__at); - CPPJIT_INITIALIZE_STRING(gInsert, insert); - CPPJIT_INITIALIZE_STRING(gValueType, value_type); - CPPJIT_INITIALIZE_STRING(gValueTypePtr, _value_type); - CPPJIT_INITIALIZE_STRING(gValueSize, value_size); - - CPPJIT_INITIALIZE_STRING(gCppReal, __cpp_real); - CPPJIT_INITIALIZE_STRING(gCppImag, __cpp_imag); - - CPPJIT_INITIALIZE_STRING(gThisModule, cppjit); - - CPPJIT_INITIALIZE_STRING(gDispInit, _init_dispatchptr); - CPPJIT_INITIALIZE_STRING(gDispGet, _get_dispatch); - - CPPJIT_INITIALIZE_STRING(gExPythonize, __cppjit_explicit_pythonize__); - CPPJIT_INITIALIZE_STRING(gPythonize, __cppjit_pythonize__); - - CPPJIT_INITIALIZE_STRING(gArray, __array__); - CPPJIT_INITIALIZE_STRING(gDType, dtype); - CPPJIT_INITIALIZE_STRING(gFromBuffer, frombuffer); + CPYRT_PYSTRINGS(CPYRT_CREATE_PYSTRING) +#undef CPYRT_CREATE_PYSTRING return true; } @@ -170,133 +26,9 @@ bool cpyrt::CreatePyStrings() { //----------------------------------------------------------------------------- PyObject* cpyrt::DestroyPyStrings() { // Remove all cached python strings. - Py_DECREF(PyStrings::gBases); - PyStrings::gBases = nullptr; - Py_DECREF(PyStrings::gBase); - PyStrings::gBase = nullptr; - Py_DECREF(PyStrings::gContains); - PyStrings::gContains = nullptr; - Py_DECREF(PyStrings::gCopy); - PyStrings::gCopy = nullptr; - Py_DECREF(PyStrings::gCppBool); - PyStrings::gCppBool = nullptr; - Py_DECREF(PyStrings::gCppName); - PyStrings::gCppName = nullptr; - Py_DECREF(PyStrings::gAnnotations); - PyStrings::gAnnotations = nullptr; - Py_DECREF(PyStrings::gCType); - PyStrings::gCType = nullptr; - Py_DECREF(PyStrings::gDeref); - PyStrings::gDeref = nullptr; - Py_DECREF(PyStrings::gPreInc); - PyStrings::gPreInc = nullptr; - Py_DECREF(PyStrings::gPostInc); - PyStrings::gPostInc = nullptr; - Py_DECREF(PyStrings::gDict); - PyStrings::gDict = nullptr; - Py_DECREF(PyStrings::gEmptyString); - PyStrings::gEmptyString = nullptr; - Py_DECREF(PyStrings::gEq); - PyStrings::gEq = nullptr; - Py_DECREF(PyStrings::gFollow); - PyStrings::gFollow = nullptr; - Py_DECREF(PyStrings::gHasValue); - PyStrings::gHasValue = nullptr; - Py_DECREF(PyStrings::gGetItem); - PyStrings::gGetItem = nullptr; - Py_DECREF(PyStrings::gGetNoCheck); - PyStrings::gGetNoCheck = nullptr; - Py_DECREF(PyStrings::gSetItem); - PyStrings::gSetItem = nullptr; - Py_DECREF(PyStrings::gInit); - PyStrings::gInit = nullptr; - Py_DECREF(PyStrings::gIter); - PyStrings::gIter = nullptr; - Py_DECREF(PyStrings::gLen); - PyStrings::gLen = nullptr; - Py_DECREF(PyStrings::gLifeLine); - PyStrings::gLifeLine = nullptr; - Py_DECREF(PyStrings::gModule); - PyStrings::gModule = nullptr; - Py_DECREF(PyStrings::gMRO); - PyStrings::gMRO = nullptr; - Py_DECREF(PyStrings::gName); - PyStrings::gName = nullptr; - Py_DECREF(PyStrings::gNe); - PyStrings::gNe = nullptr; - Py_DECREF(PyStrings::gTypeCode); - PyStrings::gTypeCode = nullptr; - Py_DECREF(PyStrings::gCTypesType); - PyStrings::gCTypesType = nullptr; - - Py_DECREF(PyStrings::gUnderlying); - PyStrings::gUnderlying = nullptr; - Py_DECREF(PyStrings::gRealInit); - PyStrings::gRealInit = nullptr; - - Py_DECREF(PyStrings::gAdd); - PyStrings::gAdd = nullptr; - Py_DECREF(PyStrings::gSub); - PyStrings::gSub = nullptr; - Py_DECREF(PyStrings::gMul); - PyStrings::gMul = nullptr; - Py_DECREF(PyStrings::gDiv); - PyStrings::gDiv = nullptr; - - Py_DECREF(PyStrings::gLShift); - PyStrings::gLShift = nullptr; - Py_DECREF(PyStrings::gLShiftC); - PyStrings::gLShiftC = nullptr; - - Py_DECREF(PyStrings::gAt); - PyStrings::gAt = nullptr; - Py_DECREF(PyStrings::gBegin); - PyStrings::gBegin = nullptr; - Py_DECREF(PyStrings::gEnd); - PyStrings::gEnd = nullptr; - Py_DECREF(PyStrings::gFirst); - PyStrings::gFirst = nullptr; - Py_DECREF(PyStrings::gSecond); - PyStrings::gSecond = nullptr; - Py_DECREF(PyStrings::gSize); - PyStrings::gSize = nullptr; - Py_DECREF(PyStrings::gTemplate); - PyStrings::gTemplate = nullptr; - Py_DECREF(PyStrings::gVectorAt); - PyStrings::gVectorAt = nullptr; - Py_DECREF(PyStrings::gInsert); - PyStrings::gInsert = nullptr; - Py_DECREF(PyStrings::gValueType); - PyStrings::gValueType = nullptr; - Py_DECREF(PyStrings::gValueTypePtr); - PyStrings::gValueTypePtr = nullptr; - Py_DECREF(PyStrings::gValueSize); - PyStrings::gValueSize = nullptr; - - Py_DECREF(PyStrings::gCppReal); - PyStrings::gCppReal = nullptr; - Py_DECREF(PyStrings::gCppImag); - PyStrings::gCppImag = nullptr; - - Py_DECREF(PyStrings::gThisModule); - PyStrings::gThisModule = nullptr; - - Py_DECREF(PyStrings::gDispInit); - PyStrings::gDispInit = nullptr; - Py_DECREF(PyStrings::gDispGet); - PyStrings::gDispGet = nullptr; - - Py_DECREF(PyStrings::gExPythonize); - PyStrings::gExPythonize = nullptr; - Py_DECREF(PyStrings::gPythonize); - PyStrings::gPythonize = nullptr; - - Py_DECREF(PyStrings::gArray); - PyStrings::gArray = nullptr; - Py_DECREF(PyStrings::gDType); - PyStrings::gDType = nullptr; - Py_DECREF(PyStrings::gFromBuffer); - PyStrings::gFromBuffer = nullptr; +#define CPYRT_DESTROY_PYSTRING(var, str) Py_CLEAR(PyStrings::var); + CPYRT_PYSTRINGS(CPYRT_DESTROY_PYSTRING) +#undef CPYRT_DESTROY_PYSTRING Py_RETURN_NONE; } diff --git a/bindings/pyroot/cppjit/src/cpyrt/PyStrings.h b/bindings/pyroot/cppjit/src/cpyrt/PyStrings.h index 64724f8b2420c..6f0d3c8dc6f9d 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/PyStrings.h +++ b/bindings/pyroot/cppjit/src/cpyrt/PyStrings.h @@ -5,82 +5,89 @@ namespace cppjit::cpyrt { // python strings kept for performance reasons -namespace PyStrings { - -extern PyObject* gAssign; -extern PyObject* gBases; -extern PyObject* gBase; -extern PyObject* gContains; -extern PyObject* gCopy; -extern PyObject* gCppBool; -extern PyObject* gCppName; -extern PyObject* gAnnotations; -extern PyObject* gCastCpp; -extern PyObject* gCType; -extern PyObject* gDeref; -extern PyObject* gPreInc; -extern PyObject* gPostInc; -extern PyObject* gDict; -extern PyObject* gEmptyString; -extern PyObject* gEq; -extern PyObject* gFollow; -extern PyObject* gHasValue; -extern PyObject* gGetItem; -extern PyObject* gGetNoCheck; -extern PyObject* gSetItem; -extern PyObject* gInit; -extern PyObject* gIter; -extern PyObject* gLen; -extern PyObject* gLifeLine; -extern PyObject* gModule; -extern PyObject* gMRO; -extern PyObject* gName; -extern PyObject* gNe; -extern PyObject* gRepr; -extern PyObject* gCppRepr; -extern PyObject* gStr; -extern PyObject* gCppStr; -extern PyObject* gTypeCode; -extern PyObject* gCTypesType; - -extern PyObject* gUnderlying; -extern PyObject* gRealInit; - -extern PyObject* gAdd; -extern PyObject* gSub; -extern PyObject* gMul; -extern PyObject* gDiv; - -extern PyObject* gLShift; -extern PyObject* gLShiftC; +// X(variable, string): each entry declares PyStrings::variable, holding the +// interned python string for "string" +#define CPYRT_PYSTRINGS(X) \ + X(gAssign, "__assign__") \ + X(gBases, "__bases__") \ + X(gBase, "__base__") \ + X(gContains, "contains") \ + X(gCopy, "copy") \ + X(gCppBool, "__cpp_bool__") \ + X(gCppName, "__cpp_name__") \ + X(gAnnotations, "__annotations__") \ + X(gCastCpp, "__cast_cpp__") \ + X(gCType, "__ctype__") \ + X(gDeref, "__deref__") \ + X(gPreInc, "__preinc__") \ + X(gPostInc, "__postinc__") \ + X(gDict, "__dict__") \ + X(gEmptyString, "") \ + X(gEq, "__eq__") \ + X(gFollow, "__follow__") \ + X(gHasValue, "has_value") \ + X(gGetItem, "__getitem__") \ + X(gGetNoCheck, "_getitem__unchecked") \ + X(gSetItem, "__setitem__") \ + X(gInit, "__init__") \ + X(gIter, "__iter__") \ + X(gLen, "__len__") \ + X(gLifeLine, "__lifeline") \ + X(gModule, "__module__") \ + X(gMRO, "__mro__") \ + X(gName, "__name__") \ + X(gNe, "__ne__") \ + X(gRepr, "__repr__") \ + X(gCppRepr, "__cpp_repr") \ + X(gStr, "__str__") \ + X(gCppStr, "__cpp_str") \ + X(gTypeCode, "typecode") \ + X(gCTypesType, "_type_") \ + \ + X(gUnderlying, "__underlying") \ + X(gRealInit, "__real_init") \ + \ + X(gAdd, "__add__") \ + X(gSub, "__sub__") \ + X(gMul, "__mul__") \ + X(gDiv, "__truediv__") \ + \ + X(gLShift, "__lshift__") \ + X(gLShiftC, "__lshiftc__") \ + \ + X(gAt, "at") \ + X(gBegin, "begin") \ + X(gEnd, "end") \ + X(gFirst, "first") \ + X(gSecond, "second") \ + X(gSize, "size") \ + X(gTemplate, "Template") \ + X(gVectorAt, "_vector__at") \ + X(gInsert, "insert") \ + X(gValueType, "value_type") \ + X(gValueTypePtr, "_value_type") \ + X(gValueSize, "value_size") \ + \ + X(gCppReal, "__cpp_real") \ + X(gCppImag, "__cpp_imag") \ + \ + X(gThisModule, "cppjit") \ + \ + X(gDispInit, "_init_dispatchptr") \ + X(gDispGet, "_get_dispatch") \ + \ + X(gExPythonize, "__cppjit_explicit_pythonize__") \ + X(gPythonize, "__cppjit_pythonize__") \ + \ + X(gArray, "__array__") \ + X(gDType, "dtype") \ + X(gFromBuffer, "frombuffer") -extern PyObject* gAt; -extern PyObject* gBegin; -extern PyObject* gEnd; -extern PyObject* gFirst; -extern PyObject* gSecond; -extern PyObject* gSize; -extern PyObject* gTemplate; -extern PyObject* gVectorAt; -extern PyObject* gInsert; -extern PyObject* gValueType; -extern PyObject* gValueTypePtr; -extern PyObject* gValueSize; - -extern PyObject* gCppReal; -extern PyObject* gCppImag; - -extern PyObject* gThisModule; - -extern PyObject* gDispInit; -extern PyObject* gDispGet; - -extern PyObject* gExPythonize; -extern PyObject* gPythonize; +namespace PyStrings { -extern PyObject* gArray; -extern PyObject* gDType; -extern PyObject* gFromBuffer; +#define CPYRT_DECLARE_PYSTRING(var, str) extern PyObject* var; +CPYRT_PYSTRINGS(CPYRT_DECLARE_PYSTRING) +#undef CPYRT_DECLARE_PYSTRING } // namespace PyStrings From 56e184f271d2ddb285d588b9f39854f3984bdeda Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Fri, 9 Oct 2026 15:21:53 +0200 Subject: [PATCH 05/71] [cppjit] [cpyrt] Remove RegisterConverterAlias() and RegisterExecutorAlias() (compiler-research/cppjit#147) ROOT used them to give Float16_t and Double32_t the converters and executors of float and double. cppjit resolves these typedefs through CppInterOp by itself, and ROOT stopped registering the aliases, so the functions have no users left. These functions were only introduced to cppyy in 2024 and were not documented: https://github.com/wlav/CPyCppyy/pull/27 (cherry picked from commit edb749b) --- bindings/pyroot/cppjit/src/cpyrt/API.h | 8 -------- bindings/pyroot/cppjit/src/cpyrt/Converters.cxx | 17 ----------------- bindings/pyroot/cppjit/src/cpyrt/Converters.h | 2 -- bindings/pyroot/cppjit/src/cpyrt/Executors.cxx | 17 ----------------- bindings/pyroot/cppjit/src/cpyrt/Executors.h | 2 -- 5 files changed, 46 deletions(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/API.h b/bindings/pyroot/cppjit/src/cpyrt/API.h index 627132a2e265b..083b5d1fa5c63 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/API.h +++ b/bindings/pyroot/cppjit/src/cpyrt/API.h @@ -145,10 +145,6 @@ typedef Converter* (*ConverterFactory_t)(cdims_t); CPYRT_EXTERN bool RegisterConverter(const std::string& name, ConverterFactory_t); -// register a custom converter that is a reference to an existing converter -CPYRT_EXTERN bool RegisterConverterAlias(const std::string& name, - const std::string& target); - // remove a custom converter CPYRT_EXTERN bool UnregisterConverter(const std::string& name); @@ -176,10 +172,6 @@ CPYRT_EXTERN void DestroyConverter(Converter* p); typedef Executor* (*ExecutorFactory_t)(cdims_t); CPYRT_EXTERN bool RegisterExecutor(const std::string& name, ExecutorFactory_t); -// register a custom executor that is a reference to an existing converter -CPYRT_EXTERN bool RegisterExecutorAlias(const std::string& name, - const std::string& target); - // remove a custom executor CPYRT_EXTERN bool UnregisterExecutor(const std::string& name); diff --git a/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx b/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx index c21d00ef182e9..85b9e9486c926 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx @@ -3985,23 +3985,6 @@ bool cpyrt::RegisterConverter(const std::string& name, cf_t fac) { return true; } -//---------------------------------------------------------------------------- -CPYRT_EXPORT -bool cpyrt::RegisterConverterAlias(const std::string& name, - const std::string& target) { - // register a custom converter that is a reference to an existing converter - auto f = gConvFactories.find(name); - if (f != gConvFactories.end()) - return false; - - auto t = gConvFactories.find(target); - if (t == gConvFactories.end()) - return false; - - gConvFactories[name] = t->second; - return true; -} - //---------------------------------------------------------------------------- CPYRT_EXPORT bool cpyrt::UnregisterConverter(const std::string& name) { diff --git a/bindings/pyroot/cppjit/src/cpyrt/Converters.h b/bindings/pyroot/cppjit/src/cpyrt/Converters.h index bc2f65d61e20d..d208464825606 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Converters.h +++ b/bindings/pyroot/cppjit/src/cpyrt/Converters.h @@ -40,8 +40,6 @@ CPYRT_EXPORT Converter* CreateConverter(interop::TCppType_t type, CPYRT_EXPORT void DestroyConverter(Converter* p); typedef Converter* (*cf_t)(cdims_t d); CPYRT_EXPORT bool RegisterConverter(const std::string& name, cf_t fac); -CPYRT_EXPORT bool RegisterConverterAlias(const std::string& name, - const std::string& target); CPYRT_EXPORT bool UnregisterConverter(const std::string& name); // converters for special cases (only here b/c of external use of diff --git a/bindings/pyroot/cppjit/src/cpyrt/Executors.cxx b/bindings/pyroot/cppjit/src/cpyrt/Executors.cxx index 6fc105e417f9f..14afe0d401799 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Executors.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Executors.cxx @@ -1134,23 +1134,6 @@ bool cpyrt::RegisterExecutor(const std::string& name, ef_t fac) { return true; } -//---------------------------------------------------------------------------- -CPYRT_EXPORT -bool cpyrt::RegisterExecutorAlias(const std::string& name, - const std::string& target) { - // register a custom executor that is a reference to an existing converter - auto f = gExecFactories.find(name); - if (f != gExecFactories.end()) - return false; - - auto t = gExecFactories.find(target); - if (t == gExecFactories.end()) - return false; - - gExecFactories[name] = t->second; - return true; -} - //---------------------------------------------------------------------------- CPYRT_EXPORT bool cpyrt::UnregisterExecutor(const std::string& name) { diff --git a/bindings/pyroot/cppjit/src/cpyrt/Executors.h b/bindings/pyroot/cppjit/src/cpyrt/Executors.h index c8b0702b5fee7..959afd1775c86 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Executors.h +++ b/bindings/pyroot/cppjit/src/cpyrt/Executors.h @@ -39,8 +39,6 @@ CPYRT_EXPORT Executor* CreateExecutor(interop::TCppType_t type, cdims_t = 0); CPYRT_EXPORT void DestroyExecutor(Executor* p); typedef Executor* (*ef_t)(cdims_t); CPYRT_EXPORT bool RegisterExecutor(const std::string& name, ef_t fac); -CPYRT_EXPORT bool RegisterExecutorAlias(const std::string& name, - const std::string& target); CPYRT_EXPORT bool UnregisterExecutor(const std::string& name); // helper for the actual call From 0b24f980396df0168ab26fc41bfc78ac11f70835 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Fri, 9 Oct 2026 15:21:56 +0200 Subject: [PATCH 06/71] [cppjit] Check for smart pointers by class instead of by name (compiler-research/cppjit#143) GetSmartPtrInfo() took a type name and resolved it with ResolveName(), which declares a __typeof__ alias for every name it has not seen yet. It is called when creating the converter of each class-type argument, so the first call of a method of a new class template instance, e.g. the constructor of std::set with its std::less and std::allocator arguments, declared several such aliases. All callers already have the class, so pass it instead and compare its name against the known smart pointer templates. The alias declaration also silently attempted to instantiate class template specializations, so a specialization that can't be defined (of a template that is only declared) was never instantiated again. Without it, Cpp::GetNumBases() is the first to try when the Python class is created, and it prints an "explicit instantiation of undefined template" error. Check with Cpp::IsComplete() first, which instantiates without diagnostics. For example: ```python import time import cppjit cppjit.cppdef("#include \n") types = ["int", "long", "short", "char", "float", "double", "unsigned int", "unsigned long"] classes = [cppjit.gbl.std.set[t] for t in types] start = time.perf_counter() for cls in classes: cls() # first construction of each class print(f"first construction of {len(classes)} std::set classes: {1000 * (time.perf_counter() - start):.0f} ms") ``` This takes about 444 ms instead of 461 ms (median of 3 trials, Linux x86_64, Python 3.14, CppInterOp/LLVM 22.1.8). The savings grow with the number of distinct template instantiations whose methods are first called; the interpreter-state growth from the dropped alias declarations matters beyond the reruns. (cherry picked from commit f7e785f) --- bindings/pyroot/cppjit/src/cpyrt/CPPScope.cxx | 7 +------ bindings/pyroot/cppjit/src/cpyrt/Converters.cxx | 4 ++-- .../pyroot/cppjit/src/interop/cppjit_interop.h | 2 +- .../pyroot/cppjit/src/interop/interop_wrapper.cxx | 14 ++++++-------- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPScope.cxx b/bindings/pyroot/cppjit/src/cpyrt/CPPScope.cxx index 9f695789b6683..dd5801abed713 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPScope.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPScope.cxx @@ -207,16 +207,11 @@ static PyObject* pt_new(PyTypeObject* subtype, PyObject* args, PyObject* kwds) { // creation of the python-side class; extend the size if this is a smart ptr // // A namespace can never be a smart pointer, so skip the check for namespaces. - // This matters beyond performance: GetSmartPtrInfo() resolves the scope name - // through a slow interpreter lookup that triggers autoloading of the library - // providing the scope, which we don't want to do unnecessarily. interop::TCppScope_t raw; interop::TCppMethod_t deref; if (CPPScope_CheckExact(subtype) && !interop::IsNamespace(((CPPScope*)subtype)->fCppType)) { - if (interop::GetSmartPtrInfo( - interop::GetScopedFinalName(((CPPScope*)subtype)->fCppType), &raw, - &deref)) + if (interop::GetSmartPtrInfo(((CPPScope*)subtype)->fCppType, &raw, &deref)) subtype->tp_basicsize = sizeof(CPPSmartClass); } diff --git a/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx b/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx index 85b9e9486c926..da661fa383c45 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx @@ -3639,7 +3639,7 @@ cppjit::cpyrt::CreateConverter(const std::string& fullType, cdims_t dims) { Converter* result = nullptr; if (interop::TCppScope_t klass = interop::GetFullScope(realType)) { interop::TCppScope_t raw; - if (interop::GetSmartPtrInfo(realType, &raw, nullptr)) { + if (interop::GetSmartPtrInfo(klass, &raw, nullptr)) { if (cpd == "") { result = new SmartPtrConverter(klass, raw, control); } else if (cpd == "&") { @@ -3902,7 +3902,7 @@ cppjit::cpyrt::CreateConverter(interop::TCppType_t type, cdims_t dims) { (klass || (klass = interop::GetFullScope(realTypeStr)))) { // std::byte is a special enum class used to access raw memory interop::TCppScope_t raw; - if (interop::GetSmartPtrInfo(realTypeStr, &raw, nullptr)) { + if (interop::GetSmartPtrInfo(klass, &raw, nullptr)) { if (cpd == "") { result = new SmartPtrConverter(klass, raw, control); } else if (cpd == "&") { diff --git a/bindings/pyroot/cppjit/src/interop/cppjit_interop.h b/bindings/pyroot/cppjit/src/interop/cppjit_interop.h index db2ccf0b51dc4..763de03b45fef 100644 --- a/bindings/pyroot/cppjit/src/interop/cppjit_interop.h +++ b/bindings/pyroot/cppjit/src/interop/cppjit_interop.h @@ -248,7 +248,7 @@ bool IsSubclass(TCppScope_t derived, TCppScope_t base); RPY_EXPORTED bool IsSmartPtr(TCppScope_t klass); RPY_EXPORTED -bool GetSmartPtrInfo(const std::string&, TCppScope_t* raw, TCppMethod_t* deref); +bool GetSmartPtrInfo(TCppScope_t klass, TCppScope_t* raw, TCppMethod_t* deref); // calculate offsets between declared and actual type, up-cast: direction > 0; // down-cast: direction < 0 RPY_EXPORTED diff --git a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx index ef48f950e50d2..0add59261d711 100644 --- a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx +++ b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx @@ -1411,23 +1411,21 @@ bool interop::IsSmartPtr(TCppScope_t klass) { return false; } -bool interop::GetSmartPtrInfo(const std::string& tname, TCppScope_t* raw, +bool interop::GetSmartPtrInfo(TCppScope_t klass, TCppScope_t* raw, TCppMethod_t* deref) { - // TODO: We can directly accept scope instead of name - const std::string& rn = ResolveName(tname); - if (gSmartPtrTypes.find(rn.substr(0, rn.find("<"))) == gSmartPtrTypes.end()) + TCppScope_t scope = interop::GetUnderlyingScope(klass); + if (!scope || !interop::IsSmartPtr(scope)) return false; if (!raw && !deref) return true; - TCppScope_t scope = interop::GetScope(rn); - if (!scope) - return false; - std::vector ops; { std::lock_guard Lock(InterOpMutex); + // the operator can only be found once the specialization is instantiated + if (!Cpp::IsComplete(scope)) + return false; Cpp::GetOperator(scope, Cpp::Operator::OP_Arrow, ops, /*kind=*/Cpp::OperatorArity::kBoth); if (ops.size() != 1) From a41f6638a262c4780d8c7009991eae86d921613e Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Fri, 9 Oct 2026 19:37:35 +0200 Subject: [PATCH 07/71] [cppjit] [cpyrt] Fix leaked container reference in vector_iter (compiler-research/cppjit#148) vector_iter() took two references to the container, one when storing it and one after deciding whether the container is a temporary, while the iterator's dealloc releases only one. Every iteration over a `std::vector` proxy therefore leaked a reference to it: the proxy, and the C++ object if Python owns it, never got freed. ROOT's own cppyy copy got this right in root-project/root#19017 (commit 700fee6e7b6c), which moved the temporary check before the increment so that PyUnstable_Object_IsUniqueReferencedTemporary() sees the untouched refcount on Python 3.14, and dropped the original increment. The port of that change to the compiler-research fork, compiler-research/CPyCppyy#151 (commit compiler-research/CPyCppyy@8eba11b58c, reapplied as compiler-research/CPyCppyy@7e086216a3), added the new increment after the check but kept the original one. cppjit inherited both in its initial import (compiler-research/cppjit@38b22b99be), and ROOT with the vendoring in 552d291c328. Upstream cppjit still has both. Beyond the memory, the leak made ROOT's pyroot-pyz-ttree-branch flaky: the proxies leaked by the earlier read test stayed registered in the memory regulator under addresses the closed file had freed, so when a later `new std::vector` landed on one of them, reading the pointer data member returned the stale proxy instead of a reference proxy, and TTree::Branch() got bound to the proxy's own pointer slot. (cherry picked from commit 3df629f) --- bindings/pyroot/cppjit/src/cpyrt/Pythonize.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/Pythonize.cxx b/bindings/pyroot/cppjit/src/cpyrt/Pythonize.cxx index 5781345db3fdd..02ad193ae39a8 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Pythonize.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Pythonize.cxx @@ -686,10 +686,10 @@ static PyObject* vector_iter(PyObject* v) { if (!vi) return nullptr; - Py_INCREF(v); vi->ii_container = v; // tell the iterator code to set a life line if this container is a temporary + // (checked before taking the iterator's own reference) vi->vi_flags = vectoriterobject::kDefault; #if PY_VERSION_HEX >= 0x030e0000 if (PyUnstable_Object_IsUniqueReferencedTemporary(v) || From 7158e0b45f0ef63d84c31235999497d37ffac48a Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Thu, 27 Aug 2026 17:06:18 +0200 Subject: [PATCH 08/71] [cppjit] Adopt Clang-driven overload resolution from CppInterOp (cherry picked from commit c866488) --- bindings/pyroot/cppjit/CMakeLists.txt | 2 + .../cppjit/python/cppjit/_cpython_cppjit.py | 4 + .../pyroot/cppjit/src/cpyrt/CPPExcInstance.h | 3 + .../pyroot/cppjit/src/cpyrt/CPPInstance.cxx | 54 +-- .../pyroot/cppjit/src/cpyrt/CPPInstance.h | 47 +++ .../pyroot/cppjit/src/cpyrt/CPPMethod.cxx | 138 ------ bindings/pyroot/cppjit/src/cpyrt/CPPMethod.h | 11 +- .../pyroot/cppjit/src/cpyrt/CPPOverload.cxx | 351 +++++++++++----- .../pyroot/cppjit/src/cpyrt/CPPOverload.h | 74 ++-- bindings/pyroot/cppjit/src/cpyrt/CPPScope.cxx | 11 +- bindings/pyroot/cppjit/src/cpyrt/CPPScope.h | 2 + .../pyroot/cppjit/src/cpyrt/CallContext.h | 27 +- .../pyroot/cppjit/src/cpyrt/Converters.cxx | 13 +- .../pyroot/cppjit/src/cpyrt/Dispatcher.cxx | 1 + .../pyroot/cppjit/src/cpyrt/LowLevelViews.cxx | 1 + .../pyroot/cppjit/src/cpyrt/LowLevelViews.h | 1 + .../pyroot/cppjit/src/cpyrt/ProxyWrappers.cxx | 15 +- bindings/pyroot/cppjit/src/cpyrt/PyCallable.h | 8 +- .../pyroot/cppjit/src/cpyrt/TemplateProxy.cxx | 240 ++++------- .../pyroot/cppjit/src/cpyrt/TemplateProxy.h | 20 +- bindings/pyroot/cppjit/src/cpyrt/Utility.cxx | 204 ++++++++- bindings/pyroot/cppjit/src/cpyrt/Utility.h | 5 +- .../pyroot/cppjit/src/cpyrt/cpyrtModule.cxx | 13 +- .../cppjit/src/interop/cppjit_interop.h | 24 +- .../cppjit/src/interop/interop_wrapper.cxx | 393 +++++++++++++++--- .../pyroot/cppjit/test/test_advancedcpp.py | 4 + .../pyroot/cppjit/test/test_conversions.py | 7 +- .../pyroot/cppjit/test/test_cpp11features.py | 4 +- .../cppjit/test/test_crossinheritance.py | 18 +- bindings/pyroot/cppjit/test/test_datatypes.py | 20 +- .../pyroot/cppjit/test/test_doc_features.py | 2 +- bindings/pyroot/cppjit/test/test_fragile.py | 30 +- bindings/pyroot/cppjit/test/test_leakcheck.py | 2 +- bindings/pyroot/cppjit/test/test_overloads.py | 12 +- bindings/pyroot/cppjit/test/test_pythonify.py | 4 +- .../pyroot/cppjit/test/test_regression.py | 31 +- bindings/pyroot/cppjit/test/test_stltypes.py | 15 +- bindings/pyroot/cppjit/test/test_templates.py | 14 +- 38 files changed, 1177 insertions(+), 648 deletions(-) diff --git a/bindings/pyroot/cppjit/CMakeLists.txt b/bindings/pyroot/cppjit/CMakeLists.txt index 79828f6e307c8..f4db194c69631 100644 --- a/bindings/pyroot/cppjit/CMakeLists.txt +++ b/bindings/pyroot/cppjit/CMakeLists.txt @@ -10,6 +10,8 @@ include(GNUInstallDirs) # This option won't make a lot of sense since we only ship the shared library in site-packages # Perhaps this should permanently be OFF and users can build their own CppInterOp if they want to run the tests? option(CPPJIT_ENABLE_CPPINTEROP_TESTS "enable CppInterOp tests" OFF) +# Temporary pin to the overload-resolution development branch so CI builds +# and tests it. Returns to a pinned commit once that branch lands. set(CPPINTEROP_GIT_REPOSITORY "https://github.com/compiler-research/CppInterOp.git" CACHE STRING "") set(CPPINTEROP_GIT_TAG "c8126249dd18363000b89333b34313ca2a83fb82" CACHE STRING "") set(CPPINTEROP_SOURCE_DIR "" CACHE PATH diff --git a/bindings/pyroot/cppjit/python/cppjit/_cpython_cppjit.py b/bindings/pyroot/cppjit/python/cppjit/_cpython_cppjit.py index f41de438c0367..93ddb21ac24a6 100644 --- a/bindings/pyroot/cppjit/python/cppjit/_cpython_cppjit.py +++ b/bindings/pyroot/cppjit/python/cppjit/_cpython_cppjit.py @@ -25,6 +25,8 @@ "_end_capture_stderr", "_lock_interpreter", "_unlock_interpreter", + "OverloadResolutionException", + "OverloadAmbiguityException", ] @@ -207,6 +209,8 @@ def _add_default_paths(): bind_object = _backend.bind_object nullptr = _backend.nullptr default = _backend.default +OverloadResolutionException = _backend.OverloadResolutionException +OverloadAmbiguityException = _backend.OverloadAmbiguityException def load_reflection_info(name): diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPExcInstance.h b/bindings/pyroot/cppjit/src/cpyrt/CPPExcInstance.h index 35e4c9fb624d7..6a52a0e8dfe22 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPExcInstance.h +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPExcInstance.h @@ -1,6 +1,9 @@ #ifndef CPYRT_CPPEXCINSTANCE_H #define CPYRT_CPPEXCINSTANCE_H +#include "Python.h" +#include "cpyrt/CommonDefs.h" + ////////////////////////////////////////////////////////////////////////////// // // // cppjit::cpyrt::CPPExceptionInstance // diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPInstance.cxx b/bindings/pyroot/cppjit/src/cpyrt/CPPInstance.cxx index 13ccf3a7b02d5..b2220b7d46ccc 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPInstance.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPInstance.cxx @@ -42,51 +42,6 @@ extern PyObject* gNullPtrObject; // pointer to the smart pointer. They carry a pointer to the Python-sode smart // class for dereferencing to get to the actual instance pointer. -//- private helpers ---------------------------------------------------------- -namespace { - -// Several specific use cases require extra data in a CPPInstance, but can not -// be a new type. E.g. cross-inheritance derived types are by definition added -// a posterio, and caching of datamembers is up to the datamember, not the -// instance type. To not have normal use of CPPInstance take extra memory, this -// extended data can slot in place of fObject for those use cases. - -struct ExtendedData { - ExtendedData() - : fObject(nullptr), fSmartClass(nullptr), fDispatchPtr(nullptr), - fArraySize(0) {} - ~ExtendedData() { - for (auto& pc : fDatamemberCache) - Py_XDECREF(pc.second); - fDatamemberCache.clear(); - } - - // the original object reference it replaces (Note: has to be first data - // member, see usage in GetObjectRaw(), e.g. for ptr-ptr passing) - void* fObject; - - // for caching expensive-to-create data member representations - cpyrt::CI_DatamemberCache_t fDatamemberCache; - - // for smart pointer types - cpyrt::CPPSmartClass* fSmartClass; - - // for back-referencing from Python-derived instances - cpyrt::DispatchPtr* fDispatchPtr; - - // for representing T* as a low-level array - Py_ssize_t fArraySize; -}; - -} // unnamed namespace - -#define EXT_OBJECT(pyobj) ((ExtendedData*)((pyobj)->fObject))->fObject -#define DATA_CACHE(pyobj) ((ExtendedData*)((pyobj)->fObject))->fDatamemberCache -#define SMART_CLS(pyobj) ((ExtendedData*)((pyobj)->fObject))->fSmartClass -#define SMART_TYPE(pyobj) SMART_CLS(pyobj)->fCppType -#define DISPATCHPTR(pyobj) ((ExtendedData*)((pyobj)->fObject))->fDispatchPtr -#define ARRAY_SIZE(pyobj) ((ExtendedData*)((pyobj)->fObject))->fArraySize - inline void cpyrt::CPPInstance::CreateExtension() { if (fFlags & kIsExtended) return; @@ -549,7 +504,8 @@ static inline PyObject* eqneq_binop(CPPClass* klass, PyObject* self, const char* cppop = op == Py_EQ ? "==" : "!="; PyCallable* pyfunc = FindBinaryOperator(self, obj, cppop); if (pyfunc) - binop = (PyObject*)CPPOverload_New(cppop, pyfunc); + binop = (PyObject*)CPPOverload_New( + cppop, interop::GetParentScope(pyfunc->GetMethod().data), pyfunc); else { Py_INCREF(Py_None); binop = Py_None; @@ -613,7 +569,8 @@ static inline void* cast_actual(void* obj) { PyCallable* pyfunc = \ Utility::FindBinaryOperator((PyObject*)self, other, #op); \ if (pyfunc) \ - ometh = (PyObject*)CPPOverload_New(#label, pyfunc); \ + ometh = (PyObject*)CPPOverload_New( \ + #label, interop::GetParentScope(pyfunc->GetMethod().data), pyfunc); \ } \ meth = ometh; @@ -1005,7 +962,8 @@ static PyGetSetDef op_getset[] = { PyErr_Clear(); \ PyCallable* pyfunc = Utility::FindBinaryOperator(left, right, #op); \ if (pyfunc) \ - meth = (PyObject*)CPPOverload_New(#name, pyfunc); \ + meth = (PyObject*)CPPOverload_New( \ + #name, interop::GetParentScope(pyfunc->GetMethod().data), pyfunc); \ else { \ PyErr_SetString(PyExc_NotImplementedError, ""); \ return nullptr; \ diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPInstance.h b/bindings/pyroot/cppjit/src/cpyrt/CPPInstance.h index c94ecfa16c220..4aa06ee2b70ef 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPInstance.h +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPInstance.h @@ -17,6 +17,8 @@ #include "CommonDefs.h" #include "cppjit_interop.h" +#include "cpyrt/DispatchPtr.h" + // Standard #include #include @@ -146,4 +148,49 @@ void op_dealloc_nofree(CPPInstance*); } // namespace cppjit::cpyrt +//- private helpers ---------------------------------------------------------- +namespace { + +// Several specific use cases require extra data in a CPPInstance, but can not +// be a new type. E.g. cross-inheritance derived types are by definition added +// a posterio, and caching of datamembers is up to the datamember, not the +// instance type. To not have normal use of CPPInstance take extra memory, this +// extended data can slot in place of fObject for those use cases. + +struct ExtendedData { + ExtendedData() + : fObject(nullptr), fSmartClass(nullptr), fDispatchPtr(nullptr), + fArraySize(0) {} + ~ExtendedData() { + for (auto& pc : fDatamemberCache) + Py_XDECREF(pc.second); + fDatamemberCache.clear(); + } + + // the original object reference it replaces (Note: has to be first data + // member, see usage in GetObjectRaw(), e.g. for ptr-ptr passing) + void* fObject; + + // for caching expensive-to-create data member representations + cppjit::cpyrt::CI_DatamemberCache_t fDatamemberCache; + + // for smart pointer types + cppjit::cpyrt::CPPSmartClass* fSmartClass; + + // for back-referencing from Python-derived instances + cppjit::cpyrt::DispatchPtr* fDispatchPtr; + + // for representing T* as a low-level array + Py_ssize_t fArraySize; +}; + +} // unnamed namespace + +#define EXT_OBJECT(pyobj) ((ExtendedData*)((pyobj)->fObject))->fObject +#define DATA_CACHE(pyobj) ((ExtendedData*)((pyobj)->fObject))->fDatamemberCache +#define SMART_CLS(pyobj) ((ExtendedData*)((pyobj)->fObject))->fSmartClass +#define SMART_TYPE(pyobj) SMART_CLS(pyobj)->fCppType +#define DISPATCHPTR(pyobj) ((ExtendedData*)((pyobj)->fObject))->fDispatchPtr +#define ARRAY_SIZE(pyobj) ((ExtendedData*)((pyobj)->fObject))->fArraySize + #endif // !CPYRT_CPPINSTANCE_H diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPMethod.cxx b/bindings/pyroot/cppjit/src/cpyrt/CPPMethod.cxx index 0221f836748ac..2f38ae3194262 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPMethod.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPMethod.cxx @@ -446,144 +446,6 @@ PyObject* cpyrt::CPPMethod::Reflex(interop::Reflex::RequestId_t request, return PyCallable::Reflex(request, format); } -//---------------------------------------------------------------------------- -int cpyrt::CPPMethod::GetPriority() { - // To help with overload selection, methods are given a priority based on the - // affinity of Python and C++ types. Priority only matters for methods that - // have an equal number of arguments and types that are possible substitutes - // (the normal selection mechanisms would simply distinguish them otherwise). - - // The following types are ordered, in favor (variants implicit): - // - // bool >> long >> int >> short - // double >> long double >> float - // const char* >> char - // - // Further, all integer types are preferred over floating point b/c int to - // float is allowed implicitly, float to int is not. - // - // Special cases that are disliked include void* and unknown/incomplete types. - // Also, moves are preferred over references. std::initializer_list is not a - // nice conversion candidate either, but needs to be higher priority to mix - // well with implicit conversions. - // TODO: extend this to favour classes that are not bases. - // TODO: profile this method (it's expensive, but should be called too often) - - int priority = 0; - - const size_t nArgs = interop::GetMethodNumArgs(fMethod); - for (int iarg = 0; iarg < (int)nArgs; ++iarg) { - const std::string aname = interop::GetMethodArgTypeAsString(fMethod, iarg); - // FIXME: convert the string comparisons with comparison to the underlying - // type: - // interop::TCppType_t type = interop::GetMethodArgType(fMethod, iarg); - - // Not builtin and spelled "const void *", so match the compacted name. - std::string compact = aname; - compact.erase(std::remove(compact.begin(), compact.end(), ' '), - compact.end()); - - if (compact.find("void*") != std::string::npos) { - priority -= 1000; // void*/void** shouldn't be too greedy - } else if (interop::IsBuiltin(aname)) { - // complex type (note: double penalty: for complex and the template type) - if (strstr(aname.c_str(), "std::complex")) - priority -= 10; // prefer double, float, etc. over conversion - - // integer types - if (strstr(aname.c_str(), "bool")) - priority += 1; // bool over int (does accept 1 and 0) - else if (strstr(aname.c_str(), "long long")) - priority += -5; // will very likely fit - else if (strstr(aname.c_str(), "long")) - priority += -10; // most affine integer type - // no need to compare with int; leave at zero - else if (strstr(aname.c_str(), "short")) - priority += -50; // not really relevant as a type - - // floating point types (note all numbers lower than integer types) - else if (strstr(aname.c_str(), "float")) - priority += -100; // not really relevant as a type - else if (strstr(aname.c_str(), "long double")) - priority += -90; // fits double with least loss of precision - else if (strstr(aname.c_str(), "double")) - priority += -80; // most affine floating point type - - // string/char types - else if (strstr(aname.c_str(), "char") && aname[aname.size() - 1] != '*') - priority += -60; // prefer (const) char* over char - - } else { - // This is a user-defined type (class, struct, enum, etc.). - - // There's a bit of hysteresis here for templates: once GetScope() is - // called, their IsComplete() succeeds, the other way around it does not. - // Since GetPriority() is likely called several times in a sort, the - // GetScope() _must_ come first, or different GetPriority() calls may - // return different results (since the 2nd time, GetScope() will have been - // called from the first), killing the stable_sort. - - // prefer more derived classes - const std::string& clean_name = TypeManip::clean_type(aname, false); - interop::TCppScope_t scope = interop::GetScope(clean_name); - if (scope) - priority += static_cast(interop::GetNumBasesLongestBranch(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 - if (aname.find("initializer_list") != std::string::npos) { - priority += 150; // needed for proper implicit conversion rules - } else if (aname.rfind("&&", aname.size() - 2) != std::string::npos) { - priority += 100; // prefer moves over other ref/ptr - } else if (scope && !interop::IsComplete(scope)) { - // class is known, but no dictionary available, 2 more cases: * and & - if (aname[aname.size() - 1] == '&') - priority += -5000; - else - priority += -2000; // prefer pointer passing over reference - } - } - } - - // prefer methods w/o optional arguments b/c ones with optional arguments are - // easier to select by providing the optional arguments explicitly - priority += ((int)interop::GetMethodReqArgs(fMethod) - (int)nArgs); - - // add a small penalty to prefer non-const methods over const ones for - // get/setitem - if (interop::IsConstMethod(fMethod) && - interop::GetName(interop::TCppScope_t(fMethod.data)) == "operator[]") - priority += -10; - - // constructors are prefered - if (interop::IsConstructor(fMethod)) - priority += 100; - - return priority; -} - -//---------------------------------------------------------------------------- -bool cpyrt::CPPMethod::IsGreedy() { - // Methods will all void*-like arguments should be sorted after template - // instanstations, so that they don't greedily take over pointers to object. - // GetPriority() is too heavy-handed, as it will pull in all the argument - // types, so use this cheaper check. - const size_t nArgs = interop::GetMethodReqArgs(fMethod); - if (!nArgs) - return false; - - for (int iarg = 0; iarg < (int)nArgs; ++iarg) { - const std::string aname = interop::GetMethodArgTypeAsString(fMethod, iarg); - if (aname.find("void *") != 0) - return false; - } - return true; -} - //---------------------------------------------------------------------------- int cpyrt::CPPMethod::GetMaxArgs() { return (int)interop::GetMethodNumArgs(fMethod); diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPMethod.h b/bindings/pyroot/cppjit/src/cpyrt/CPPMethod.h index 41e9546b4c182..85f66c66f4955 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPMethod.h +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPMethod.h @@ -46,6 +46,7 @@ class CPPMethod : public PyCallable { virtual ~CPPMethod(); public: + interop::TCppMethod_t GetMethod() override { return fMethod; } PyObject* GetSignature(bool show_formalargs = true) override; PyObject* GetSignatureNames() override; PyObject* GetSignatureTypes() override; @@ -55,9 +56,6 @@ class CPPMethod : public PyCallable { Reflex(interop::Reflex::RequestId_t request, interop::Reflex::FormatId_t = interop::Reflex::OPTIMAL) override; - int GetPriority() override; - bool IsGreedy() override; - int GetMaxArgs() override; PyObject* GetCoVarNames() override; PyObject* GetArgDefault(int iarg, bool silent = true) override; @@ -75,6 +73,12 @@ class CPPMethod : public PyCallable { fn_type, cppjit::interop::GetTypeFromScope( cppjit::interop::TCppScope_t(fMethod.data))); } + // extra info methods + bool IsOperator() { return interop::IsOperator(fMethod.data); } + bool IsConversionOperator() { + return interop::IsConversionOperator(fMethod.data); + } + bool IsStaticMethod() { return interop::IsStaticMethod(fMethod); } public: PyObject* Call(CPPInstance*& self, cpyrt_PyArgs_t args, size_t nargsf, @@ -89,7 +93,6 @@ class CPPMethod : public PyCallable { CallContext* ctxt = nullptr); PyObject* Execute(void* self, ptrdiff_t offset, CallContext* ctxt = nullptr); - interop::TCppMethod_t GetMethod() { return fMethod; } interop::TCppScope_t GetScope() { return fScope; } Executor* GetExecutor() { return fExecutor; } std::string GetSignatureString(bool show_formalargs = true); diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx index 9601620559866..a80d0277a5dc7 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx @@ -3,10 +3,15 @@ using namespace cppjit; #include "CPPInstance.h" +#include "CPPMethod.h" #include "CPPOverload.h" #include "CallContext.h" +#include "MemoryRegulator.h" +#include "ProxyWrappers.h" +#include "PyCallable.h" #include "PyStrings.h" #include "Utility.h" +#include "cppjit_interop.h" #include "structmember.h" // from Python #include "cpyrt/Reflex.h" @@ -63,9 +68,6 @@ class TPythonCallback : public PyCallable { } } - int GetPriority() override { return 100; }; - bool IsGreedy() override { return false; }; - int GetMaxArgs() override { return 100; }; PyObject* GetCoVarNames() override { // TODO: pick these up from the callable Py_RETURN_NONE; @@ -124,12 +126,6 @@ static inline bool IsPseudoFunc(CPPOverload* pymeth) { return pymeth->fMethodInfo->fFlags & CallContext::kIsPseudoFunc; } -// helper to sort on method priority -static int PriorityCmp(const std::pair& left, - const std::pair& right) { - return left.first > right.first; -} - // return helper static inline void ResetCallState(CPPInstance* descr_self, CPPInstance*& im_self) { @@ -608,101 +604,267 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, ResetCallState(pymeth->fSelf, im_self); } - // ... otherwise loop over all methods and find the one that does not fail - if (!IsSorted(mflags)) { - // sorting is based on priority, which is not stored on the method as it is - // used only once, so copy the vector of methods into one where the priority - // can be stored during sorting - std::vector> pm; - pm.reserve(methods.size()); - for (auto ptr : methods) - pm.emplace_back(ptr->GetPriority(), ptr); - std::stable_sort(pm.begin(), pm.end(), PriorityCmp); - for (CPPOverload::Methods_t::size_type i = 0; i < methods.size(); ++i) - methods[i] = pm[i].second; - pymeth->fMethodInfo->fFlags |= CallContext::kIsSorted; + std::vector overloads; + bool is_operator = false; + bool is_conversion_operator = false; + bool is_static = false; + for (auto i : pymeth->fMethodInfo->fMethods) { + if (is_operator || dynamic_cast(i)->IsOperator()) + is_operator = true; + if (is_conversion_operator || + dynamic_cast(i)->IsConversionOperator()) + is_conversion_operator = true; + if (is_static || dynamic_cast(i)->IsStaticMethod()) + is_static = true; + overloads.push_back(i->GetMethod()); } - // three stages, as the C++ ranking: exact matches, standard conversions - // (kNoImplicit: no user-defined construction from the argument), then - // implicit conversions - std::vector errors; - std::vector implicit_possible(methods.size()); - bool bHaveImplicit = false; - for (int stage = 0; stage < 3; ++stage) { - if (stage == 1) - ctxt.fFlags |= CallContext::kNoImplicit; - else if (stage == 2) { - ctxt.fFlags &= ~CallContext::kNoImplicit; - ctxt.fFlags |= CallContext::kAllowImplicit; + std::string proto = ""; + if (im_self && !(ctxt.fFlags & CallContext::kIsConstructor)) { + PyObject* self = (PyObject*)im_self; + [[maybe_unused]] auto res = + AddTypeName(proto, (PyObject*)Py_TYPE(self), self, Utility::kNone); + assert(res); + } + + ctxt.fFlags |= CallContext::kAllowImplicit; + { + size_t i = ctxt.fFlags & CallContext::kIsConstructor && !im_self ? 1 : 0; + CPPInstance* self = nullptr; + if (i) { + self = (CPPInstance*)cpyrt_PyArgs_GET_ITEM(args, 0); + } else { + self = im_self; } - for (CPPOverload::Methods_t::size_type i = 0; i < nMethods; ++i) { - if (stage && !implicit_possible[i]) - continue; // did not set implicit conversion, so don't try again - - PyObject* result = methods[i]->Call(im_self, args, nargsf, kwds, &ctxt); - if (result) { - // success: update the dispatch map for subsequent calls - if (!memoized_pc) - dispatchMap.push_back(std::make_pair(sighash, methods[i])); - else { - // debatable: apparently there are two methods that map onto the same - // sighash and preferring the latest may result in "ping pong." - for (auto& p : dispatchMap) { - if (p.first == sighash) { - p.second = methods[i]; - break; + size_t nArgs = PyVectorcall_NARGS(nargsf); + for (; i < nArgs; i++) { + PyObject* obj = cpyrt_PyArgs_GET_ITEM(args, i); + PyObject* typ = (PyObject*)Py_TYPE(obj); + // TODO: this can be a flag within CPPOverload + if ((pymeth->fMethodInfo->fName == "__getitem__") || + (pymeth->fMethodInfo->fName == "__setitem__")) { + // unpack the tuple for Python getter and setter... + // TODO: propogate this logic to TemplateProxy too + if ((typ == (PyObject*)&PyTuple_Type) && (i == (im_self ? 0 : 1))) { + for (Py_ssize_t j = 0; j < PyTuple_GET_SIZE(obj); j++) { + if (!proto.empty()) + proto += ", "; + PyObject* unpacking_obj = PyTuple_GET_ITEM(obj, j); + PyObject* unpacking_typ = (PyObject*)Py_TYPE(unpacking_obj); + if (!AddTypeName(proto, unpacking_typ, unpacking_obj, + Utility::kNone)) { + proto += "__cppjit_internal::UnknownType"; + // break; // ???: do we need to raise error here } } + continue; + } else if (i == (im_self ? 1 : 2)) + continue; + } + if (self && IsConstructor(ctxt.fFlags) && + self->ObjectIsA(/*check_smart=*/false) != + pymeth->fMethodInfo->fScope) { + // this is a constructor of cross-inherited class + interop::TCppScope_t disp = self->ObjectIsA(/*check_smart=*/false); + + // happens for Python derived types (which have a dispatcher inserted + // that is not otherwise user-visible: call it instead) and C++ derived + // classes without public constructors + + // get the dispatcher class and verify + PyObject* dispproxy = cpyrt::GetScopeProxy(disp); + if (!dispproxy) { + PyErr_SetString(PyExc_TypeError, + "dispatcher proxy was never created"); + return nullptr; } - return HandleReturn(pymeth, im_self, result); - } + if (!(((CPPClass*)dispproxy)->fFlags & CPPScope::kIsPython)) { + PyErr_SetString(PyExc_TypeError, + const_cast(("constructor for " + + interop::GetScopedFinalName(disp) + + " is not a dispatcher") + .c_str())); + return nullptr; + } - // else failure .. - if (stage != 0) { - PyErr_Clear(); // first stage errors should be the more informative - ResetCallState(pymeth->fSelf, im_self); - continue; - } + PyObject* pyobj = + cpyrt_PyObject_Call(dispproxy, args + (self == im_self ? 0 : 1), + nargsf - (self == im_self ? 0 : 1), kwds); + if (!pyobj) + return nullptr; - // collect error message/trace (automatically clears exception, too) - if (!PyErr_Occurred()) { - // this should not happen; set an error to prevent core dump and report - PyObject* sig = methods[i]->GetPrototype(); - PyErr_Format(PyExc_SystemError, "%s =>\n %s", PyUnicode_AsUTF8(sig), - (char*)"nullptr result without error in overload call"); - Py_DECREF(sig); - } + // retrieve the actual pointer, take over control, and set set + // _internal_self + intptr_t address = (intptr_t)((CPPInstance*)pyobj)->GetObject(); + if (address) { + ((CPPInstance*)pyobj) + ->CppOwns(); // b/c self will control the object on address + PyObject* res = PyObject_CallMethodObjArgs( + dispproxy, PyStrings::gDispInit, pyobj, (PyObject*)self, nullptr); + Py_XDECREF(res); + } + Py_DECREF(dispproxy); + + // return object if successful, lament if not + if (address) { + Py_INCREF(self); + + // note: constructors are no longer set to take ownership by default; + // instead that is decided by the method proxy (which carries a + // creator flag) upon return + self->Set((void*)address); + if (IsCreator(pymeth->fMethodInfo->fFlags)) + self->PythonOwns(); + + // mark as actual to prevent needless auto-casting and register on its + // class + self->fFlags |= CPPInstance::kIsActual; + if (!(((CPPClass*)Py_TYPE(self))->fFlags & CPPScope::kIsSmart)) + MemoryRegulator::RegisterPyObject( + self, interop::TCppObject_t((void*)address)); + + // handling smart types this way is deeply fugly, but if CPPInstance + // sets the proper types in op_new first, then the wrong init is + // called + if (((CPPClass*)Py_TYPE(self))->fFlags & CPPScope::kIsSmart) { + PyObject* pyclass = CreateScopeProxy( + ((CPPSmartClass*)Py_TYPE(self))->fUnderlyingType); + if (pyclass) { + self->SetSmart((PyObject*)Py_TYPE(self)); + Py_DECREF((PyObject*)Py_TYPE(self)); + Py_SET_TYPE(self, (PyTypeObject*)pyclass); + } + } - // retrieve, store, and clear errors - bool callee_error = ctxt.fFlags & (CallContext::kPyException | - CallContext::kCppException); - ctxt.fFlags &= ~(CallContext::kPyException | CallContext::kCppException); - Utility::FetchError(errors, callee_error); + // done with self + Py_DECREF(self); - if (HaveImplicit(&ctxt)) { - bHaveImplicit = true; - implicit_possible[i] = true; - ctxt.fFlags &= ~CallContext::kHaveImplicit; - } else - implicit_possible[i] = false; - ResetCallState(pymeth->fSelf, im_self); + Py_RETURN_NONE; // by definition + } + } + if (IsConstructor(ctxt.fFlags) && + interop::GetScopedFinalName(pymeth->fMethodInfo->fScope) + .find("__cppjit_internal::Dispatcher") == 0) { + if (typ == (PyObject*)&PyTuple_Type) { + if (i > 1) + proto += ", __cppjit_internal::Sep*"; + for (Py_ssize_t j = 0; j < PyTuple_GET_SIZE(obj); j++) { + if (!proto.empty()) + proto += ", "; + PyObject* unpacking_obj = PyTuple_GET_ITEM(obj, j); + PyObject* unpacking_typ = (PyObject*)Py_TYPE(unpacking_obj); + if (!AddTypeName(proto, unpacking_typ, unpacking_obj, + Utility::kNone)) { + proto += "__cppjit_internal::UnknownType"; + // break; // ???: do we need to raise error here + } + } + continue; + } + } + if (!proto.empty()) + proto += ", "; + if (!AddTypeName(proto, typ, obj, Utility::kNone)) { + proto += "__cppjit_internal::UnknownType"; + // break; // ???: do we need to raise error here + } + } + if (kwds) { + for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(kwds); i++) { + if (!proto.empty()) + proto += ", "; + PyObject* obj = cpyrt_PyArgs_GET_ITEM(args, i + nArgs); + PyObject* typ = (PyObject*)Py_TYPE(obj); + if (!AddTypeName(proto, typ, obj, Utility::kNone)) { + proto += "__cppjit_internal::UnknownType"; + break; // ???: do we need to raise error here + } + } } - - // only move forward if implicit conversions are available - if (!bHaveImplicit) - break; } - // first summarize, then add details - PyObject* topmsg = PyUnicode_FromFormat( - "none of the %d overloaded methods succeeded. Full details:", - (int)nMethods); - SetDetailedException(std::move(errors), topmsg /* steals */, - PyExc_TypeError /* default error */); - - // report failure + std::vector ambiguous_candidates; + interop::TCppMethod_t meth = interop::BestOverloadFunctionMatch( + overloads, proto, ambiguous_candidates, /*TODO:*/ nullptr, is_operator); + if (!meth && (ctxt.fFlags & CallContext::kIsConstructor)) { + // this might be a POD class/struct + // and might need the self* for the overload resolution + // to pick up the custom generated constructor defined in + // __cppjit_internal namespace + // FIXME: for the self parameter changes in CppInterOp + std::string self_type_name = ""; + if (PyVectorcall_NARGS(nargsf) > 0) { + PyObject* obj = cpyrt_PyArgs_GET_ITEM(args, 0); + PyObject* typ = (PyObject*)Py_TYPE(obj); + AddTypeName(self_type_name, typ, nullptr, Utility::kNone); + proto = self_type_name + "**, " + proto; + meth = interop::BestOverloadFunctionMatch(overloads, proto, + ambiguous_candidates, + /*TODO:*/ nullptr, is_operator); + } + } + if (meth) { + for (auto i : pymeth->fMethodInfo->fMethods) { + if (i->GetMethod() == meth) { + PyObject* result = i->Call(im_self, args, nargsf, kwds, &ctxt); + if (result) { + // success: update the dispatch map for subsequent calls + if (!memoized_pc) + dispatchMap.push_back(std::make_pair(sighash, i)); + else { + // debatable: apparently there are two methods that map onto the + // same sighash and preferring the latest may result in "ping + // pong." + for (auto& p : dispatchMap) { + if (p.first == sighash) { + p.second = i; + break; + } + } + } + return HandleReturn(pymeth, im_self, result); + } + return nullptr; + } + } + } + std::ostringstream overload_signatures; + if (ambiguous_candidates.empty()) { + for (const auto i : overloads) { + overload_signatures + << " " + << interop::GetMethodReturnTypeAsString( + i) // TODO: mention if the function is static from the list + << " " << interop::GetScopedFinalName(i.data) + << interop::GetMethodSignature(i, true) << "\n"; + } + PyErr_Format( + gOverloadResolutionException, + "Overload Resolution Failed.\nOverload set:\n%sDeduced Argument Types: " + "(%s)\n%s", + overload_signatures.str().c_str(), proto.c_str(), + is_static ? "Found static methods in the overload set. If you want to " + "invoke the static overload, try calling the function " + "using the class type instead of the instance.\n" + : ""); + } else { + for (const auto i : ambiguous_candidates) { + overload_signatures << " " << interop::GetMethodReturnTypeAsString(i) + << " " << interop::GetScopedFinalName(i.data) + << interop::GetMethodSignature(i, true) << "\n"; + } + PyErr_Format( + gOverloadAmbiguityException, + "Overload Resolution Failed. Call to \"%s\" is ambiguous.\nAmbigious " + "set:\n%sDeduced Argument Types: (%s)\n%s", + pymeth->fMethodInfo->fName.c_str(), overload_signatures.str().c_str(), + proto.c_str(), + is_static ? "Found static methods in the overload set. If you want to " + "invoke the static overload, try calling the function " + "using the class type instead of the instance.\n" + : ""); + } return nullptr; } @@ -959,11 +1121,12 @@ PyTypeObject CPPOverload_Type = { //- public members ----------------------------------------------------------- void cpyrt::CPPOverload::Set(const std::string& name, + interop::TCppScope_t scope, std::vector& methods) { // Fill in the data of a freshly created method proxy. fMethodInfo->fName = name; + fMethodInfo->fScope = scope; fMethodInfo->fMethods.swap(methods); - fMethodInfo->fFlags &= ~CallContext::kIsSorted; // special case: all constructors are considered creators by default if (name == "__init__") @@ -991,7 +1154,6 @@ void cpyrt::CPPOverload::Set(const std::string& name, void cpyrt::CPPOverload::AdoptMethod(PyCallable* pc) { // Fill in the data of a freshly created method proxy. fMethodInfo->fMethods.push_back(pc); - fMethodInfo->fFlags &= ~CallContext::kIsSorted; } //---------------------------------------------------------------------------- @@ -1001,7 +1163,6 @@ void cpyrt::CPPOverload::MergeOverload(CPPOverload* meth) { fMethodInfo->fMethods.insert(fMethodInfo->fMethods.end(), meth->fMethodInfo->fMethods.begin(), meth->fMethodInfo->fMethods.end()); - fMethodInfo->fFlags &= ~CallContext::kIsSorted; meth->fMethodInfo->fDispatchMap.clear(); meth->fMethodInfo->fMethods.clear(); } @@ -1051,7 +1212,7 @@ PyObject* cpyrt::CPPOverload::FindOverload(const std::string& signature, newmeth = mp_new(nullptr, nullptr, nullptr); CPPOverload::Methods_t vec; vec.push_back(meth->Clone()); - newmeth->Set(fMethodInfo->fName, vec); + newmeth->Set(fMethodInfo->fName, nullptr, vec); if (fSelf) { Py_INCREF(fSelf); @@ -1126,7 +1287,7 @@ PyObject* cpyrt::CPPOverload::FindOverload(PyObject* args_tuple, CPPOverload* newmeth = mp_new(nullptr, nullptr, nullptr); CPPOverload::Methods_t vec; vec.push_back(methods[best_method]->Clone()); - newmeth->Set(fMethodInfo->fName, vec); + newmeth->Set(fMethodInfo->fName, nullptr, vec); if (fSelf) { Py_INCREF(fSelf); diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.h b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.h index 87019d26f687b..7d17e00f02d5c 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.h +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.h @@ -3,6 +3,7 @@ // Bindings #include "PyCallable.h" +#include "cppjit_interop.h" // Standard #include @@ -12,35 +13,6 @@ namespace cppjit::cpyrt { -// signature hashes are also used by TemplateProxy -inline uint64_t HashSignature(cpyrt_PyArgs_t args, size_t nargsf) { - // Build a hash from the types of the given python function arguments. - uint64_t hash = 0; - - Py_ssize_t nargs = cpyrt_PyArgs_GET_SIZE(args, nargsf); - for (Py_ssize_t i = 0; i < nargs; ++i) { - // TODO: hashing in the ref-count is for moves; resolve this together with - // the improved overloads for implicit conversions - PyObject* pyobj = cpyrt_PyArgs_GET_ITEM(args, i); - hash += (uint64_t)Py_TYPE(pyobj); -#if PY_VERSION_HEX >= 0x030e0000 - hash += - (uint64_t)(PyUnstable_Object_IsUniqueReferencedTemporary(pyobj) ? 1 - : 0); -#else - hash += (uint64_t)(Py_REFCNT(pyobj) == 1 ? 1 : 0); -#endif - hash += (hash << 10); - hash ^= (hash >> 6); - } - - hash += (hash << 3); - hash ^= (hash >> 11); - hash += (hash << 15); - - return hash; -} - class CPPOverload { public: typedef std::vector> DispatchMap_t; @@ -56,6 +28,7 @@ class CPPOverload { CPPOverload::DispatchMap_t fDispatchMap; CPPOverload::Methods_t fMethods; PyObject* fDoc; + interop::TCppScope_t fScope; uint32_t fFlags; int* fRefCount; @@ -66,7 +39,8 @@ class CPPOverload { }; public: - void Set(const std::string& name, std::vector& methods); + void Set(const std::string& name, interop::TCppScope_t scope, + std::vector& methods); void AdoptMethod(PyCallable* pc); void MergeOverload(CPPOverload* meth); @@ -101,20 +75,56 @@ template inline bool CPPOverload_CheckExact(T* object) { //- creation ----------------------------------------------------------------- inline CPPOverload* CPPOverload_New(const std::string& name, + interop::TCppScope_t scope, std::vector& methods) { // Create and initialize a new method proxy from the overloads. CPPOverload* pymeth = (CPPOverload*)CPPOverload_Type.tp_new(&CPPOverload_Type, nullptr, nullptr); - pymeth->Set(name, methods); + pymeth->Set(name, scope, methods); return pymeth; } inline CPPOverload* CPPOverload_New(const std::string& name, + interop::TCppScope_t scope, PyCallable* method) { // Create and initialize a new method proxy from the method. std::vector p; p.push_back(method); - return CPPOverload_New(name, p); + return CPPOverload_New(name, scope, p); +} + +// signature hashes are also used by TemplateProxy +inline uint64_t HashSignature(cpyrt_PyArgs_t args, size_t nargsf) { + // Build a hash from the types of the given python function arguments. + uint64_t hash = 0; + + Py_ssize_t nargs = cpyrt_PyArgs_GET_SIZE(args, nargsf); + for (Py_ssize_t i = 0; i < nargs; ++i) { + // TODO: hashing in the ref-count is for moves; resolve this together with + // the improved overloads for implicit conversions + PyObject* pyobj = cpyrt_PyArgs_GET_ITEM(args, i); + hash += (uint64_t)Py_TYPE(pyobj); +#if PY_VERSION_HEX >= 0x030e0000 + hash += + (uint64_t)(PyUnstable_Object_IsUniqueReferencedTemporary(pyobj) ? 1 + : 0); +#else + hash += (uint64_t)(Py_REFCNT(pyobj) == 1 ? 1 : 0); +#endif + if (CPPOverload_CheckExact(pyobj)) { + static std::hash hash_string; + CPPOverload* overload = (CPPOverload*)pyobj; + hash += hash_string(overload->fMethodInfo->fName); + } + hash += (hash << 10); + hash ^= (hash >> 6); + } + + hash += (hash << 3); + hash ^= (hash >> 11); + hash += (hash << 15); + + return hash; } } // namespace cppjit::cpyrt diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPScope.cxx b/bindings/pyroot/cppjit/src/cpyrt/CPPScope.cxx index dd5801abed713..26b47450e7dec 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPScope.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPScope.cxx @@ -50,13 +50,8 @@ add_template(PyObject* pyclass, const std::string& name, } if (overloads) { - // adopt the new overloads - if (ncl != name) - for (auto clb : *overloads) - pytmpl->AdoptTemplate(clb); - else - for (auto clb : *overloads) - pytmpl->AdoptMethod(clb); + for (auto clb : *overloads) + pytmpl->AdoptMethod(clb); } // the caller expects a method matching the full name, thus is a @@ -401,7 +396,7 @@ static PyObject* meta_getattro(PyObject* pyclass, PyObject* pyname) { if (interop::ExistsMethodTemplate(scope, name)) attr = add_template(pyclass, name, &overloads); else - attr = (PyObject*)CPPOverload_New(name, overloads); + attr = (PyObject*)CPPOverload_New(name, scope, overloads); templated_functions_checked = true; } } diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPScope.h b/bindings/pyroot/cppjit/src/cpyrt/CPPScope.h index 04670c113252e..012be7ca3d529 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPScope.h +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPScope.h @@ -23,6 +23,8 @@ typedef struct { #endif // Standard +#include "Python.h" +#include "cppjit_interop.h" #include #include diff --git a/bindings/pyroot/cppjit/src/cpyrt/CallContext.h b/bindings/pyroot/cppjit/src/cpyrt/CallContext.h index f8000a0d82ef3..cc3bf232ee10d 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CallContext.h +++ b/bindings/pyroot/cppjit/src/cpyrt/CallContext.h @@ -30,7 +30,9 @@ struct CallContext { enum ECallFlags { kNone = 0x000000, - kIsSorted = 0x000001, // if method overload priority determined + // UNUSED with new overload resolution + // kIsSorted = 0x000001, // if method overload priority + // determined kIsCreator = 0x000002, // if method creates python-owned objects kIsConstructor = 0x000004, // if method is a C++ constructor kHaveImplicit = 0x000008, // indicate that implicit converters are available @@ -40,15 +42,18 @@ struct CallContext { kFromDescr = 0x000080, // initiated from a descriptor kUseHeuristics = 0x000100, // if method applies heuristics memory policy kImplicitSmartPtrConversion = - 0x000200, // enable implicit conversion to smart pointers - kReleaseGIL = 0x000400, // if method should release the GIL - kSetLifeLine = 0x000800, // if return value is part of 'this' - kNeverLifeLine = 0x001000, // if the return value is never part of 'this' - kPyException = 0x002000, // Python exception during method execution - kCppException = 0x004000, // C++ exception during method execution - kProtected = 0x008000, // if method should return on signals - kUseFFI = 0x010000, // not implemented - kIsPseudoFunc = 0x020000, // internal, used for introspection + 0x000200, // enable implicit conversion to smart pointers + kReleaseGIL = 0x000400, // if method should release the GIL + kSetLifeLine = 0x000800, // if return value is part of 'this' + kNeverLifeLine = 0x001000, // if the return value is never part of 'this' + kPyException = 0x002000, // Python exception during method execution + kCppException = 0x004000, // C++ exception during method execution + kProtected = 0x008000, // if method should return on signals + kUseFFI = 0x010000, // not implemented + kIsPseudoFunc = 0x020000, // internal, used for introspection + kUseStrict = 0x040000, // if method applies strict memory policy + kIsMethod = 0x080000, // If this is a class method + kIsSatticMethod = 0x100000, // If this is a static class method }; // Policies about memory handling and signal safety @@ -100,8 +105,6 @@ struct CallContext { Temporary* fTemps; }; -inline bool IsSorted(uint64_t flags) { return flags & CallContext::kIsSorted; } - inline bool IsCreator(uint64_t flags) { return flags & CallContext::kIsCreator; } diff --git a/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx b/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx index da661fa383c45..b03108ef9e508 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx @@ -2483,7 +2483,12 @@ bool cpyrt::InstanceRefConverter::SetArg(PyObject* pyobject, Parameter& para, interop::TCppScope_t cls; if (pyobj->IsSmart()) { cls = pyobj->ObjectIsA(false); - if (cls && interop::IsSubclass(cls, fClass)) { + interop::TCppScope_t deref_cls = nullptr; + interop::TCppScope_t deref_fClass = nullptr; + interop::GetSmartPtrInfo(cls, &deref_cls, nullptr); + interop::GetSmartPtrInfo(fClass, &deref_fClass, nullptr); + if (deref_cls && deref_fClass && + interop::IsSubclass(deref_cls, deref_fClass)) { para.fValue.fVoidp = pyobj->GetObjectRaw(); argset = true; } @@ -2506,7 +2511,7 @@ bool cpyrt::InstanceRefConverter::SetArg(PyObject* pyobject, Parameter& para, } // calculate offset between formal and actual arguments - if (cls != fClass) { + if ((cls != fClass) && (!pyobj->IsSmart())) { para.fValue.fIntPtr += interop::GetBaseOffset( cls, fClass, para.fValue.fVoidp, 1 /* up-cast */); } @@ -2896,10 +2901,12 @@ static void* PyFunction_AsCPointer(PyObject* pyobject, if (pytmpl->fTemplateArgs) fullname += PyUnicode_AsUTF8(pytmpl->fTemplateArgs); interop::TCppScope_t scope = ((CPPClass*)pytmpl->fTI->fPyClass)->fCppType; + std::string ret{}, sig{}; GetSignatureFromFnType(fn_type, ret, sig); + std::vector ambiguous_candidates; interop::TCppMethod_t cppmeth = interop::GetMethodTemplate( - scope, fullname, sig.substr(1, sig.size() - 2)); + scope, fullname, sig.substr(1, sig.size() - 2), ambiguous_candidates); if (cppmeth) { void* fptr = (void*)interop::GetFunctionAddress(cppmeth, false); if (fptr) diff --git a/bindings/pyroot/cppjit/src/cpyrt/Dispatcher.cxx b/bindings/pyroot/cppjit/src/cpyrt/Dispatcher.cxx index 40b1dbdfa74b7..dcfbd2071a5df 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Dispatcher.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Dispatcher.cxx @@ -524,6 +524,7 @@ bool cpyrt::InsertDispatcher(CPPScope* klass, PyObject* bases, PyObject* dct, code << "};\n}"; // finally, compile the code + // printf("\n\n%s\n\n", code.str().c_str()); if (!interop::Compile(code.str())) { err << "failed to compile the dispatcher code"; return false; diff --git a/bindings/pyroot/cppjit/src/cpyrt/LowLevelViews.cxx b/bindings/pyroot/cppjit/src/cpyrt/LowLevelViews.cxx index d4d81fa536a43..94e06850af62d 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/LowLevelViews.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/LowLevelViews.cxx @@ -1095,6 +1095,7 @@ CreateLowLevelViewT(T* address, cpyrt::cdims_t shape, PyObject* args = PyTuple_New(0); LowLevelView* llp = (LowLevelView*)LowLevelView_Type.tp_new( &LowLevelView_Type, args, nullptr); + llp->fCppTypeName = (format ? "void" : typecode_traits::name); Py_DECREF(args); Py_buffer& view = llp->fBufInfo; diff --git a/bindings/pyroot/cppjit/src/cpyrt/LowLevelViews.h b/bindings/pyroot/cppjit/src/cpyrt/LowLevelViews.h index 80e7f02bcc4de..7d828af3c5682 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/LowLevelViews.h +++ b/bindings/pyroot/cppjit/src/cpyrt/LowLevelViews.h @@ -27,6 +27,7 @@ class LowLevelView { void** fBuf; Converter* fConverter; Converter* fElemCnv; + const char* fCppTypeName; typedef LowLevelView* (*Creator_t)(void*, cdims_t); Creator_t fCreator; // for slicing, which requires copying diff --git a/bindings/pyroot/cppjit/src/cpyrt/ProxyWrappers.cxx b/bindings/pyroot/cppjit/src/cpyrt/ProxyWrappers.cxx index 0b8f9c5fcf7f6..712d96f40d5bb 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/ProxyWrappers.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/ProxyWrappers.cxx @@ -155,7 +155,8 @@ static inline void sync_templates(PyObject* pyclass, PyErr_Clear(); Py_DECREF(dct); if (!TemplateProxy_Check(attr)) { - TemplateProxy* pytmpl = TemplateProxy_New(mtCppName, mtName, pyclass); + TemplateProxy* pytmpl = + TemplateProxy_New(mtCppName, mtName, pyclass, mtName == "__init__"); if (CPPOverload_Check(attr)) pytmpl->MergeOverload((CPPOverload*)attr); PyType_Type.tp_setattro(pyclass, pyname, (PyObject*)pytmpl); @@ -285,10 +286,7 @@ static int BuildScopeProxyDict(interop::TCppScope_t scope, PyObject* pyclass, // add only here, not to the cache of collected methods PyObject* attr = PyObject_GetAttrString(pyclass, const_cast(mtName.c_str())); - if (isTemplate) - ((TemplateProxy*)attr)->AdoptTemplate(pycall); - else - ((TemplateProxy*)attr)->AdoptMethod(pycall); + ((TemplateProxy*)attr)->AdoptMethod(pycall); Py_DECREF(attr); // for operator[]/() that returns by ref, also add __setitem__ @@ -307,10 +305,7 @@ static int BuildScopeProxyDict(interop::TCppScope_t scope, PyObject* pyclass, PyObject_SetAttrString(pyclass, const_cast("__setitem__"), (PyObject*)pysi); } - if (isTemplate) - pysi->AdoptTemplate(new CPPSetItem(scope, method)); - else - pysi->AdoptMethod(new CPPSetItem(scope, method)); + pysi->AdoptMethod(new CPPSetItem(scope, method)); Py_XDECREF(pysi); } @@ -410,7 +405,7 @@ static int BuildScopeProxyDict(interop::TCppScope_t scope, PyObject* pyclass, if (!attr) PyErr_Clear(); // normal case, add a new method - CPPOverload* method = CPPOverload_New(imd->first, imd->second); + CPPOverload* method = CPPOverload_New(imd->first, scope, imd->second); PyObject* pymname = PyUnicode_InternFromString( const_cast(method->GetName().c_str())); PyType_Type.tp_setattro(pyclass, pymname, (PyObject*)method); diff --git a/bindings/pyroot/cppjit/src/cpyrt/PyCallable.h b/bindings/pyroot/cppjit/src/cpyrt/PyCallable.h index f9a64de44bd48..5cc951c32ff48 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/PyCallable.h +++ b/bindings/pyroot/cppjit/src/cpyrt/PyCallable.h @@ -6,10 +6,14 @@ // Bindings #include "CallContext.h" #include "cppjit_interop.h" +#include "cpyrt.h" #include "cpyrt/Reflex.h" namespace cppjit::cpyrt { +extern PyObject* gOverloadResolutionException; +extern PyObject* gOverloadAmbiguityException; + class CPPInstance; class PyCallable { @@ -17,6 +21,7 @@ class PyCallable { virtual ~PyCallable() {} public: + virtual interop::TCppMethod_t GetMethod() { return nullptr; } virtual PyObject* GetSignature(bool show_formalargs = true) = 0; virtual PyObject* GetSignatureNames() = 0; virtual PyObject* GetSignatureTypes() = 0; @@ -31,9 +36,6 @@ class PyCallable { return nullptr; }; - virtual int GetPriority() = 0; - virtual bool IsGreedy() = 0; - virtual int GetMaxArgs() = 0; virtual PyObject* GetCoVarNames() = 0; virtual PyObject* GetArgDefault(int /* iarg */, bool silent = true) = 0; diff --git a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx index 4fc68fbde55c3..d59eaa768499d 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx @@ -1,5 +1,6 @@ // Bindings #include "cpyrt.h" +#include "unicodeobject.h" using namespace cppjit; #include "CPPClassMethod.h" @@ -15,6 +16,7 @@ using namespace cppjit; // Standard #include +#include namespace cppjit::cpyrt { @@ -26,8 +28,7 @@ static inline std::string targs2str(TemplateProxy* pytmpl) { //---------------------------------------------------------------------------- TemplateInfo::TemplateInfo() - : fPyClass(nullptr), fNonTemplated(nullptr), fTemplated(nullptr), - fLowPriority(nullptr), fDoc(nullptr) { + : fPyClass(nullptr), fOverloads(nullptr), fDoc(nullptr) { /* empty */ } @@ -36,9 +37,7 @@ TemplateInfo::~TemplateInfo() { Py_XDECREF(fPyClass); Py_XDECREF(fDoc); - Py_DECREF(fNonTemplated); - Py_DECREF(fTemplated); - Py_DECREF(fLowPriority); + Py_DECREF(fOverloads); for (const auto& p : fDispatchMap) { for (const auto& c : p.second) { @@ -50,51 +49,44 @@ TemplateInfo::~TemplateInfo() { //---------------------------------------------------------------------------- void TemplateProxy::MergeOverload(CPPOverload* mp) { // Store overloads of this templated method. - bool isGreedy = false; - for (auto pc : mp->fMethodInfo->fMethods) { - if (pc->IsGreedy()) { - isGreedy = true; - break; - } - } - - CPPOverload* cppol = isGreedy ? fTI->fLowPriority : fTI->fNonTemplated; - cppol->MergeOverload(mp); + fTI->fOverloads->MergeOverload(mp); } void TemplateProxy::AdoptMethod(PyCallable* pc) { // Store overload of this templated method. - CPPOverload* cppol = pc->IsGreedy() ? fTI->fLowPriority : fTI->fNonTemplated; - cppol->AdoptMethod(pc); -} - -void TemplateProxy::AdoptTemplate(PyCallable* pc) { - // Store known template methods. - fTI->fTemplated->AdoptMethod(pc); + fTI->fOverloads->AdoptMethod(pc); } //---------------------------------------------------------------------------- PyObject* TemplateProxy::Instantiate(const std::string& fname, cpyrt_PyArgs_t args, size_t nargsf, - Utility::ArgPreference pref, int* pcnt) { + Utility::ArgPreference pref, int* pcnt, + bool include_non_templated) { // Instantiate (and cache) templated methods, return method if any std::string proto = ""; std::string protoPair = ""; + bool skipSelf = fTI->fIsConstructor && (!fSelf || fSelf == Py_None); + if (fSelf && (fSelf != Py_None) && (!fTI->fIsConstructor)) { + PyObject* self = (PyObject*)fSelf; + assert(AddTypeName(proto, (PyObject*)Py_TYPE(self), self, Utility::kNone)); + } // adjust arguments for self if this is a rebound global function - bool isNS = (((CPPScope*)fTI->fPyClass)->fFlags & CPPScope::kIsNamespace); - if (!isNS && cpyrt_PyArgs_GET_SIZE(args, nargsf) && - (!fSelf || (fSelf == Py_None && - !interop::IsStaticTemplate( - ((CPPScope*)fTI->fPyClass)->fCppType, fname)))) { - args += 1; - nargsf -= 1; - } + // bool isNS = (((CPPScope*)fTI->fPyClass)->fFlags & CPPScope::kIsNamespace); + // if (!fTI->fIsConstructor && !isNS && cpyrt_PyArgs_GET_SIZE(args, nargsf) && + // \ + // (!fSelf || + // (fSelf == Py_None && + // !interop::IsStaticTemplate(((CPPScope*)fTI->fPyClass)->fCppType, + // fname)))) { + // args += 1; + // nargsf -= 1; + // } Py_ssize_t argc = cpyrt_PyArgs_GET_SIZE(args, nargsf); if (argc != 0) { - PyObject* tpArgs = PyTuple_New(argc); - for (Py_ssize_t i = 0; i < argc; ++i) { + PyObject* tpArgs = PyTuple_New(skipSelf ? argc - 1 : argc); + for (Py_ssize_t i = skipSelf ? 1 : 0, j = 0; i < argc; ++i, ++j) { PyObject* itemi = cpyrt_PyArgs_GET_ITEM(args, i); bool bArgSet = false; @@ -104,7 +96,7 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, TemplateProxy* tn = (TemplateProxy*)itemi; PyObject* f = PyUnicode_FromFormat("%s%s", tn->fTI->fCppName.c_str(), targs2str(tn).c_str()); - PyTuple_SET_ITEM(tpArgs, i, f); + PyTuple_SET_ITEM(tpArgs, j, f); bArgSet = true; } PyObject* pytc = nullptr; @@ -123,7 +115,7 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, PyObject* pyptrname = Utility::CT2CppName(pytc, ptrdef.c_str(), true); if (pyptrname) { - PyTuple_SET_ITEM(tpArgs, i, pyptrname); + PyTuple_SET_ITEM(tpArgs, j, pyptrname); bArgSet = true; // string added, but not counted towards nStrings } @@ -151,7 +143,7 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, Py_XDECREF(pytc); pytc = nullptr; if (pyactname) { - PyTuple_SET_ITEM(tpArgs, i, pyactname); + PyTuple_SET_ITEM(tpArgs, j, pyactname); bArgSet = true; // string added, but not counted towards nStrings } @@ -163,18 +155,19 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, PyErr_Clear(); PyObject* tp = (PyObject*)Py_TYPE(itemi); Py_INCREF(tp); - PyTuple_SET_ITEM(tpArgs, i, tp); + PyTuple_SET_ITEM(tpArgs, j, tp); } } - PyObject* pyargs = PyTuple_New(argc); - for (Py_ssize_t i = 0; i < argc; ++i) { + PyObject* pyargs = PyTuple_New(skipSelf ? argc - 1 : argc); + for (Py_ssize_t i = skipSelf ? 1 : 0, j = 0; i < argc; ++i, ++j) { PyObject* item = cpyrt_PyArgs_GET_ITEM(args, i); Py_INCREF(item); - PyTuple_SET_ITEM(pyargs, i, item); + PyTuple_SET_ITEM(pyargs, j, item); } - const std::string& name_v1 = - Utility::ConstructTemplateArgs(nullptr, tpArgs, pyargs, pref, 0, pcnt); + const std::string& name_v1 = Utility::ConstructTemplateArgs( + nullptr, tpArgs, pyargs, pref, 0, + pcnt /*, usingThisParameter=??? */); // FIXME // A Python 2-tuple is typed above as std::initializer_list, but it may // equally represent a std::pair, to which an initializer_list can not @@ -221,7 +214,8 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, nullptr, tpArgsPair, pyargs, pref, 0, nullptr); Py_DECREF(tpArgsPair); if (!PyErr_Occurred() && name_v2.size()) - protoPair = name_v2.substr(1, name_v2.size() - 2); + protoPair = (proto.empty() ? "" : proto + ", ") + + name_v2.substr(1, name_v2.size() - 2); else PyErr_Clear(); } @@ -236,17 +230,24 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, return nullptr; } - if (name_v1.size()) - proto = name_v1.substr(1, name_v1.size() - 2); + if (name_v1.size()) { + if (!proto.empty()) + proto.append(", "); + proto.append(name_v1.substr(1, name_v1.size() - 2)); + } } // the following causes instantiation as necessary interop::TCppScope_t scope = ((CPPClass*)fTI->fPyClass)->fCppType; - interop::TCppMethod_t cppmeth = - interop::GetMethodTemplate(scope, fname, proto); + std::vector ambiguous_candidates; + interop::TCppMethod_t cppmeth = interop::GetMethodTemplate( + scope, fname, proto, ambiguous_candidates, include_non_templated); if (!cppmeth && !protoPair.empty() && protoPair != proto) { // retry with 2-tuples typed as std::pair instead of std::initializer_list - cppmeth = interop::GetMethodTemplate(scope, fname, protoPair); + ambiguous_candidates.clear(); + cppmeth = interop::GetMethodTemplate(scope, fname, protoPair, + ambiguous_candidates, + include_non_templated); if (cppmeth) proto = protoPair; } @@ -277,8 +278,8 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, pos = proto.find("initializer_list", pos + 6); } - interop::TCppMethod_t m2 = - interop::GetMethodTemplate(scope, fname, proto); + interop::TCppMethod_t m2 = interop::GetMethodTemplate( + scope, fname, proto, ambiguous_candidates, include_non_templated); if (m2 && m2 != cppmeth) { // replace if the new method with vector was found; otherwise just // continue with the previously found method with initializer_list. @@ -327,7 +328,7 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, // Case 1/2: method simply did not exist before if (!pyol) { // actual overload to use (now owns meth) - pyol = (PyObject*)CPPOverload_New(fname, meth); + pyol = (PyObject*)CPPOverload_New(fname, scope, meth); if (bIsConstructor) { // TODO: this is an ugly hack :( ((CPPOverload*)pyol)->fMethodInfo->fFlags |= @@ -349,16 +350,16 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, // Case 5: must be a template proxy, meaning that current template name is // not a template overload else if (bIsCppTP) { - ((TemplateProxy*)pyol)->AdoptTemplate(meth->Clone()); + ((TemplateProxy*)pyol)->AdoptMethod(meth->Clone()); Py_DECREF(pyol); - pyol = (PyObject*)CPPOverload_New(fname, meth); // takes ownership + pyol = (PyObject*)CPPOverload_New(fname, scope, meth); // takes ownership } // Case 6: pre-existing object is not a CPPOverload nor TemplateProxy // we do not cache it, as this might be a pythonization (monkey-patched // func/method) else { Py_DECREF(pyol); - pyol = (PyObject*)CPPOverload_New(fname, meth); + pyol = (PyObject*)CPPOverload_New(fname, scope, meth); } // Special Case if name was aliased (e.g. typedef in template instantiation) @@ -454,30 +455,8 @@ static PyObject* tpp_doc(TemplateProxy* pytmpl, void*) { // Forward to method proxies to doc all overloads PyObject* doc = nullptr; - if (pytmpl->fTI->fNonTemplated->HasMethods()) - doc = PyObject_GetAttrString((PyObject*)pytmpl->fTI->fNonTemplated, - "__doc__"); - if (pytmpl->fTI->fTemplated->HasMethods()) { - PyObject* doc2 = - PyObject_GetAttrString((PyObject*)pytmpl->fTI->fTemplated, "__doc__"); - if (doc && doc2) { - PyUnicode_AppendAndDel(&doc, PyUnicode_FromString("\n")); - PyUnicode_AppendAndDel(&doc, doc2); - } else if (!doc && doc2) { - doc = doc2; - } - } - if (pytmpl->fTI->fLowPriority->HasMethods()) { - PyObject* doc2 = - PyObject_GetAttrString((PyObject*)pytmpl->fTI->fLowPriority, "__doc__"); - if (doc && doc2) { - PyUnicode_AppendAndDel(&doc, PyUnicode_FromString("\n")); - PyUnicode_AppendAndDel(&doc, doc2); - } else if (!doc && doc2) { - doc = doc2; - } - } - + if (pytmpl->fTI->fOverloads->HasMethods()) + doc = PyObject_GetAttrString((PyObject*)pytmpl->fTI->fOverloads, "__doc__"); if (doc) return doc; @@ -685,8 +664,8 @@ static PyObject* tpp_vectorcall(TemplateProxy* pytmpl, PyObject* const* args, PyErr_Clear(); // not cached or failed call; try instantiation - pymeth = pytmpl->Instantiate(PyUnicode_AsUTF8(pyfullname), args, nargsf, - Utility::kNone); + pymeth = pytmpl->Instantiate(PyUnicode_AsUTF8(pyfullname), args, + nargsf, Utility::kNone, nullptr, false); if (pymeth) { // attempt actual call; same as above, allow implicit conversion of // arguments @@ -711,56 +690,30 @@ static PyObject* tpp_vectorcall(TemplateProxy* pytmpl, PyObject* const* args, return nullptr; } - // case 2: select known non-template overload - result = SelectAndForward(pytmpl, pytmpl->fTI->fNonTemplated, args, nargsf, - kwds, true /* implicitOkay */, - false /* use_targs */, sighash, errors); - if (result) - TPPCALL_RETURN; - - // case 3: select known template overload - result = SelectAndForward(pytmpl, pytmpl->fTI->fTemplated, args, nargsf, kwds, - false /* implicitOkay */, true /* use_targs */, - sighash, errors); - if (result) - TPPCALL_RETURN; - - // case 4: auto-instantiation from types of arguments - for (auto pref : {Utility::kReference, Utility::kPointer, Utility::kValue}) { - // TODO: no need to loop if there are no non-instance arguments; also, - // should any failed lookup be removed? - int pcnt = 0; - pymeth = - pytmpl->Instantiate(pytmpl->fTI->fCppName, args, nargsf, pref, &pcnt); - if (pymeth) { - // attempt actual call; even if argument based, allow implicit - // conversions, for example for non-template arguments - result = CallMethodImp(pytmpl, pymeth, args, nargsf, kwds, /*impOK=*/true, - sighash); - if (result) - TPPCALL_RETURN; - } - Utility::FetchError(errors); - if (!pcnt) - break; // preference never used; no point trying others + // case 2: create a template instantiation + int pcnt = 0; // FIXME: this is unused and can be removed + pymeth = pytmpl->Instantiate(pytmpl->fTI->fCppName, args, nargsf, + Utility::kReference, &pcnt, true); + if (pymeth) { + // attempt actual call; even if argument based, allow implicit conversions, + // for example for non-template arguments + result = CallMethodImp(pytmpl, pymeth, args, nargsf, kwds, /*impOK=*/true, + sighash); + if (result) + TPPCALL_RETURN; } + Utility::FetchError(errors); - // case 5: low priority methods, such as ones that take void* arguments - result = SelectAndForward(pytmpl, pytmpl->fTI->fLowPriority, args, nargsf, - kwds, false /* implicitOkay */, - false /* use_targs */, sighash, errors); - if (result) - TPPCALL_RETURN; - - // error reporting is fraud, given the numerous steps taken, but more details - // seems better + // error reporting is fraud, given the numerous steps taken, but more + // details seems better if (!errors.empty()) { PyObject* topmsg = - PyUnicode_FromString("Template method resolution failed:"); - Utility::SetDetailedException(std::move(errors), topmsg /* steals */, - PyExc_TypeError /* default error */); + PyUnicode_InternFromString("Template method resolution failed:"); + Utility::SetDetailedException( + std::move(errors), topmsg /* steals */, + gOverloadResolutionException /* default error */); } else { - PyErr_Format(PyExc_TypeError, + PyErr_Format(gOverloadResolutionException, "cannot resolve method template call for \'%s\'", pytmpl->fTI->fCppName.c_str()); } @@ -855,7 +808,7 @@ static PyGetSetDef tpp_getset[] = { //---------------------------------------------------------------------------- void TemplateProxy::Set(const std::string& cppname, const std::string& pyname, - PyObject* pyclass) { + PyObject* pyclass, bool is_constructor) { // Initialize the proxy for the given 'pyclass.' fSelf = nullptr; fTemplateArgs = nullptr; @@ -863,11 +816,11 @@ void TemplateProxy::Set(const std::string& cppname, const std::string& pyname, fTI->fCppName = cppname; Py_XINCREF(pyclass); fTI->fPyClass = pyclass; + fTI->fIsConstructor = is_constructor; std::vector dummy; - fTI->fNonTemplated = CPPOverload_New(pyname, dummy); - fTI->fTemplated = CPPOverload_New(pyname, dummy); - fTI->fLowPriority = CPPOverload_New(pyname, dummy); + fTI->fOverloads = + CPPOverload_New(pyname, nullptr, dummy); // FIXME: should not be nullptr fVectorCall = (vectorcallfunc)tpp_vectorcall; } @@ -883,23 +836,17 @@ static PyObject* tpp_overload(TemplateProxy* pytmpl, PyObject* args) { interop::TCppScope_t scope = nullptr; interop::TCppMethod_t cppmeth = nullptr; std::string proto; + std::vector ambiguous_candidates; if (PyArg_ParseTuple(args, const_cast("s|i:__overload__"), &sigarg, &want_const)) { want_const = PyTuple_GET_SIZE(args) == 1 ? -1 : want_const; // check existing overloads in order - PyObject* ol = pytmpl->fTI->fNonTemplated->FindOverload(sigarg, want_const); + PyObject* ol = pytmpl->fTI->fOverloads->FindOverload(sigarg, want_const); if (ol) return ol; PyErr_Clear(); - ol = pytmpl->fTI->fTemplated->FindOverload(sigarg, want_const); - if (ol) - return ol; - PyErr_Clear(); - ol = pytmpl->fTI->fLowPriority->FindOverload(sigarg, want_const); - if (ol) - return ol; proto = Utility::ConstructTemplateArgs(nullptr, args); // Propagate the error that occurs if we can't construct the C++ name @@ -910,14 +857,15 @@ static PyObject* tpp_overload(TemplateProxy* pytmpl, PyObject* args) { scope = ((CPPClass*)pytmpl->fTI->fPyClass)->fCppType; cppmeth = interop::GetMethodTemplate(scope, pytmpl->fTI->fCppName, - proto.substr(1, proto.size() - 2)); + proto.substr(1, proto.size() - 2), + ambiguous_candidates); // ???: include_non_templated } else if (PyArg_ParseTuple(args, const_cast("ss:__overload__"), &sigarg, &tmplarg)) { scope = ((CPPClass*)pytmpl->fTI->fPyClass)->fCppType; std::string full_name = std::string(pytmpl->fTI->fCppName) + "<" + tmplarg + ">"; - cppmeth = interop::GetMethodTemplate(scope, full_name, sigarg); + cppmeth = interop::GetMethodTemplate(scope, full_name, sigarg, ambiguous_candidates); // ???: include_non_templated } else if (PyArg_ParseTuple(args, const_cast("O|i:__overload__"), &sigarg_tuple, &want_const)) { PyErr_Clear(); @@ -925,17 +873,10 @@ static PyObject* tpp_overload(TemplateProxy* pytmpl, PyObject* args) { // check existing overloads in order PyObject* ol = - pytmpl->fTI->fNonTemplated->FindOverload(sigarg_tuple, want_const); + pytmpl->fTI->fOverloads->FindOverload(sigarg_tuple, want_const); if (ol) return ol; PyErr_Clear(); - ol = pytmpl->fTI->fTemplated->FindOverload(sigarg_tuple, want_const); - if (ol) - return ol; - PyErr_Clear(); - ol = pytmpl->fTI->fLowPriority->FindOverload(sigarg_tuple, want_const); - if (ol) - return ol; proto.reserve(128); proto.push_back('<'); @@ -955,7 +896,8 @@ static PyObject* tpp_overload(TemplateProxy* pytmpl, PyObject* args) { scope = ((CPPClass*)pytmpl->fTI->fPyClass)->fCppType; cppmeth = interop::GetMethodTemplate(scope, pytmpl->fTI->fCppName, - proto.substr(1, proto.size() - 2)); + proto.substr(1, proto.size() - 2), + ambiguous_candidates); } else { PyErr_Format(PyExc_TypeError, "Unexpected arguments to __overload__"); return nullptr; @@ -979,7 +921,7 @@ static PyObject* tpp_overload(TemplateProxy* pytmpl, PyObject* args) { } else meth = new CPPMethod(scope, cppmeth); - return (PyObject*)CPPOverload_New(pytmpl->fTI->fCppName + proto, meth); + return (PyObject*)CPPOverload_New(pytmpl->fTI->fCppName + proto, scope, meth); } static PyMethodDef tpp_methods[] = {{(char*)"__overload__", diff --git a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.h b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.h index ff9ad99911ef7..d2a926959594d 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.h +++ b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.h @@ -34,12 +34,10 @@ class TemplateInfo { public: std::string fCppName; PyObject* fPyClass; - CPPOverload* fNonTemplated; // holder for non-template overloads - CPPOverload* fTemplated; // holder for templated overloads - CPPOverload* fLowPriority; // low priority overloads such as void*/void** - + CPPOverload* fOverloads; // holder for all overloads TP_DispatchMap_t fDispatchMap; PyObject* fDoc; + bool fIsConstructor; }; typedef std::shared_ptr TP_TInfo_t; @@ -48,9 +46,10 @@ class TemplateProxy { private: friend TemplateProxy* TemplateProxy_New(const std::string& cppname, const std::string& pyname, - PyObject* pyclass); + PyObject* pyclass, + bool isConstructor); void Set(const std::string& cppname, const std::string& pyname, - PyObject* pyclass); + PyObject* pyclass, bool is_constructor); public: // public, as the python C-API works with C structs PyObject_HEAD PyObject* fSelf; // must be first (same layout as CPPOverload) @@ -62,10 +61,10 @@ class TemplateProxy { public: void MergeOverload(CPPOverload* mp); void AdoptMethod(PyCallable* pc); - void AdoptTemplate(PyCallable* pc); PyObject* Instantiate(const std::string& fname, cpyrt_PyArgs_t tmplArgs, size_t nargsf, Utility::ArgPreference, - int* pcnt = nullptr); + int* pcnt = nullptr, + bool include_non_templated = false); private: // private, as the python C-API will handle creation TemplateProxy() = delete; @@ -85,14 +84,15 @@ template inline bool TemplateProxy_CheckExact(T* object) { //- creation ----------------------------------------------------------------- inline TemplateProxy* TemplateProxy_New(const std::string& cppname, const std::string& pyname, - PyObject* pyclass) { + PyObject* pyclass, + bool isConstructor = false) { // Create and initialize a new template method proxy for the class. if (!CPPScope_Check(pyclass)) return nullptr; TemplateProxy* pytmpl = (TemplateProxy*)TemplateProxy_Type.tp_new( &TemplateProxy_Type, nullptr, nullptr); - pytmpl->Set(cppname, pyname, pyclass); + pytmpl->Set(cppname, pyname, pyclass, isConstructor); return pytmpl; } diff --git a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx index 9538a57b46260..208610c60bcee 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx @@ -2,16 +2,19 @@ #include "cpyrt.h" using namespace cppjit; +#include "CPPExcInstance.h" #include "CPPFunction.h" #include "CPPInstance.h" #include "CPPOverload.h" #include "CustomPyTypes.h" +#include "LowLevelViews.h" #include "ProxyWrappers.h" #include "PyCallable.h" #include "PyStrings.h" #include "TemplateProxy.h" #include "TypeManip.h" #include "Utility.h" +#include "cppjit_interop.h" #include "cpyrt/DispatchPtr.h" // Standard @@ -23,6 +26,7 @@ using namespace cppjit; #include #include #include +#include //- data _____________________________________________________________________ typedef std::unordered_map TC2POperatorMapping_t; @@ -30,6 +34,8 @@ static TC2POperatorMapping_t gC2POperatorMapping; static std::set gOpSkip; static std::set gOpRemove; +extern PyTypeObject PyNullPtr_t_Type; + namespace cppjit::cpyrt { // special objects extern PyObject* gNullPtrObject; @@ -239,7 +245,8 @@ bool cpyrt::Utility::AddToClass(PyObject* pyclass, const char* label, if (PyErr_Occurred()) PyErr_Clear(); Py_XDECREF((PyObject*)method); - method = CPPOverload_New(label, pyfunc); + method = CPPOverload_New( + label, interop::GetParentScope(pyfunc->GetMethod().data), pyfunc); PyObject* pylabel = PyUnicode_InternFromString(const_cast(label)); bool isOk = PyType_Type.tp_setattro(pyclass, pylabel, (PyObject*)method) == 0; @@ -262,8 +269,9 @@ static inline cpyrt::PyCallable* BuildOperator(const std::string& lcname, bool reverse = false) { // Helper to find a function with matching signature in 'funcs'. - interop::TCppMethod_t meth = - interop::GetGlobalOperator(scope, lcname, rcname, op); + std::vector ambiguous_candidates; + interop::TCppMethod_t meth = interop::GetGlobalOperator( + scope, lcname, rcname, op, ambiguous_candidates); if (!meth) return nullptr; @@ -396,8 +404,9 @@ cpyrt::Utility::FindBinaryOperator(const std::string& lcname, } fname << lcname << ", " << rcname << ">"; proto << "const " << lcname << "&, const " << rcname; - interop::TCppMethod_t method = - interop::GetMethodTemplate(s_intern, fname.str(), proto.str()); + std::vector ambiguous_candidates; + interop::TCppMethod_t method = interop::GetMethodTemplate( + s_intern, fname.str(), proto.str(), ambiguous_candidates); if (method) pyfunc = new CPPFunction(s_intern, method); } @@ -422,9 +431,10 @@ static inline std::string AnnotationAsText(PyObject* pyobj) { return PyUnicode_AsUTF8(pyobj); } -static bool AddTypeName(std::string& tmpl_name, PyObject* tn, PyObject* arg, - cpyrt::Utility::ArgPreference pref, - int* pcnt = nullptr) { +bool cpyrt::Utility::AddTypeName(std::string& tmpl_name, PyObject* tn, + PyObject* arg, + cpyrt::Utility::ArgPreference pref, + int* pcnt) { // Determine the appropriate C++ type for a given Python type; this is a // helper because it can recurse if the type is list or tuple and needs // matching on std::vector. @@ -470,11 +480,80 @@ static bool AddTypeName(std::string& tmpl_name, PyObject* tn, PyObject* arg, return true; } + if (tn == (PyObject*)&PyComplex_Type) { + tmpl_name.append("std::complex"); + return true; + } + + if (tn == (PyObject*)&PyRange_Type) { + tmpl_name.append("std::initializer_list"); + return true; + } + + if (tn == (PyObject*)&PyNullPtr_t_Type) { + tmpl_name.append("std::nullptr_t"); + return true; + } + + PyObject* array_module = PyImport_ImportModule("array"); + PyObject* array_type = PyObject_GetAttrString(array_module, "array"); + if (tn == array_type) { + PyObject* typecode = PyObject_GetAttrString(arg, "typecode"); + const char* typecode_str = PyUnicode_AsUTF8(typecode); + switch (*typecode_str) { + case 'b': + tmpl_name.append("signed char*"); + break; + case 'B': + tmpl_name.append("unsigned char*"); + break; + case 'u': + tmpl_name.append("wchar_t*"); + break; + case 'h': + tmpl_name.append("signed short*"); + break; + case 'H': + tmpl_name.append("unsigned short*"); + break; + case 'i': + tmpl_name.append("signed int*"); + break; + case 'I': + tmpl_name.append("unsigned int*"); + break; + case 'l': + tmpl_name.append("signed long*"); + break; + case 'L': + tmpl_name.append("unsigned long*"); + break; + case 'q': + tmpl_name.append("signed long long*"); + break; + case 'Q': + tmpl_name.append("unsigned long long*"); + break; + case 'f': + tmpl_name.append("float*"); + break; + case 'd': + tmpl_name.append("double*"); + break; + } + Py_DECREF(array_module); + Py_DECREF(array_type); + Py_DECREF(typecode); + return true; + } + Py_XDECREF(array_module); + Py_XDECREF(array_type); + if (tn == (PyObject*)&PyList_Type || tn == (PyObject*)&PyTuple_Type) { if (arg && PySequence_Size(arg)) { std::string subtype{"std::initializer_list<"}; PyObject* item = PySequence_GetItem(arg, 0); - ArgPreference subpref = pref == kValue ? kValue : kPointer; + ArgPreference subpref = pref == kPointer ? kPointer : kValue; bool ret = AddTypeName(subtype, (PyObject*)Py_TYPE(item), item, subpref); if (ret) { tmpl_name.append(subtype); @@ -491,13 +570,42 @@ static bool AddTypeName(std::string& tmpl_name, PyObject* tn, PyObject* arg, return true; } + if (tn == (PyObject*)&LowLevelView_Type) { + LowLevelView* ll = (LowLevelView*)arg; + tmpl_name.append(ll->fCppTypeName); + if (ll->fBufInfo.ndim > 0) { + for (int i = ll->fBufInfo.ndim; i > 0; i--) + tmpl_name.push_back('*'); + } else { + tmpl_name.push_back('*'); + } + return true; + } + + if (tn == (PyObject*)&CPPExcInstance_Type) { + // this is a execption class + // that is derived from std::exception + // FIXME: merge this code path with CPPScope -> CPPInstance path below + CPPInstance* inst = (CPPInstance*)((CPPExcInstance*)arg)->fCppInstance; + if (inst) { + tmpl_name.append(interop::GetScopedFinalName(inst->ObjectIsA(false))); + if (inst->fFlags & CPPInstance::kIsRValue) + tmpl_name.append("&&"); + else + tmpl_name.append("&"); + return true; + } + } + if (CPPScope_Check(tn)) { - auto cpp_type = interop::GetScopedFinalName(((CPPClass*)tn)->fCppType); - tmpl_name.append(full_scope(cpp_type)); + bool resolved = false; if (arg) { // try to specialize the type match for the given object CPPInstance* pyobj = (CPPInstance*)arg; if (CPPInstance_Check(pyobj)) { + auto cpp_type = interop::GetScopedFinalName( + pyobj->IsSmart() ? SMART_TYPE(pyobj) : ((CPPClass*)tn)->fCppType); + tmpl_name.append(full_scope(cpp_type)); if (pyobj->fFlags & CPPInstance::kIsRValue) tmpl_name.append("&&"); else { @@ -508,8 +616,13 @@ static bool AddTypeName(std::string& tmpl_name, PyObject* tn, PyObject* arg, else if (pref != kValue) tmpl_name.push_back('&'); } + resolved = true; } } + if (!resolved) { + auto cpp_type = interop::GetScopedFinalName(((CPPClass*)tn)->fCppType); + tmpl_name.append(full_scope(cpp_type)); + } return true; } @@ -523,11 +636,19 @@ static bool AddTypeName(std::string& tmpl_name, PyObject* tn, PyObject* arg, return true; } + if (tn == (PyObject*)&TemplateProxy_Type) { + TemplateProxy* tp = (TemplateProxy*)arg; + tmpl_name.append(tp->fTI->fCppName); + if (tp->fTemplateArgs) + tmpl_name.append(PyUnicode_AsUTF8(tp->fTemplateArgs)); + return true; + } + if (arg && PyCallable_Check(arg)) { // annotated/typed Python function PyObject* annot = PyObject_GetAttr(arg, PyStrings::gAnnotations); if (annot) { - if (PyDict_Check(annot) && 1 < PyDict_Size(annot)) { + if (PyDict_Check(annot)) { PyObject* ret = PyDict_GetItemString(annot, "return"); if (ret) { // dict is ordered, with the last value being the return type @@ -601,6 +722,14 @@ static bool AddTypeName(std::string& tmpl_name, PyObject* tn, PyObject* arg, return true; } PyErr_Clear(); + + // We append std::nullptr_t and hope for the best. + // We know this is a pure Python function, but we don't have any + // annotation information. If the receiver takes a pointer this works, + // but if the receiver takes a function reference like `int + // callback(int(&f)(int))`, this fails. + tmpl_name.append("std::nullptr_t"); + return true; } for (auto nn : {PyStrings::gCppName, PyStrings::gName}) { @@ -637,11 +766,9 @@ static bool AddTypeName(std::string& tmpl_name, PyObject* tn, PyObject* arg, return false; } -std::string cpyrt::Utility::ConstructTemplateArgs(PyObject* pyname, - PyObject* tpArgs, - PyObject* args, - ArgPreference pref, - int argoff, int* pcnt) { +std::string cpyrt::Utility::ConstructTemplateArgs( + PyObject* pyname, PyObject* tpArgs, PyObject* args, ArgPreference pref, + int argoff, int* pcnt, bool usingThisParameter) { // Helper to construct the "" part of a templated name // (either for a class or method lookup bool justOne = !PyTuple_CheckExact(tpArgs); @@ -665,9 +792,10 @@ std::string cpyrt::Utility::ConstructTemplateArgs(PyObject* pyname, // some common numeric types (separated out for performance: checking for // __cpp_name__ and/or __name__ is rather expensive) } else { - if (!AddTypeName(tmpl_name, tn, - (args ? PyTuple_GET_ITEM(args, i) : nullptr), pref, - pcnt)) { + if (!AddTypeName( + tmpl_name, tn, (args ? PyTuple_GET_ITEM(args, i) : nullptr), + (usingThisParameter && i == argoff) ? ArgPreference::kNone : pref, + pcnt)) { PyErr_SetString( PyExc_TypeError, "could not construct C++ name from provided template argument."); @@ -699,6 +827,11 @@ static bool AddTypeName(std::vector& types, PyObject* tn, if (tn == (PyObject*)&PyLong_Type) { if (arg) { +#if PY_VERSION_HEX < 0x03000000 + long l = PyInt_AS_LONG(arg); + types.push_back( + interop::GetType((l < INT_MIN || INT_MAX < l) ? "long" : "int").data); +#else PY_LONG_LONG ll = PyLong_AsLongLong(arg); if (ll == (PY_LONG_LONG)-1 && PyErr_Occurred()) { PyErr_Clear(); @@ -717,6 +850,7 @@ static bool AddTypeName(std::vector& types, PyObject* tn, ? ((ll < LONG_MIN || LONG_MAX < ll) ? "long long" : "long") : "int") .data); +#endif } else { types.push_back(interop::GetType("int").data); } @@ -724,6 +858,32 @@ static bool AddTypeName(std::vector& types, PyObject* tn, return true; } +#if PY_VERSION_HEX < 0x03000000 + if (tn == (PyObject*)&PyLong_Type) { + if (arg) { + PY_LONG_LONG ll = PyLong_AsLongLong(arg); + if (ll == (PY_LONG_LONG)-1 && PyErr_Occurred()) { + PyErr_Clear(); + PY_ULONG_LONG ull = PyLong_AsUnsignedLongLong(arg); + if (ull == (PY_ULONG_LONG)-1 && PyErr_Occurred()) { + PyErr_Clear(); + types.push_back(interop::GetType("long") + .data); // still out of range, will fail later + } else + types.push_back(interop::GetType("unsigned long long") + .data); // since already failed long long + } else + types.push_back(interop::GetType((ll < LONG_MIN || LONG_MAX < ll) + ? "long long" + : "long") + .data); + } else + types.push_back(interop::GetType("long").data); + + return true; + } +#endif + if (tn == (PyObject*)&PyFloat_Type) { // special case for floats (Python-speak for double) if from argument (only) types.push_back(interop::GetType(arg ? "double" : "float").data); @@ -737,7 +897,11 @@ static bool AddTypeName(std::vector& types, PyObject* tn, return true; } +#if PY_VERSION_HEX < 0x03000000 + if (tn == (PyObject*)&PyString_Type) { +#else if (tn == (PyObject*)&PyUnicode_Type) { +#endif types.push_back( interop::GetType("std::string", /* enable_slow_lookup */ true).data); return true; diff --git a/bindings/pyroot/cppjit/src/cpyrt/Utility.h b/bindings/pyroot/cppjit/src/cpyrt/Utility.h index acd6936ce7880..d80dedb59ba2e 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Utility.h +++ b/bindings/pyroot/cppjit/src/cpyrt/Utility.h @@ -37,10 +37,13 @@ PyCallable* FindBinaryOperator( // helper for template classes and methods enum ArgPreference { kNone, kPointer, kReference, kValue }; +bool AddTypeName(std::string& tmpl_name, PyObject* tn, PyObject* arg, + ArgPreference pref, int* pcnt = nullptr); std::string ConstructTemplateArgs(PyObject* pyname, PyObject* tpArgs, PyObject* args = nullptr, ArgPreference = kNone, int argoff = 0, - int* pcnt = nullptr); + int* pcnt = nullptr, + bool usingThisParameter = false); std::vector GetTemplateArgsTypes(PyObject* scope, PyObject* tpArgs, PyObject* args = nullptr, ArgPreference = kNone, diff --git a/bindings/pyroot/cppjit/src/cpyrt/cpyrtModule.cxx b/bindings/pyroot/cppjit/src/cpyrt/cpyrtModule.cxx index 880b1466d8ff5..e6706b8771b49 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/cpyrtModule.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/cpyrtModule.cxx @@ -92,7 +92,7 @@ static PyNumberMethods nullptr_as_number = { 0 // nb_inplace_matrix_multiply }; -static PyTypeObject PyNullPtr_t_Type = { +PyTypeObject PyNullPtr_t_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) "nullptr_t", // tp_name sizeof(PyObject), // tp_basicsize 0, // tp_itemsize @@ -237,6 +237,8 @@ PyObject* gBusException = nullptr; PyObject* gSegvException = nullptr; PyObject* gIllException = nullptr; PyObject* gAbrtException = nullptr; +PyObject* gOverloadResolutionException = nullptr; +PyObject* gOverloadAmbiguityException = nullptr; std::unordered_set gPinnedTypes; std::ostringstream gCapturedError; std::streambuf* gOldErrorBuffer = nullptr; @@ -1107,6 +1109,15 @@ PyObject* Init() { gAbrtException = PyErr_NewException((char*)"cppjit.ll.AbortSignal", cppfatal, nullptr); PyModule_AddObject(gThisModule, (char*)"AbortSignal", gAbrtException); + gOverloadResolutionException = PyErr_NewException( + (char*)"cppjit.OverloadResolutionException", nullptr, nullptr); + PyModule_AddObject(gThisModule, (char*)"OverloadResolutionException", + gOverloadResolutionException); + gOverloadAmbiguityException = + PyErr_NewException((char*)"cppjit.OverloadAmbiguityException", + gOverloadResolutionException, nullptr); + PyModule_AddObject(gThisModule, (char*)"OverloadAmbiguityException", + gOverloadAmbiguityException); // gbl namespace is injected in cppjit.py diff --git a/bindings/pyroot/cppjit/src/interop/cppjit_interop.h b/bindings/pyroot/cppjit/src/interop/cppjit_interop.h index 763de03b45fef..90ee79dd57e43 100644 --- a/bindings/pyroot/cppjit/src/interop/cppjit_interop.h +++ b/bindings/pyroot/cppjit/src/interop/cppjit_interop.h @@ -93,7 +93,7 @@ TCppType_t GetType(const std::string& name, bool enable_slow_lookup = false); RPY_EXPORTED bool AppendTypesSlow(const std::string& name, std::vector& types, - TCppScope_t parent = nullptr); + TCppScope_t parent = nullptr, bool append_unknown = false); RPY_EXPORTED TCppType_t GetComplexType(const std::string& element_type); RPY_EXPORTED @@ -248,7 +248,9 @@ bool IsSubclass(TCppScope_t derived, TCppScope_t base); RPY_EXPORTED bool IsSmartPtr(TCppScope_t klass); RPY_EXPORTED -bool GetSmartPtrInfo(TCppScope_t klass, TCppScope_t* raw, TCppMethod_t* deref); +bool GetSmartPtrInfo(const std::string&, TCppScope_t* raw, TCppMethod_t* deref); +RPY_EXPORTED +bool GetSmartPtrInfo(TCppScope_t, TCppScope_t* raw, TCppMethod_t* deref); // calculate offsets between declared and actual type, up-cast: direction > 0; // down-cast: direction < 0 RPY_EXPORTED @@ -322,13 +324,27 @@ RPY_EXPORTED bool IsStaticTemplate(TCppScope_t scope, const std::string& name); RPY_EXPORTED TCppMethod_t GetMethodTemplate(TCppScope_t scope, const std::string& name, - const std::string& proto); + const std::string& proto, + std::vector& ambiguous_candidates, + bool include_non_templated = false); +RPY_EXPORTED +bool IsNonStaticMethod(TCppMethod_t func); +RPY_EXPORTED +TCppMethod_t BestOverloadFunctionMatch( + const std::vector& candidates, const std::string& proto, + std::vector& ambiguous_candidates, + TCppScope_t parent_scope = nullptr, bool is_operator = false); +RPY_EXPORTED +bool IsOperator(TCppScope_t scope); +RPY_EXPORTED +bool IsConversionOperator(TCppScope_t scope); RPY_EXPORTED void GetClassOperators(TCppScope_t klass, const std::string& opname, std::vector& operators); RPY_EXPORTED TCppMethod_t GetGlobalOperator(TCppScope_t scope, const std::string& lc, - const std::string& rc, const std::string& op); + const std::string& rc, const std::string& op, + std::vector& ambiguous_candidates); // method properties --------------------------------------------------------- RPY_EXPORTED diff --git a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx index 0add59261d711..9aa2ad245c52a 100644 --- a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx +++ b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx @@ -247,9 +247,13 @@ static void defineRuntimeHelpers() { " bool is_not_equal(const C1& c1, const C2& c2) { return " "(bool)(c1 != c2); } }", /*silent=*/false); + Cpp::Declare(R"(namespace __cppjit_internal { + class UnknownType {}; + })", + /*silent=*/false); // helper for multiple inheritance - Cpp::Declare("namespace __cppjit_internal { struct Sep; }", + Cpp::Declare("namespace __cppjit_internal { struct Sep {}; }", /*silent=*/false); } @@ -303,7 +307,7 @@ static inline char* cppstring_to_cstring(const std::string& cppstr) { bool interop::Compile(const std::string& code, bool silent) { std::lock_guard Lock(InterOpMutex); // Declare returns an enum which equals 0 on success - return !Cpp::Declare(code.c_str(), silent); + return !Cpp::Declare(code.c_str(), false); } void interop::LockInterpreter() { InterOpMutex.lock(); } @@ -404,7 +408,14 @@ interop::TCppType_t interop::GetRealType(TCppType_t type) { interop::TCppType_t interop::GetPointerType(TCppType_t type) { std::lock_guard Lock(InterOpMutex); - return Cpp::GetPointerType(type); + Cpp::ValueKind ref = Cpp::GetValueKind(type); + TCppType_t pointer_type = type; + pointer_type = Cpp::GetPointerType(Cpp::GetNonReferenceType(type)); + if (ref == Cpp::ValueKind::LValue) + return Cpp::GetReferencedType(pointer_type, false); + if (ref == Cpp::ValueKind::RValue) + return Cpp::GetReferencedType(pointer_type, true); + return pointer_type; } interop::TCppType_t interop::GetReferencedType(TCppType_t type, bool rvalue) { @@ -521,7 +532,8 @@ static bool is_identifier(std::string_view s) { // returns true if no new type was added. bool interop::AppendTypesSlow(const std::string& name, std::vector& types, - interop::TCppScope_t parent) { + interop::TCppScope_t parent, + bool append_unknown) { // Add no new type if string is empty if (name.empty()) @@ -549,8 +561,8 @@ bool interop::AppendTypesSlow(const std::string& name, }; std::string resolved_name = name; - replace_all(resolved_name, "std::initializer_list<", - "std::vector<"); // replace initializer_list with vector + // replace_all(resolved_name, "std::initializer_list<", "std::vector<"); // + // replace initializer_list with vector // If we have a single identifier, we don't need anything complicated. // Try scoped lookup first (catches type aliases / nested types declared @@ -579,7 +591,7 @@ bool interop::AppendTypesSlow(const std::string& name, std::string code = "template struct __cppjit_interop_AppendTypesSlow {};\n"; if (!struct_count) - Cpp::Declare(code.c_str(), /*silent=*/true); // initialize the trampoline + Cpp::Declare(code.c_str(), /*silent=*/false); // initialize the trampoline // Perform the lookup from within the innermost (reopenable) namespace // enclosing `parent` -- `parent` itself if it is a namespace, else the @@ -667,8 +679,14 @@ bool interop::AppendTypesSlow(const std::string& name, } if (!type) { - types.clear(); - return true; + static interop::TCppType_t unknown_type = Cpp::GetTypeFromScope( + interop::GetScope("__cppjit_internal::UnknownType")); + if (append_unknown) { + types.emplace_back(unknown_type.data); + continue; + } else { + return true; + } } if (is_integral(i)) @@ -710,9 +728,10 @@ interop::TCppType_t interop::GetType(const std::string& name, } if (!enable_slow_lookup) { - if (name.find("::") != std::string::npos) - throw std::runtime_error( - "Calling interop::GetType with qualified name '" + name + "'\n"); + // if (name.find("::") != std::string::npos) + // throw std::runtime_error("Calling interop::GetType with qualified + // name '" + // + name + "'\n"); return nullptr; } @@ -1436,8 +1455,11 @@ bool interop::GetSmartPtrInfo(TCppScope_t klass, TCppScope_t* raw, // type not being an array. Its return type is dependent, so it has // to be instantiated (it takes no arguments) before the pointee // type can be determined - and before it can be called. - if (Cpp::IsTemplatedFunction(ops[0])) - ops[0] = Cpp::BestOverloadFunctionMatch(ops, {}, {}); + if (Cpp::IsTemplatedFunction(ops[0])) { + std::vector ambiguous_candidates; + ops[0] = Cpp::BestOverloadFunctionMatch(ops, {}, {}, + ambiguous_candidates); + } if (!ops[0]) return false; } @@ -1449,6 +1471,20 @@ bool interop::GetSmartPtrInfo(TCppScope_t klass, TCppScope_t* raw, return (!deref || *deref) && (!raw || *raw); } +bool interop::GetSmartPtrInfo(const std::string& tname, TCppScope_t* raw, + TCppMethod_t* deref) { + // TODO: We can directly accept scope instead of name + const std::string& rn = ResolveName(tname); + if (gSmartPtrTypes.find(rn.substr(0, rn.find("<"))) == gSmartPtrTypes.end()) + return false; + + if (!raw && !deref) + return true; + + TCppScope_t scope = interop::GetScope(rn); + return GetSmartPtrInfo(scope, raw, deref); +} + // type offsets -------------------------------------------------------------- ptrdiff_t interop::GetBaseOffset(TCppScope_t derived, TCppScope_t base, TCppObject_t /*address*/, int direction, @@ -1749,9 +1785,235 @@ bool interop::IsStaticTemplate(TCppScope_t scope, const std::string& name) { return is_static; } -interop::TCppMethod_t interop::GetMethodTemplate(TCppScope_t scope, - const std::string& name, - const std::string& proto) { +bool interop::IsNonStaticMethod(interop::TCppMethod_t func) { + return Cpp::IsMethod(func) && !Cpp::IsStaticMethod(func); +} + +bool IsTemplateParamEquivalentTypes(interop::TCppType_t typ1, + interop::TCppType_t typ2, + Cpp::QualKind& qual, Cpp::ValueKind& ref, + bool& pointer) { + qual = Cpp::QualKind::None; + ref = Cpp::ValueKind::None; + pointer = false; + + ref = ref ^ Cpp::GetValueKind(typ1); + ref = ref ^ Cpp::GetValueKind(typ2); + typ2 = Cpp::IsReferenceType(typ2) ? Cpp::GetNonReferenceType(typ2) : typ2; + + pointer ^= Cpp::IsPointerType(typ1); + pointer ^= Cpp::IsPointerType(typ2); + typ2 = Cpp::IsPointerType(typ2) ? Cpp::GetPointeeType(typ2) : typ2; + + if (Cpp::IsTemplateParmType(typ2)) + return true; // we assume template parameter type is equivalent to any type + return false; +} + +namespace { +interop::TCppMethod_t BestOverloadFunctionMatch( + const std::vector& candidates, + std::vector& arg_types, + const std::vector& templ_params, + std::vector& ambiguous_candidates, + interop::TCppScope_t parent_scope, bool is_operator) { + std::lock_guard Lock(InterOpMutex); + + interop::TCppMethod_t cppmeth = Cpp::BestOverloadFunctionMatch( + candidates, templ_params, arg_types, ambiguous_candidates, is_operator); + if (cppmeth) + return cppmeth; + + interop::TCppMethod_t potential_cppmeth = nullptr; + bool potential_cppmeth_ambiguous = false; + for (auto fn : candidates) { + size_t required_arg_count = Cpp::GetFunctionRequiredArgs(fn); + size_t arg_count = Cpp::GetFunctionNumArgs(fn); + bool skipThis = + interop::IsNonStaticMethod(fn) && !interop::IsConstructor(fn); + if (skipThis) { + arg_count++; + required_arg_count++; + } + if (arg_types.size() > arg_count || arg_types.size() < required_arg_count) + continue; + + // we track potential_cppmeth based on number of args only + // and not the type of these arguments + if (!potential_cppmeth) + potential_cppmeth = fn; + else + potential_cppmeth_ambiguous = true; + + size_t I = 0; + for (auto& arg : arg_types) { + if (I == 0 && skipThis) { + // ignore the this parameter + I++; + continue; + } + Cpp::QualKind qual = Cpp::QualKind::None; + Cpp::ValueKind ref = Cpp::ValueKind::None; + bool pointer = false; + static interop::TCppScope_t initializer_list_scope = + interop::GetScopeFromType(interop::GetTypeFromScope( + interop::GetScope("std::initializer_list"))); + static interop::TCppScope_t vector_scope = + interop::GetScope("std::vector"); + static interop::TCppType_t string_type = interop::GetRealType( + interop::GetTypeFromScope(interop::GetScope("std::string"))); + static interop::TCppType_t wstring_type = interop::GetRealType( + interop::GetTypeFromScope(interop::GetScope("std::wstring"))); + static interop::TCppType_t char_type = interop::GetType("char"); + static interop::TCppType_t char_pointer_type = + interop::GetPointerType(char_type); + static interop::TCppType_t wchar_pointer_type = + interop::GetPointerType(interop::GetType("wchar_t")); + interop::TCppType_t fn_arg_type = + Cpp::GetFunctionArgType(fn, skipThis ? I - 1 : I); + assert(string_type); + interop::TCppType_t old_type = nullptr; + if (Cpp::IsEquivalentTypes(arg.m_Type, fn_arg_type, qual, ref, pointer) && + pointer) { + // Only promotion to pointer should be allowed. + old_type = arg.m_Type; + arg.m_Type = interop::GetPointerType(arg.m_Type).data; + if (ref != Cpp::ValueKind::None) + arg.m_Type = Cpp::GetNonReferenceType(arg.m_Type).data; + } else if (Cpp::IsEquivalentTypes(string_type, arg.m_Type, qual, ref, + pointer) && + (Cpp::IsEquivalentTypes(char_pointer_type, fn_arg_type, qual, + ref, pointer)) && + (Cpp::IsPointerType(Cpp::GetNonReferenceType( + interop::ResolveType(fn_arg_type))))) { + // promote std::string to char* + old_type = arg.m_Type; + arg.m_Type = char_pointer_type.data; + } else if (Cpp::IsEquivalentTypes(string_type, arg.m_Type, qual, ref, + pointer) && + (Cpp::IsEquivalentTypes(char_type, fn_arg_type, qual, ref, + pointer))) { + // promote std::string to char + // is this fine? + old_type = arg.m_Type; + arg.m_Type = char_type.data; + } else if (Cpp::IsEquivalentTypes(string_type, fn_arg_type, qual, ref, + pointer) && + (Cpp::IsEquivalentTypes(char_pointer_type, arg.m_Type, qual, + ref, pointer)) && + (Cpp::IsPointerType(Cpp::GetNonReferenceType( + interop::ResolveType(arg.m_Type))))) { + // promote char* to std::string + old_type = arg.m_Type; + arg.m_Type = string_type.data; + } else if (Cpp::IsEquivalentTypes(string_type, arg.m_Type, qual, ref, + pointer) && + (Cpp::IsEquivalentTypes(wchar_pointer_type, fn_arg_type, qual, + ref, pointer)) && + (Cpp::IsPointerType(Cpp::GetNonReferenceType( + interop::ResolveType(fn_arg_type))))) { + // promote std::string to wchar_t* + old_type = arg.m_Type; + arg.m_Type = wchar_pointer_type.data; + } else if (Cpp::IsEquivalentTypes(wstring_type, fn_arg_type, qual, ref, + pointer) && + (Cpp::IsEquivalentTypes(wchar_pointer_type, arg.m_Type, qual, + ref, pointer)) && + (Cpp::IsPointerType(Cpp::GetNonReferenceType( + interop::ResolveType(arg.m_Type))))) { + // promote wchar_t* to std::wstring + old_type = arg.m_Type; + arg.m_Type = wstring_type.data; + // } else if (Cpp::IsEquivalentTypes(string_type, arg.m_Type, qual, ref, + // pointer) && (Cpp::IsEquivalentTypes(wstring_type, fn_arg_type, qual, + // ref, pointer))) { + // // promote std::string to std::wstring + // old_type = arg.m_Type; + // arg.m_Type = wstring_type.data; + // } else if (Cpp::IsEquivalentTypes(char_pointer_type, arg.m_Type, + // qual, ref, pointer) && (Cpp::IsEquivalentTypes(wstring_type, + // fn_arg_type, qual, ref, pointer)) && + // (Cpp::IsPointerType(Cpp::GetNonReferenceType(interop::ResolveType(arg.m_Type)))) + // && + // (Cpp::IsPointerType(Cpp::GetNonReferenceType(interop::ResolveType(fn_arg_type))))) + // { + // // promote char* to wchar_t* + // old_type = arg.m_Type; + // arg.m_Type = wstring_type.data; + } else if (auto ArgD = interop::GetScopeFromType(arg.m_Type), + ParamD = interop::GetScopeFromType(fn_arg_type); + (ArgD && ParamD) && + Cpp::GetTemplatedDecl(ArgD) == Cpp::GetTemplatedDecl(ParamD) && + Cpp::GetTemplatedDecl(ArgD) == initializer_list_scope && + !Cpp::IsTemplateParmType(fn_arg_type)) { + // cast from std::initializer_list to + // std::initializer_list where T is the actual concrete C++ side type + // for this function + old_type = arg.m_Type; + arg.m_Type = fn_arg_type.data; + } else if (IsTemplateParamEquivalentTypes(arg.m_Type, fn_arg_type, qual, + ref, pointer)) { + // this is a templated type, we want to cast references/pointers + old_type = arg.m_Type; + if (ref != Cpp::ValueKind::None) { + if (Cpp::GetValueKind(arg.m_Type) == ref) { + // remove reference + arg.m_Type = Cpp::GetNonReferenceType(arg.m_Type).data; + } else { + // create a reference + arg.m_Type = + Cpp::GetReferencedType(Cpp::GetNonReferenceType(arg.m_Type), + ref == Cpp::ValueKind::RValue) + .data; + } + } + if (pointer) { + if (Cpp::IsPointerType(arg.m_Type)) { + // remove pointer + arg.m_Type = Cpp::GetPointeeType(arg.m_Type).data; + } else { + // add pointer + arg.m_Type = Cpp::GetPointerType(arg.m_Type).data; + } + } + if (old_type == arg.m_Type) { + assert(false && "This should be unreachable according to the author " + "of this code"); + old_type = nullptr; + } + } + if (old_type) { + if (!cppmeth) { + // FIXME: this call should happen outside the inner for loop? + // FIXME: How to handle ambiguous_candidates in this case? + cppmeth = Cpp::BestOverloadFunctionMatch( + candidates, {}, arg_types, ambiguous_candidates, is_operator); + } else if (interop::TCppMethod_t new_meth = + Cpp::BestOverloadFunctionMatch(candidates, {}, arg_types, + ambiguous_candidates, + is_operator); + new_meth && new_meth != cppmeth) { + return nullptr; // ambiguous, raise diagnostic + } + arg.m_Type = old_type.data; + } + I++; + } + } + + // proper argument type match failed + // trying improper argument length match + if (!cppmeth && !potential_cppmeth_ambiguous && + !Cpp::IsTemplatedFunction(potential_cppmeth)) + cppmeth = potential_cppmeth; + return cppmeth; +} +} // namespace + +interop::TCppMethod_t interop::GetMethodTemplate( + TCppScope_t scope, const std::string& name, const std::string& proto, + std::vector& ambiguous_candidates, + bool include_non_templated) { std::string pureName; std::string explicit_params; @@ -1767,7 +2029,16 @@ interop::TCppMethod_t interop::GetMethodTemplate(TCppScope_t scope, std::lock_guard Lock(InterOpMutex); std::vector unresolved_candidate_methods; - Cpp::GetClassTemplatedMethods(pureName, scope, unresolved_candidate_methods); + { + std::lock_guard Lock(InterOpMutex); + Cpp::GetClassTemplatedMethods(pureName, scope, + unresolved_candidate_methods); + if (include_non_templated) { + auto fns = Cpp::GetFunctionsUsingName(scope, pureName); + for (auto i : fns) + unresolved_candidate_methods.push_back(i); + } + } if (unresolved_candidate_methods.empty() && name.find("operator") == 0) { // try operators interop::GetClassOperators(scope, pureName, unresolved_candidate_methods); @@ -1776,35 +2047,54 @@ interop::TCppMethod_t interop::GetMethodTemplate(TCppScope_t scope, // cpyrt assumes that we attempt instantiation here std::vector arg_types; std::vector templ_params; - interop::AppendTypesSlow(proto, arg_types, scope); - interop::AppendTypesSlow(explicit_params, templ_params, scope); + interop::AppendTypesSlow(proto, arg_types, scope, true); + interop::AppendTypesSlow(explicit_params, templ_params, scope, true); + + std::vector candidate_methods; + for (auto fn : unresolved_candidate_methods) + if (!interop::IsDeletedMethod(fn)) + candidate_methods.push_back(fn); + interop::TCppMethod_t cppmeth = nullptr; - cppmeth = Cpp::BestOverloadFunctionMatch(unresolved_candidate_methods, - templ_params, arg_types); - - // If overload resolution failed but explicit template arguments were - // supplied, fall back to direct template-argument substitution: ask Sema - // to instantiate each candidate with the explicit args. Sema's SFINAE - // rejects overloads whose substitution fails (e.g. the initializer_list - // form of std::make_any with non-init-list explicit args), so iterating - // gives back exactly the viable specialisation. The wrapper-side argument - // conversion then handles e.g. taking the address of an instance when the - // substituted parameter is a pointer. - if (!cppmeth && !templ_params.empty()) { - for (const auto& cand : unresolved_candidate_methods) { - if (Cpp::DeclRef spec = Cpp::InstantiateTemplate( - TCppScope_t(cand.data), templ_params.data(), templ_params.size(), - /*instantiate_body=*/false)) { - cppmeth = spec.data; - break; - } - } + { + cppmeth = ::BestOverloadFunctionMatch(candidate_methods, arg_types, + templ_params, ambiguous_candidates, + scope, /*is_operator=*/false); + } + if (cppmeth) + return cppmeth; + + if (unresolved_candidate_methods.size() == 1 && !templ_params.empty()) { + cppmeth = Cpp::InstantiateTemplate( + TCppScope_t(unresolved_candidate_methods[0].data), + templ_params.data(), templ_params.size(), + /*instantiate_body=*/false) + .data; } return TCppMethod_t(cppmeth.data); // if it fails, use Sema to propogate info about why it failed (DeductionInfo) } +interop::TCppMethod_t interop::BestOverloadFunctionMatch( + const std::vector& candidates, + const std::string& proto, + std::vector& ambiguous_candidates, + TCppScope_t parent_scope, bool is_operator) { + std::lock_guard Lock(InterOpMutex); + std::vector arg_types; + if (!proto.empty() && + interop::AppendTypesSlow(proto, arg_types, parent_scope, true)) + return nullptr; + std::vector non_deleted; + for (auto fn : candidates) + if (!interop::IsDeletedMethod(fn)) + non_deleted.push_back(fn); + return ::BestOverloadFunctionMatch(non_deleted, arg_types, {}, + ambiguous_candidates, parent_scope, + is_operator); +} + static inline bool is_basic_string_of(const std::string& n, const char* charT) { // Match "std::basic_string& operators) { @@ -1846,14 +2143,16 @@ void interop::GetClassOperators(interop::TCppScope_t klass, /*kind=*/Cpp::OperatorArity::kBoth); } -interop::TCppMethod_t interop::GetGlobalOperator(TCppScope_t scope, - const std::string& lc, - const std::string& rc, - const std::string& opname) { +interop::TCppMethod_t interop::GetGlobalOperator( + interop::TCppScope_t scope, const std::string& lc, const std::string& rc, + const std::string& opname, + std::vector& ambiguous_candidates) { std::string rc_type = type_remap(rc, lc); std::string lc_type = type_remap(lc, rc); - std::vector overloads; + std::vector unresolved_candidate_methods; + std::vector overloads; + std::lock_guard Lock(InterOpMutex); Cpp::GetOperator(scope, Cpp::GetOperatorFromSpelling(opname), overloads, /*kind=*/Cpp::OperatorArity::kBoth); @@ -1878,8 +2177,8 @@ interop::TCppMethod_t interop::GetGlobalOperator(TCppScope_t scope, else return nullptr; } - interop::TCppMethod_t cppmeth = - Cpp::BestOverloadFunctionMatch(overloads, {}, arg_types); + interop::TCppMethod_t cppmeth = Cpp::BestOverloadFunctionMatch( + overloads, {}, arg_types, ambiguous_candidates, true); if (cppmeth) return cppmeth; return nullptr; diff --git a/bindings/pyroot/cppjit/test/test_advancedcpp.py b/bindings/pyroot/cppjit/test/test_advancedcpp.py index b59ddc59e1af4..fd45b1d46f632 100644 --- a/bindings/pyroot/cppjit/test/test_advancedcpp.py +++ b/bindings/pyroot/cppjit/test/test_advancedcpp.py @@ -673,6 +673,9 @@ def test17_assign_to_return_byref(self): assert len(a) == 1 assert a[0].m_i == 33 + @mark.xfail( + reason="New Overload Resolution: Multiple possible converters available" + ) def test18_math_converters(self): """Test operator int/long/double incl. typedef""" @@ -707,6 +710,7 @@ def test19_comparator(self): assert a.__eq__(a) == False assert b.__eq__(b) == False + @mark.xfail(reason="New overload resolution: constness of methods") def test20_overload_order_with_proper_return(self): """Test return type against proper overload w/ const and covariance""" diff --git a/bindings/pyroot/cppjit/test/test_conversions.py b/bindings/pyroot/cppjit/test/test_conversions.py index 803d48b5cfc0b..c077f6d5e1ce5 100644 --- a/bindings/pyroot/cppjit/test/test_conversions.py +++ b/bindings/pyroot/cppjit/test/test_conversions.py @@ -1,6 +1,6 @@ import py from pytest import mark, raises -from support import IS_CLANG_REPL, IS_CLING, IS_MAC, setup_make +from support import setup_make currpath = py.path.local(__file__).dirpath() test_dct = "conversions_cxx" @@ -97,9 +97,8 @@ def test03_error_handling(self): gc.collect() assert CC.s_count == 0 - @mark.xfail( - condition=IS_MAC or IS_CLING, run=IS_CLANG_REPL, reason="Crashes on Cling" - ) + # @mark.xfail(run=IS_CLANG_REPL, condition = IS_MAC or IS_CLING, reason = "Crashes on Cling") + @mark.xfail(strict=True, reason="FIXME: New Overload Resolution") def test04_implicit_conversion_from_tuple(self): """Allow implicit conversions from tuples as arguments {}-like""" diff --git a/bindings/pyroot/cppjit/test/test_cpp11features.py b/bindings/pyroot/cppjit/test/test_cpp11features.py index 0a475572bae85..53c5cdf3f7dfb 100644 --- a/bindings/pyroot/cppjit/test/test_cpp11features.py +++ b/bindings/pyroot/cppjit/test/test_cpp11features.py @@ -495,7 +495,7 @@ def test15_unique_ptr_template_deduction(self): assert not not uptr_out uptr_in = cppjit.gbl.std.make_unique["int"]() - with raises(ValueError): # not an RValue + with raises(TypeError): # not an RValue # TODO: This should be Overload Error cppjit.gbl.UniqueTempl.returnptr[int](uptr_in) @mark.xfail(condition=IS_MAC, reason="Fails on Mac platforms") @@ -524,7 +524,7 @@ def test16_unique_ptr_moves(self): up = ns.returnptr_move(cppjit.gbl.std.move(up)) assert up and up.get()[0] == 42 - with raises(TypeError): + with raises(cppjit.OverloadResolutionException): ns.returnptr_move(up) def test17_unique_ptr_data(self): diff --git a/bindings/pyroot/cppjit/test/test_crossinheritance.py b/bindings/pyroot/cppjit/test/test_crossinheritance.py index 05ffb20a998eb..10e2394c800da 100644 --- a/bindings/pyroot/cppjit/test/test_crossinheritance.py +++ b/bindings/pyroot/cppjit/test/test_crossinheritance.py @@ -1015,14 +1015,10 @@ def __init__(self, val1=None, val2=None, val3=None, nArgs=3): a1 = val1 is not None and (val1,) or () a2 = val2 is not None and (val2,) or () a3 = val3 is not None and (val3,) or () - if nArgs == 3: - super(MyPyDerived, self).__init__(a1, a2, a3) - elif nArgs == 0: + if nArgs == 0: super(MyPyDerived, self).__init__() - elif nArgs == 1: - super(MyPyDerived, self).__init__(a1) - elif nArgs == 2: - super(MyPyDerived, self).__init__(a1, a2) + else: + super(MyPyDerived, self).__init__(a1, a2, a3) def x(self): return 16 @@ -1116,7 +1112,7 @@ def test24_non_copyable(self): class NoCopyNoMove { public: - NoCopyNoMove() = delete; + NoCopyNoMove() = default; NoCopyNoMove(const NoCopyNoMove&) = delete; NoCopyNoMove(NoCopyNoMove&&) = delete; NoCopyNoMove& operator=(const NoCopyNoMove&) = delete; @@ -1245,21 +1241,21 @@ class PyDerived(kls): def __init__(self): super(PyDerived, self).__init__() - with raises(TypeError): + with raises(cppjit.OverloadResolutionException): PyDerived() class PyDerived(cppjit.multi(kls, ns.Simple)): def __init__(self): super(PyDerived, self).__init__() - with raises(TypeError): + with raises(cppjit.OverloadResolutionException): PyDerived() class PyDerived(cppjit.multi(ns.Simple, kls)): def __init__(self): super(PyDerived, self).__init__() - with raises(TypeError): + with raises(cppjit.OverloadResolutionException): PyDerived() def test27_interfaces(self): diff --git a/bindings/pyroot/cppjit/test/test_datatypes.py b/bindings/pyroot/cppjit/test/test_datatypes.py index 5e508d32dcee6..b6f0f17682abf 100644 --- a/bindings/pyroot/cppjit/test/test_datatypes.py +++ b/bindings/pyroot/cppjit/test/test_datatypes.py @@ -155,17 +155,17 @@ def test01_instance_data_read_access(self): assert COd(2).m_size == 2 assert COd(2).m_cplx == 7.0 + 42j - assert COd(3.14).m_size == 42 - assert COd(3.14).m_cplx == 3.14 + 0j + # assert COd(3.14).m_size == 42 + # assert COd(3.14).m_cplx == 3.14+0j assert COd(9.0 + 7j).m_size == 42 assert COd(9.0 + 7j).m_cplx == 9.0 + 7j assert COf(2).m_size == 2 assert COf(2).m_cplx == scf(7, 42) - assert COf(3.14).m_size == 42 - assert COf(3.14).m_cplx == scf(3.14, 0) - assert COf(9.0 + 7j).m_size == 42 - assert COf(9.0 + 7j).m_cplx == scf(9.0, 7.0) + # assert COf(3.14).m_size == 42 + # assert COf(3.14).m_cplx == scf(3.14, 0) + # assert COf(9.+7j).m_size == 42 + # assert COf(9.+7j).m_cplx == scf(9., 7.) # reading of enum types assert c.m_enum == CppjitTestData.kNothing @@ -474,9 +474,8 @@ def test03_array_passing(self): # NULL/nullptr passing (will use short*) assert not c.pass_array(0) raises(Exception, c.pass_array(0).__getitem__, 0) # raises SegfaultException - assert raises(TypeError, c.pass_array, None) - assert not c.pass_array(cppjit.nullptr) - raises(Exception, c.pass_array(cppjit.nullptr).__getitem__, 0) # id. id. + assert raises(cppjit.OverloadResolutionException, c.pass_array, None) + assert raises(cppjit.OverloadAmbiguityException, c.pass_array, cppjit.nullptr) c.__destruct__() @@ -1703,7 +1702,7 @@ def __init__(self): super(Derived, self).__init__() self.execute = self.xyz - def xyz(self): + def xyz(self) -> cppjit.gbl.std.string: return "xyz" d = Derived() @@ -2487,6 +2486,7 @@ def test46_small_int_enums(self): assert ns.func_int8() == -1 assert ns.func_uint8() == 255 + @mark.xfail(reason="New Overload Resolution") def test47_hidden_name_enum(self): """Usage of hidden name enum""" diff --git a/bindings/pyroot/cppjit/test/test_doc_features.py b/bindings/pyroot/cppjit/test/test_doc_features.py index 82b6a8c58ba29..cc556ccb42191 100644 --- a/bindings/pyroot/cppjit/test/test_doc_features.py +++ b/bindings/pyroot/cppjit/test/test_doc_features.py @@ -743,7 +743,7 @@ def test09_templated_function(self): assert raises(TypeError, mul[int, int], 1, 1.0) assert type(mul[int, int, float](1, 1)) == float # TODO: the following error message is rather confusing :( - assert raises(TypeError, mul[int, int], 1, "a") + assert raises(cppjit.OverloadResolutionException, mul[int, int], 1, "a") assert mul["double, double, double"](1.0, 5) == 5.0 diff --git a/bindings/pyroot/cppjit/test/test_fragile.py b/bindings/pyroot/cppjit/test/test_fragile.py index 4c348344030d9..1732aae63fedb 100644 --- a/bindings/pyroot/cppjit/test/test_fragile.py +++ b/bindings/pyroot/cppjit/test/test_fragile.py @@ -94,8 +94,8 @@ def test03_arguments(self): assert fragile.D().check() == ord("D") d = fragile.D() - raises(TypeError, d.overload, None) - raises(TypeError, d.overload, None, None, None) + raises(cppjit.OverloadResolutionException, d.overload, None) + raises(cppjit.OverloadResolutionException, d.overload, None, None, None) d.overload("a") d.overload(1) @@ -204,35 +204,39 @@ def test10_documentation(self): d = fragile.D() try: - d.check(None) # raises TypeError + d.check(None) # raises OverloadResolutionException assert 0 - except TypeError as e: - assert "fragile::D::check()" in str(e) - assert "TypeError: takes at most 0 arguments (1 given)" in str(e) - assert "TypeError: takes at least 2 arguments (1 given)" in str(e) + except cppjit.OverloadResolutionException as e: + err_msg = str(e).replace(" ", "") + assert "fragile::D::check()" in err_msg + assert "fragile::D::check(int, int)".replace(" ", "") in err_msg + assert ( + "Deduced Argument Types: (::fragile::D&, NoneType)".replace(" ", "") + in err_msg + ) try: - d.overload(None) # raises TypeError + d.overload(None) # raises OverloadResolutionException assert 0 - except TypeError as e: + except cppjit.OverloadResolutionException as e: # TODO: pypy-c does not indicate which argument failed to convert, CPython does # likewise there are still minor differences in descriptiveness of messages err_msg = str(e).replace(" ", "") assert "fragile::D::overload()" in err_msg - assert "TypeError: takes at most 0 arguments (1 given)" in str(e) assert "fragile::D::overload(fragile::no_such_class*)" in err_msg - # assert "no converter available for 'fragile::no_such_class*'" in str(e) assert ( "void fragile::D::overload(char, int i = 0)".replace(" ", "") in err_msg ) - # assert "char or small int type expected" in str(e) assert ( "void fragile::D::overload(int, fragile::no_such_class * p = 0)".replace( " ", "" ) in err_msg ) - # assert "int/long conversion expects an integer object" in str(e) + assert ( + "Deduced Argument Types: (::fragile::D&, NoneType)".replace(" ", "") + in err_msg + ) j = fragile.J() assert fragile.J.method1.__doc__ == j.method1.__doc__ diff --git a/bindings/pyroot/cppjit/test/test_leakcheck.py b/bindings/pyroot/cppjit/test/test_leakcheck.py index 6e02f089d5aa6..4e77c3adde50a 100644 --- a/bindings/pyroot/cppjit/test/test_leakcheck.py +++ b/bindings/pyroot/cppjit/test/test_leakcheck.py @@ -68,7 +68,7 @@ def check_func(self, scope, func, *args, **kwds): self.runit_template(N, scope, func, tmpl_args, *args, **kwds) gc.collect() - assert len(gc.get_objects()) == pre + assert len(gc.get_objects()) <= pre # XXX if last < self.process.memory_info().rss: fail += 1 diff --git a/bindings/pyroot/cppjit/test/test_overloads.py b/bindings/pyroot/cppjit/test/test_overloads.py index 9c774a8fe54c9..5125853c29379 100644 --- a/bindings/pyroot/cppjit/test/test_overloads.py +++ b/bindings/pyroot/cppjit/test/test_overloads.py @@ -89,7 +89,7 @@ def test03_fragile_class_based_overloads(self): assert more_overloads().call(bb) == "bb_ol" assert more_overloads().call(cc_ol()) == "cc_ol" dd = cppjit.bind_object(cppjit.nullptr, dd_ol) - with raises(TypeError): + with raises(ReferenceError): more_overloads().call(dd) dd = cppjit.gbl.get_dd_ol() assert more_overloads().call(dd) == "dd_ol" @@ -102,10 +102,12 @@ def test04_fully_fragile_overloads(self): more_overloads2 = cppjit.gbl.more_overloads2 bb = cppjit.bind_object(cppjit.nullptr, cppjit.gbl.bb_ol) - assert more_overloads2().call(bb) == "bb_olptr" + with raises(ReferenceError): + more_overloads2().call(bb) dd = cppjit.bind_object(cppjit.nullptr, cppjit.gbl.dd_ol) - assert more_overloads2().call(dd, 1) == "dd_olptr" + with raises(ReferenceError): + more_overloads2().call(dd, 1) def test05_array_overloads(self): """Test functions overloaded on different arrays""" @@ -260,10 +262,10 @@ class MyClass3 { with raises(ns.ConfigFileNotFoundError): ns.MyClass1("some_file") - with raises(TypeError): + with raises(ns.ConfigFileNotFoundError): ns.MyClass2("some_file") - with raises(TypeError): + with raises(cppjit.OverloadResolutionException): ns.MyClass3("some_file") def test11_deep_inheritance(self): diff --git a/bindings/pyroot/cppjit/test/test_pythonify.py b/bindings/pyroot/cppjit/test/test_pythonify.py index 0355b0adb469a..7c9bbcfc6145e 100644 --- a/bindings/pyroot/cppjit/test/test_pythonify.py +++ b/bindings/pyroot/cppjit/test/test_pythonify.py @@ -65,7 +65,7 @@ def test03_calling_static_functions(self): raises(TypeError, example01_class.staticAddOneToInt, 1, []) raises(TypeError, example01_class.staticAddOneToInt, 1.0) - raises(TypeError, example01_class.staticAddOneToInt, maxint32 + 1) + raises(ValueError, example01_class.staticAddOneToInt, maxint32 + 1) res = example01_class.staticAddToDouble(0.09) assert res == 0.09 + 0.01 @@ -123,7 +123,7 @@ def test04_constructing_and_calling(self): res = instance.addToStringValue("-12") # TODO: this leaks assert res == "30" - res = instance.staticAddOneToInt(pylong(1)) + res = type(instance).staticAddOneToInt(pylong(1)) # FIXME assert res == 2 instance.__destruct__() diff --git a/bindings/pyroot/cppjit/test/test_regression.py b/bindings/pyroot/cppjit/test/test_regression.py index cbd5f566667d4..11d3a23c1e38f 100644 --- a/bindings/pyroot/cppjit/test/test_regression.py +++ b/bindings/pyroot/cppjit/test/test_regression.py @@ -324,10 +324,10 @@ def test13_char_star_over_char(self): std::string call(char) { return "char"; } } - namespace csoc2 { - std::string call(char) { return "char"; } - std::string call(const char*) { return "const char*"; } - } + // namespace csoc2 { + // std::string call(char) { return "char"; } + // std::string call(const char*) { return "const char*"; } + // } namespace csoc3 { std::string call(char) { return "char"; } @@ -338,8 +338,8 @@ def test13_char_star_over_char(self): assert cppjit.gbl.csoc1.call("0") == "char" raises(ValueError, cppjit.gbl.csoc1.call, "00") - assert cppjit.gbl.csoc2.call("0") == "const char*" - assert cppjit.gbl.csoc2.call("00") == "const char*" + # assert cppjit.gbl.csoc2.call('0') == 'const char*' + # assert cppjit.gbl.csoc2.call('00') == 'const char*' assert cppjit.gbl.csoc3.call("0") == "string" assert cppjit.gbl.csoc3.call("00") == "string" @@ -1460,6 +1460,7 @@ class Foo { with raises(cppjit.gbl.std.logic_error): foo.bar() + @mark.xfail(strict=True, reason="New Overload Resolution: initializer_list/vector") def test47_initializer_list_fail(self, capfd): """Conversion to intializer_list requires default constructor""" @@ -1499,6 +1500,24 @@ def test48_templated_using_with_const(self): assert gbl.NN["std::vector::value_type, 5"]().F is True + def test49_callbacks_with_typedef(self): + """Test callback function with typedefs arg types""" + + import cppjit + from cppjit import gbl + + cppjit.cppdef(r""" + namespace N49 { + typedef signed int i32; + typedef unsigned int u32; + u32 callback(i32 i) { return i > 0 ? i : 0; } + long callme(unsigned int (*fn)(signed int), int k) { return fn(k); } + } + """) + + ns = gbl.N49 + assert ns.callme(ns.callback, 49) == 49 + def test50_using_decl_base_this_offset(self): """A using-declaration-imported method from a non-zero-offset base must adjust the `this` pointer to that base's subobject. diff --git a/bindings/pyroot/cppjit/test/test_stltypes.py b/bindings/pyroot/cppjit/test/test_stltypes.py index b9aec12cce73c..7148bb83d1865 100644 --- a/bindings/pyroot/cppjit/test/test_stltypes.py +++ b/bindings/pyroot/cppjit/test/test_stltypes.py @@ -1,6 +1,7 @@ # -*- coding: UTF-8 -*- import sys +import cppjit import py from pytest import mark, raises, skip from support import ( @@ -198,8 +199,8 @@ def __getitem__(self, i): assert type2test(IterFuncStop(s)) == type2test() # as above, no strings # assert type2test(c for c in "123") == type2test("123") - raises(TypeError, type2test, IterNextOnly(s)) - raises(TypeError, type2test, IterNoNext(s)) + raises(cppjit.OverloadResolutionException, type2test, IterNextOnly(s)) + raises(cppjit.OverloadResolutionException, type2test, IterNoNext(s)) raises(ZeroDivisionError, type2test, IterGenExc(s)) @@ -404,7 +405,9 @@ def test05_push_back_iterables_with_iadd(self): assert v[4] == 5 assert v[5] == 6 - raises(TypeError, v.__iadd__, (7, "8")) # string shouldn't pass + raises( + cppjit.OverloadResolutionException, v.__iadd__, (7, "8") + ) # string shouldn't pass assert len(v) == 7 # TODO: decide whether this should roll-back v2 = cppjit.gbl.std.vector(int)() @@ -703,7 +706,8 @@ def test17_vector_cpp17_style(self): v = cppjit.gbl.std.vector(l) assert list(l) == l - @mark.xfail(condition=IS_MAC and IS_CLING, run=False, reason="Crashes on OSX-Cling") + # @mark.xfail(condition=IS_MAC and IS_CLING, run=False, reason="Crashes on OSX-Cling") + @mark.xfail(strict=True, reason="New Overload Resolution: numpy/vector") def test18_array_interface(self): """Test usage of __array__ from numpy""" @@ -867,6 +871,7 @@ class vCont { assert len([x for x in verts if isinstance(x, ns.Mvertex)]) == 1 + @mark.xfail(strict=True, reason="New Overload Resolution: numpy/vector") def test23_copy_conversion(self): """Vector given an array of different type should copy convert""" @@ -1505,7 +1510,7 @@ def test06_initialize_from_dict(self): assert m["1"] == 1 assert m["2"] == 2 - with raises(TypeError): + with raises(cppjit.OverloadResolutionException): m = mtype[int, str]({"1": 1, "2": 2}) @mark.xfail(condition=IS_MAC, reason="Fails on OS X") diff --git a/bindings/pyroot/cppjit/test/test_templates.py b/bindings/pyroot/cppjit/test/test_templates.py index 584c60c0e0c97..e362d7f968986 100644 --- a/bindings/pyroot/cppjit/test/test_templates.py +++ b/bindings/pyroot/cppjit/test/test_templates.py @@ -443,7 +443,9 @@ def get_tn(ns): assert get_tn(ns) == "some_variadic::A" # static functions - a.sa(1, 1.0, "a") + type(a).sa( + 1, 1.0, "a" + ) # FIXME: call to static function through the instance should be supported assert ( get_tn(ns).find("some_variadic::A::void(int&&,double&&,std::") == 0 @@ -480,7 +482,9 @@ def get_tn(ns): assert get_tn(ns) == "some_variadic::B" # static functions - b.sb(1, 1.0, "a") + type(b).sb( + 1, 1.0, "a" + ) # FIXME: call to static function through the instance should be supported assert get_tn(ns).find("some_variadic::B::void(int&&,double&&,std::") == 0 ns.B.sb(1, 1.0, "a") assert get_tn(ns).find("some_variadic::B::void(int&&,double&&,std::") == 0 @@ -537,7 +541,11 @@ def test19_templated_operator_add(self): namespace OperatorAddTest { template class CustomVec { + template + friend class CustomVec; + V fX; + public: CustomVec() : fX(0) {} CustomVec(const V & a) : fX(a) { } @@ -920,7 +928,7 @@ def ann_ref_mod(node: "FPTA::Node&") -> ns.EventId: # noqa: F722 node.fData = 81 return ev_id - s = ns.Simulator() + s = ns.Simulator # based on reflected __cpp_name__ assert s.Schedule1(ns.Time(1.0), ns.cpp_adapt, ns.Node(42)).fId == 42 From db4e494162ff01a9d8795ab029af62e9a394b8d2 Mon Sep 17 00:00:00 2001 From: Aaron Jomy Date: Wed, 2 Sep 2026 15:48:37 +0200 Subject: [PATCH 09/71] [cppjit] Fix crashes and overload regressions on new overload resolution (cherry picked from commit 0b3dc26) --- .../pyroot/cppjit/src/cpyrt/CPPOverload.cxx | 36 +++++++++++++++++-- .../pyroot/cppjit/src/cpyrt/Converters.cxx | 24 +++++++++---- .../pyroot/cppjit/src/cpyrt/Dispatcher.cxx | 1 - .../pyroot/cppjit/src/cpyrt/TemplateProxy.cxx | 5 ++- 4 files changed, 55 insertions(+), 11 deletions(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx index a80d0277a5dc7..4f77d5dcdbfe1 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx @@ -792,9 +792,13 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, // and might need the self* for the overload resolution // to pick up the custom generated constructor defined in // __cppjit_internal namespace - // FIXME: for the self parameter changes in CppInterOp std::string self_type_name = ""; - if (PyVectorcall_NARGS(nargsf) > 0) { + // only prepend self and retry when there are real value args to forward to + // a generated/forwarding ctor. with no value args this is default + // construction: prepending self would then spuriously match the implicit + // copy ctor (self binds const T&) instead of failing, which is what the + // base-second dispatcher hit. + if (PyVectorcall_NARGS(nargsf) > 0 && !proto.empty()) { PyObject* obj = cpyrt_PyArgs_GET_ITEM(args, 0); PyObject* typ = (PyObject*)Py_TYPE(obj); AddTypeName(self_type_name, typ, nullptr, Utility::kNone); @@ -804,6 +808,34 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, /*TODO:*/ nullptr, is_operator); } } + // Subscript tie-break: cppjit maps both operator[] and operator() into the + // __getitem__/__setitem__ overload set (operator() is needed for 2D access, + // m[i,j] -> operator()(i,j)). For single-index access both can have identical + // signatures, which Clang then reports as ambiguous -- yet in C++ `x[i]` + // binds to operator[] alone and never considers operator(). Mirror that: if + // the ambiguity for a subscript method contains exactly one operator[], + // prefer it. + if (!meth && !ambiguous_candidates.empty() && + (pymeth->fMethodInfo->fName == "__getitem__" || + pymeth->fMethodInfo->fName == "__setitem__")) { + interop::TCppMethod_t subscript = nullptr; + bool unique = true; + for (auto cand : ambiguous_candidates) { + const std::string nm = interop::GetScopedFinalName(cand.data); + if (nm.size() >= 10 && + nm.compare(nm.size() - 10, 10, "operator[]") == 0) { + if (subscript) { + unique = false; + break; + } + subscript = cand; + } + } + if (subscript && unique) { + meth = subscript; + ambiguous_candidates.clear(); + } + } if (meth) { for (auto i : pymeth->fMethodInfo->fMethods) { if (i->GetMethod() == meth) { diff --git a/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx b/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx index b03108ef9e508..d19f07115815c 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx @@ -3013,9 +3013,13 @@ static void* PyFunction_AsCPointer(PyObject* pyobject, return nullptr; // TODO: is there no easier way? - static interop::TCppScope_t scope = - interop::GetScope("__cppjit_internal"); + // FIXME: cache the scope handle again (llvm/llvm-project#201844): on + // released LLVM a static handle to __cppjit_internal goes stale across + // incremental PTUs and the lookup below comes back empty. + interop::TCppScope_t scope = interop::GetScope("__cppjit_internal"); const auto& methods = interop::GetMethodsFromName(scope, wname.str()); + if (methods.empty()) + return nullptr; wpraddress = interop::GetFunctionAddress(methods[0], false); sWrapperReference[wpraddress] = ref; @@ -3093,17 +3097,23 @@ bool cpyrt::FunctionPointerConverter::ToMemory(PyObject* pyobject, //- std::function converter -------------------------------------------------- bool cpyrt::StdFunctionConverter::SetArg(PyObject* pyobject, Parameter& para, CallContext* ctxt) { - // prefer normal "object" conversion - CallContextRAII noimp(ctxt); - if (fConverter->SetArg(pyobject, para, ctxt)) - return true; + // prefer normal "object" conversion; keep kNoImplicit scoped to this attempt + // only, so it does not suppress the implicit copy the wrapper-fallback relies + // on + { + CallContextRAII noimp(ctxt); + if (fConverter->SetArg(pyobject, para, ctxt)) + return true; + } PyErr_Clear(); // else create a wrapper function if (this->FunctionPointerConverter::SetArg(pyobject, para, ctxt)) { // retrieve the wrapper pointer and capture it in a temporary std::function, - // then try normal conversion a second time + // then convert again with implicit conversion allowed (kNoImplicit scoped + // out above): the wrapper is flagged kIsLValue for reuse, so binding it to + // a std::function&& parameter needs an implicit copy, not a move. PyObject* func = this->FunctionPointerConverter::FromMemory(¶.fValue.fVoidp); if (func) { diff --git a/bindings/pyroot/cppjit/src/cpyrt/Dispatcher.cxx b/bindings/pyroot/cppjit/src/cpyrt/Dispatcher.cxx index dcfbd2071a5df..40b1dbdfa74b7 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Dispatcher.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Dispatcher.cxx @@ -524,7 +524,6 @@ bool cpyrt::InsertDispatcher(CPPScope* klass, PyObject* bases, PyObject* dct, code << "};\n}"; // finally, compile the code - // printf("\n\n%s\n\n", code.str().c_str()); if (!interop::Compile(code.str())) { err << "failed to compile the dispatcher code"; return false; diff --git a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx index d59eaa768499d..d368c9d10d152 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx @@ -68,7 +68,10 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, bool skipSelf = fTI->fIsConstructor && (!fSelf || fSelf == Py_None); if (fSelf && (fSelf != Py_None) && (!fTI->fIsConstructor)) { PyObject* self = (PyObject*)fSelf; - assert(AddTypeName(proto, (PyObject*)Py_TYPE(self), self, Utility::kNone)); + bool addedSelf = + AddTypeName(proto, (PyObject*)Py_TYPE(self), self, Utility::kNone); + assert(addedSelf); + (void)addedSelf; } // adjust arguments for self if this is a rebound global function From d45e4a14c10981e9554f3fee586473e9f3912b0a Mon Sep 17 00:00:00 2001 From: Aaron Jomy Date: Wed, 2 Sep 2026 15:48:47 +0200 Subject: [PATCH 10/71] [cppjit] Update tests for new overload resolution behaviour (cherry picked from commit 3f12220) --- bindings/pyroot/cppjit/test/test_boost.py | 6 ++++++ bindings/pyroot/cppjit/test/test_crossinheritance.py | 3 +++ bindings/pyroot/cppjit/test/test_eigen.py | 6 ++++++ bindings/pyroot/cppjit/test/test_fragile.py | 9 ++++++++- bindings/pyroot/cppjit/test/test_numba.py | 10 ++++++---- bindings/pyroot/cppjit/test/test_stltypes.py | 9 +++++++++ bindings/pyroot/cppjit/test/test_templates.py | 3 +++ 7 files changed, 41 insertions(+), 5 deletions(-) diff --git a/bindings/pyroot/cppjit/test/test_boost.py b/bindings/pyroot/cppjit/test/test_boost.py index 157c066ffb448..79a43cae07e7e 100644 --- a/bindings/pyroot/cppjit/test/test_boost.py +++ b/bindings/pyroot/cppjit/test/test_boost.py @@ -58,6 +58,12 @@ def test01_any_class(self): run=False, reason="invalid reads in the JITed boost constructors under valgrind", ) + @mark.xfail( + reason="New Overload Resolution: a forwarding-reference template " + "operator= (any::operator=(ValueType&&)) fails to instantiate for " + "lvalue arguments, and the non-template move-assign fallback rightly " + "rejects an lvalue; pending" + ) def test02_any_usage(self): """boost::any assignment and casting""" diff --git a/bindings/pyroot/cppjit/test/test_crossinheritance.py b/bindings/pyroot/cppjit/test/test_crossinheritance.py index 10e2394c800da..79b889d07e8a7 100644 --- a/bindings/pyroot/cppjit/test/test_crossinheritance.py +++ b/bindings/pyroot/cppjit/test/test_crossinheritance.py @@ -1085,6 +1085,9 @@ def return_const(self): assert a.return_const().m_value == "abcdef" assert ns.callit(a).m_value == "abcdef" + @mark.xfail( + reason="New Overload Resolution: super().__init__(self) should forward self to the templated NoCopyNoMove(DerivedType*) ctor but mis-resolves to the deleted copy ctor Dispatcher(Dispatcher&); pending" + ) def test24_non_copyable(self): """Inheriting from a non-copyable base class""" diff --git a/bindings/pyroot/cppjit/test/test_eigen.py b/bindings/pyroot/cppjit/test/test_eigen.py index 88a077236e1b6..fc896deafd0d5 100644 --- a/bindings/pyroot/cppjit/test/test_eigen.py +++ b/bindings/pyroot/cppjit/test/test_eigen.py @@ -28,6 +28,12 @@ def setup_class(cls): warnings.simplefilter("ignore") cppjit.include("Eigen/Dense") + @mark.xfail( + reason="New Overload Resolution: operators a using-declaration imports " + "(Eigen brings the DenseBase subscript operators into Matrix that way) " + "are enumerated as using-shadows since CppInterOp #995, and " + "Cpp::BestOverloadFunctionMatch skips shadow candidates; pending" + ) def test01_simple_matrix_and_vector(self): """Basic creation of an Eigen::Matrix and Eigen::Vector""" diff --git a/bindings/pyroot/cppjit/test/test_fragile.py b/bindings/pyroot/cppjit/test/test_fragile.py index 1732aae63fedb..2dfe15fd161ba 100644 --- a/bindings/pyroot/cppjit/test/test_fragile.py +++ b/bindings/pyroot/cppjit/test/test_fragile.py @@ -656,7 +656,7 @@ def test25_cppdef_error_reporting(self): int add42(int i) { return i + 42; } }""") - @mark.xfail(condition=IS_CLANG_REPL, reason="Fails on ClangRepl") + @mark.xfail(condition=IS_CLANG_REPL, reason="Fails with ClangRepl") @mark.xfail( run=False, reason="rolling back a failed declaration asserts in ROOT's unload " @@ -778,6 +778,13 @@ def test31_template_with_class_enum(self): for ns, val in [(cppjit.gbl, 42), (cppjit.gbl.ClassEnumNS, 37)]: assert ns.EnumTemplate[ns.ClassEnumA.A]().foo() == val + @mark.xfail( + run=False, + reason="New Overload Resolution: calling through a pointer-valued global " + "proxy deduces the self argument as T*, and Cpp::BestOverloadFunctionMatch " + "feeds that rvalue classification into Sema::AddMethodCandidate, which " + "asserts FromClassification.isLValue() on assert-enabled clang", + ) def test32_overloaded_method_error_with_null_object(self): """Check exception type and message when method invoked on instance without C++ object""" diff --git a/bindings/pyroot/cppjit/test/test_numba.py b/bindings/pyroot/cppjit/test/test_numba.py index 081e4aeb2569c..4fa654ff15ba6 100644 --- a/bindings/pyroot/cppjit/test/test_numba.py +++ b/bindings/pyroot/cppjit/test/test_numba.py @@ -570,8 +570,9 @@ def square_vec_slow(x): a = np.random.randint(1, 100, size=10000, dtype=np.int64) b = np.random.randint(1, 4, size=10, dtype=np.int64) - x = cppjit.gbl.std.vector["long"](a.flatten()) - y = cppjit.gbl.std.vector["long"](b.flatten()) + # std::vector no longer fill-constructs from a raw buffer; go via list + x = cppjit.gbl.std.vector["long"](a.flatten().tolist()) + y = cppjit.gbl.std.vector["long"](b.flatten().tolist()) t0 = time.time() add_vec_fast(ns.BoxVector(x)) @@ -658,8 +659,9 @@ def np_dot_product(x, y): # for i in a: # vec_list.append([vector['long'](i[0]), vector['long'](i[1])]) - x = cppjit.gbl.std.vector["long"](a.flatten()) - y = cppjit.gbl.std.vector["long"](b.flatten()) + # std::vector no longer fill-constructs from a raw buffer; go via list + x = cppjit.gbl.std.vector["long"](a.flatten().tolist()) + y = cppjit.gbl.std.vector["long"](b.flatten().tolist()) d = ns.DotVector(x, y) dot_product_fast(d) res = 0 diff --git a/bindings/pyroot/cppjit/test/test_stltypes.py b/bindings/pyroot/cppjit/test/test_stltypes.py index 7148bb83d1865..0efbed10cb9c9 100644 --- a/bindings/pyroot/cppjit/test/test_stltypes.py +++ b/bindings/pyroot/cppjit/test/test_stltypes.py @@ -1871,6 +1871,12 @@ def test01_string_through_string_view(self): assert countit(v) == 4 assert countit_cr(v) == 4 + @mark.xfail( + reason="New Overload Resolution: the one-argument " + "basic_string_view(const char*) constructor is never considered for " + "a Python str (the two-argument (const char*, size_t) form works); " + "pending" + ) def test02_string_view_from_unicode(self): """Life-time management of converted unicode strings""" @@ -2502,6 +2508,9 @@ def test02_span_argument_conversions(self): class TestSTLANY: @mark.xfail(condition=IS_WINDOWS, reason="Fails on Windows") + @mark.xfail( + reason="New Overload Resolution: std::make_any(MyClass&) is ill-formed C++ (clang: no matching function); test relied on non-standard cppjit conversion" + ) def test01_make_any(self): """ Test that std::make_any can be used for class types. diff --git a/bindings/pyroot/cppjit/test/test_templates.py b/bindings/pyroot/cppjit/test/test_templates.py index e362d7f968986..eedff095f7079 100644 --- a/bindings/pyroot/cppjit/test/test_templates.py +++ b/bindings/pyroot/cppjit/test/test_templates.py @@ -1227,6 +1227,9 @@ class TNaVU; @mark.xfail( condition=IS_MAC and IS_CLING, run=False, reason="Crashes on OS X + Cling" ) + @mark.xfail( + reason="New Overload Resolution: passing a null-bound instance where a pointer is expected (bind_object(nullptr, T) -> T*, a cppjit convenience not expressible in regular C++ — the plain nullptr form works) fails in InstanceConverter — pending" + ) def test33_using_template_argument(self): """`using` type as template argument""" From 4fadd289b0bbcd7270bd1a1425a2568ca8e122dc Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Tue, 13 Oct 2026 10:41:03 +0530 Subject: [PATCH 11/71] [cppjit] minor refactors (cherry picked from commit a57f54a) --- .../pyroot/cppjit/src/cpyrt/CPPOverload.cxx | 7 +--- .../pyroot/cppjit/src/cpyrt/Converters.cxx | 9 ++--- bindings/pyroot/cppjit/src/cpyrt/Utility.cxx | 8 ---- .../cppjit/src/interop/cppjit_interop.h | 2 + .../cppjit/src/interop/interop_wrapper.cxx | 38 ++++++++++--------- bindings/pyroot/cppjit/test/test_fragile.py | 2 +- 6 files changed, 29 insertions(+), 37 deletions(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx index 4f77d5dcdbfe1..3a4ae0c6def95 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx @@ -794,10 +794,7 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, // __cppjit_internal namespace std::string self_type_name = ""; // only prepend self and retry when there are real value args to forward to - // a generated/forwarding ctor. with no value args this is default - // construction: prepending self would then spuriously match the implicit - // copy ctor (self binds const T&) instead of failing, which is what the - // base-second dispatcher hit. + // a generated/forwarding ctor. if (PyVectorcall_NARGS(nargsf) > 0 && !proto.empty()) { PyObject* obj = cpyrt_PyArgs_GET_ITEM(args, 0); PyObject* typ = (PyObject*)Py_TYPE(obj); @@ -838,7 +835,7 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, } if (meth) { for (auto i : pymeth->fMethodInfo->fMethods) { - if (i->GetMethod() == meth) { + if (interop::GetUnderlyingScope(i->GetMethod().data).data == meth) { PyObject* result = i->Call(im_self, args, nargsf, kwds, &ctxt); if (result) { // success: update the dispatch map for subsequent calls diff --git a/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx b/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx index d19f07115815c..4a2265eaf9a1e 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx @@ -3013,13 +3013,10 @@ static void* PyFunction_AsCPointer(PyObject* pyobject, return nullptr; // TODO: is there no easier way? - // FIXME: cache the scope handle again (llvm/llvm-project#201844): on - // released LLVM a static handle to __cppjit_internal goes stale across - // incremental PTUs and the lookup below comes back empty. - interop::TCppScope_t scope = interop::GetScope("__cppjit_internal"); + static interop::TCppScope_t scope = + interop::GetScope("__cppjit_internal"); const auto& methods = interop::GetMethodsFromName(scope, wname.str()); - if (methods.empty()) - return nullptr; + assert(methods.size() == 1); wpraddress = interop::GetFunctionAddress(methods[0], false); sWrapperReference[wpraddress] = ref; diff --git a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx index 208610c60bcee..1a8312fa4d129 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx @@ -31,7 +31,6 @@ using namespace cppjit; //- data _____________________________________________________________________ typedef std::unordered_map TC2POperatorMapping_t; static TC2POperatorMapping_t gC2POperatorMapping; -static std::set gOpSkip; static std::set gOpRemove; extern PyTypeObject PyNullPtr_t_Type; @@ -51,13 +50,6 @@ struct InitOperatorMapping_t { InitOperatorMapping_t() { // Initialize the global map of operator names C++ -> python. - gOpSkip.insert("[]"); // __s/getitem__, depends on return type - gOpSkip.insert("+"); // __add__, depends on # of args (see __pos__) - gOpSkip.insert("-"); // __sub__, id. (eq. __neg__) - gOpSkip.insert("*"); // __mul__ or __deref__ - gOpSkip.insert("++"); // __postinc__ or __preinc__ - gOpSkip.insert("--"); // __postdec__ or __predec__ - gOpRemove.insert("new"); // this and the following not handled at all gOpRemove.insert("new[]"); gOpRemove.insert("delete"); diff --git a/bindings/pyroot/cppjit/src/interop/cppjit_interop.h b/bindings/pyroot/cppjit/src/interop/cppjit_interop.h index 90ee79dd57e43..9fc5e75e32586 100644 --- a/bindings/pyroot/cppjit/src/interop/cppjit_interop.h +++ b/bindings/pyroot/cppjit/src/interop/cppjit_interop.h @@ -67,6 +67,8 @@ TCppType_t ResolveEnumReferenceType(TCppType_t type); RPY_EXPORTED TCppType_t ResolveEnumPointerType(TCppType_t type); RPY_EXPORTED +TCppType_t GetNonReferenceType(TCppType_t type); +RPY_EXPORTED TCppType_t GetRealType(TCppType_t type); RPY_EXPORTED TCppType_t GetPointerType(TCppType_t type); diff --git a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx index 9aa2ad245c52a..60a901baabd3f 100644 --- a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx +++ b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx @@ -361,6 +361,10 @@ interop::TCppType_t interop::ResolveEnumPointerType(TCppType_t type) { return type; } +interop::TCppType_t interop::GetNonReferenceType(interop::TCppType_t type) { + return (type && Cpp::IsReferenceType(type)) ? Cpp::GetNonReferenceType(type) : type; +} + interop::TCppType_t int_like_type(interop::TCppType_t type) { interop::TCppType_t check_int_typedefs = type; if (Cpp::IsPointerType(check_int_typedefs)) @@ -410,7 +414,7 @@ interop::TCppType_t interop::GetPointerType(TCppType_t type) { std::lock_guard Lock(InterOpMutex); Cpp::ValueKind ref = Cpp::GetValueKind(type); TCppType_t pointer_type = type; - pointer_type = Cpp::GetPointerType(Cpp::GetNonReferenceType(type)); + pointer_type = Cpp::GetPointerType(interop::GetNonReferenceType(type)); if (ref == Cpp::ValueKind::LValue) return Cpp::GetReferencedType(pointer_type, false); if (ref == Cpp::ValueKind::RValue) @@ -1856,8 +1860,7 @@ interop::TCppMethod_t BestOverloadFunctionMatch( Cpp::ValueKind ref = Cpp::ValueKind::None; bool pointer = false; static interop::TCppScope_t initializer_list_scope = - interop::GetScopeFromType(interop::GetTypeFromScope( - interop::GetScope("std::initializer_list"))); + Cpp::GetTemplatedDecl(interop::GetScope("std::initializer_list")); static interop::TCppScope_t vector_scope = interop::GetScope("std::vector"); static interop::TCppType_t string_type = interop::GetRealType( @@ -1879,13 +1882,13 @@ interop::TCppMethod_t BestOverloadFunctionMatch( old_type = arg.m_Type; arg.m_Type = interop::GetPointerType(arg.m_Type).data; if (ref != Cpp::ValueKind::None) - arg.m_Type = Cpp::GetNonReferenceType(arg.m_Type).data; + arg.m_Type = interop::GetNonReferenceType(arg.m_Type).data; } else if (Cpp::IsEquivalentTypes(string_type, arg.m_Type, qual, ref, pointer) && (Cpp::IsEquivalentTypes(char_pointer_type, fn_arg_type, qual, ref, pointer)) && - (Cpp::IsPointerType(Cpp::GetNonReferenceType( - interop::ResolveType(fn_arg_type))))) { + (Cpp::IsPointerType( + interop::GetNonReferenceType(interop::ResolveType(fn_arg_type))))) { // promote std::string to char* old_type = arg.m_Type; arg.m_Type = char_pointer_type.data; @@ -1901,8 +1904,8 @@ interop::TCppMethod_t BestOverloadFunctionMatch( pointer) && (Cpp::IsEquivalentTypes(char_pointer_type, arg.m_Type, qual, ref, pointer)) && - (Cpp::IsPointerType(Cpp::GetNonReferenceType( - interop::ResolveType(arg.m_Type))))) { + (Cpp::IsPointerType( + interop::GetNonReferenceType(interop::ResolveType(arg.m_Type))))) { // promote char* to std::string old_type = arg.m_Type; arg.m_Type = string_type.data; @@ -1910,8 +1913,8 @@ interop::TCppMethod_t BestOverloadFunctionMatch( pointer) && (Cpp::IsEquivalentTypes(wchar_pointer_type, fn_arg_type, qual, ref, pointer)) && - (Cpp::IsPointerType(Cpp::GetNonReferenceType( - interop::ResolveType(fn_arg_type))))) { + (Cpp::IsPointerType( + interop::GetNonReferenceType(interop::ResolveType(fn_arg_type))))) { // promote std::string to wchar_t* old_type = arg.m_Type; arg.m_Type = wchar_pointer_type.data; @@ -1919,8 +1922,8 @@ interop::TCppMethod_t BestOverloadFunctionMatch( pointer) && (Cpp::IsEquivalentTypes(wchar_pointer_type, arg.m_Type, qual, ref, pointer)) && - (Cpp::IsPointerType(Cpp::GetNonReferenceType( - interop::ResolveType(arg.m_Type))))) { + (Cpp::IsPointerType( + interop::GetNonReferenceType(interop::ResolveType(arg.m_Type))))) { // promote wchar_t* to std::wstring old_type = arg.m_Type; arg.m_Type = wstring_type.data; @@ -1958,13 +1961,12 @@ interop::TCppMethod_t BestOverloadFunctionMatch( if (ref != Cpp::ValueKind::None) { if (Cpp::GetValueKind(arg.m_Type) == ref) { // remove reference - arg.m_Type = Cpp::GetNonReferenceType(arg.m_Type).data; + arg.m_Type = interop::GetNonReferenceType(arg.m_Type).data; } else { // create a reference - arg.m_Type = - Cpp::GetReferencedType(Cpp::GetNonReferenceType(arg.m_Type), - ref == Cpp::ValueKind::RValue) - .data; + arg.m_Type = Cpp::GetReferencedType(interop::GetNonReferenceType(arg.m_Type), + ref == Cpp::ValueKind::RValue) + .data; } } if (pointer) { @@ -2186,6 +2188,8 @@ interop::TCppMethod_t interop::GetGlobalOperator( // method properties --------------------------------------------------------- bool interop::IsDeletedMethod(TCppMethod_t method) { + if (Cpp::IsTemplatedFunction(method)) + method = Cpp::GetTemplatedDecl(method.data).data; return Cpp::IsFunctionDeleted(method); } diff --git a/bindings/pyroot/cppjit/test/test_fragile.py b/bindings/pyroot/cppjit/test/test_fragile.py index 2dfe15fd161ba..e82c9bd654e01 100644 --- a/bindings/pyroot/cppjit/test/test_fragile.py +++ b/bindings/pyroot/cppjit/test/test_fragile.py @@ -656,7 +656,7 @@ def test25_cppdef_error_reporting(self): int add42(int i) { return i + 42; } }""") - @mark.xfail(condition=IS_CLANG_REPL, reason="Fails with ClangRepl") + @mark.xfail(condition=IS_CLANG_REPL, reason="Fails with ClangRepl (passes with LLVM 23)") @mark.xfail( run=False, reason="rolling back a failed declaration asserts in ROOT's unload " From b69c600d3226842f7023f2fcddb29596ea99c461 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Tue, 13 Oct 2026 16:26:00 +0530 Subject: [PATCH 12/71] [cppjit] Implement `IsEquivalentTypes` to check if two types are equivalent `IsEquivalentTypes` checks whether two types are equivalent up to CVR qualifiers, references and pointers, or whether one type derives from the other. Through its out-parameters it reports how the types differ: - `qual`: the CVR qualifiers that differ, - `ref`: the kind of reference (lvalue/rvalue) that was stripped, - `pointer`: whether a pointer was stripped. (cherry picked from commit f48cc29) --- .../cppjit/src/interop/interop_wrapper.cxx | 146 +++++++++++++----- 1 file changed, 107 insertions(+), 39 deletions(-) diff --git a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx index 60a901baabd3f..e1c8e777e6095 100644 --- a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx +++ b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx @@ -1793,16 +1793,84 @@ bool interop::IsNonStaticMethod(interop::TCppMethod_t func) { return Cpp::IsMethod(func) && !Cpp::IsStaticMethod(func); } +static bool IsEquivalentTypesImpl(interop::TCppType_t typ1, + interop::TCppType_t typ2, Cpp::QualKind& qual, + Cpp::ValueKind& ref, bool& pointer) { + // check if same type + if (Cpp::IsSameType(typ1, typ2) || Cpp::IsTypeDerivedFrom(typ1, typ2) || + Cpp::IsTypeDerivedFrom(typ2, typ1)) + return true; + + // check if same type after removing qualifiers; canonicalize first so that + // qualifiers hidden behind a typedef are removed too + if (Cpp::IsSameType(Cpp::RemoveTypeQualifier(Cpp::GetCanonicalType(typ1), + Cpp::QualKind::All), + Cpp::RemoveTypeQualifier(Cpp::GetCanonicalType(typ2), + Cpp::QualKind::All))) { + for (Cpp::QualKind q : {Cpp::QualKind::Const, Cpp::QualKind::Volatile, + Cpp::QualKind::Restrict}) + if (Cpp::HasTypeQualifier(typ1, q) != Cpp::HasTypeQualifier(typ2, q)) + qual = qual | q; + return true; + } + + // check if same type after removing references + if (Cpp::IsReferenceType(typ1) && + IsEquivalentTypesImpl(Cpp::GetNonReferenceType(typ1), typ2, qual, ref, + pointer)) { + ref = Cpp::GetValueKind(typ1); + return true; + } + if (Cpp::IsReferenceType(typ2) && + IsEquivalentTypesImpl(Cpp::GetNonReferenceType(typ2), typ1, qual, ref, + pointer)) { + ref = Cpp::GetValueKind(typ2); + return true; + } + + // check if same type after removing pointers + if (Cpp::IsPointerType(typ1) && + IsEquivalentTypesImpl(Cpp::GetPointeeType(typ1), typ2, qual, ref, + pointer)) { + pointer = true; + return true; + } + if (Cpp::IsPointerType(typ2) && + IsEquivalentTypesImpl(Cpp::GetPointeeType(typ2), typ1, qual, ref, + pointer)) { + pointer = true; + return true; + } + return false; +} + +// Checks if two types are equivalent up to CVR qualifiers, references and +// pointers, or if one type derives from the other. On return, qual holds the +// CVR qualifiers that differ between the two types, ref the kind of reference +// that was stripped and pointer whether a pointer was stripped. +static bool IsEquivalentTypes(interop::TCppType_t typ1, + interop::TCppType_t typ2, Cpp::QualKind& qual, + Cpp::ValueKind& ref, bool& pointer) { + qual = static_cast(0); + ref = Cpp::ValueKind::None; + pointer = false; + if (!typ1 || !typ2) + return false; + return IsEquivalentTypesImpl(typ1, typ2, qual, ref, pointer); +} + bool IsTemplateParamEquivalentTypes(interop::TCppType_t typ1, interop::TCppType_t typ2, Cpp::QualKind& qual, Cpp::ValueKind& ref, bool& pointer) { - qual = Cpp::QualKind::None; + qual = static_cast(0); ref = Cpp::ValueKind::None; pointer = false; - ref = ref ^ Cpp::GetValueKind(typ1); - ref = ref ^ Cpp::GetValueKind(typ2); + Cpp::ValueKind ref1 = Cpp::GetValueKind(typ1); + Cpp::ValueKind ref2 = Cpp::GetValueKind(typ2); + if (ref1 != ref2) + ref = ref1 != Cpp::ValueKind::None ? ref1 : ref2; typ2 = Cpp::IsReferenceType(typ2) ? Cpp::GetNonReferenceType(typ2) : typ2; pointer ^= Cpp::IsPointerType(typ1); @@ -1856,13 +1924,13 @@ interop::TCppMethod_t BestOverloadFunctionMatch( I++; continue; } - Cpp::QualKind qual = Cpp::QualKind::None; + Cpp::QualKind qual = static_cast(0); Cpp::ValueKind ref = Cpp::ValueKind::None; bool pointer = false; static interop::TCppScope_t initializer_list_scope = Cpp::GetTemplatedDecl(interop::GetScope("std::initializer_list")); - static interop::TCppScope_t vector_scope = - interop::GetScope("std::vector"); + // static interop::TCppScope_t vector_scope = + // interop::GetScope("std::vector"); static interop::TCppType_t string_type = interop::GetRealType( interop::GetTypeFromScope(interop::GetScope("std::string"))); static interop::TCppType_t wstring_type = interop::GetRealType( @@ -1876,65 +1944,65 @@ interop::TCppMethod_t BestOverloadFunctionMatch( Cpp::GetFunctionArgType(fn, skipThis ? I - 1 : I); assert(string_type); interop::TCppType_t old_type = nullptr; - if (Cpp::IsEquivalentTypes(arg.m_Type, fn_arg_type, qual, ref, pointer) && + if (IsEquivalentTypes(arg.m_Type, fn_arg_type, qual, ref, pointer) && pointer) { // Only promotion to pointer should be allowed. old_type = arg.m_Type; arg.m_Type = interop::GetPointerType(arg.m_Type).data; if (ref != Cpp::ValueKind::None) arg.m_Type = interop::GetNonReferenceType(arg.m_Type).data; - } else if (Cpp::IsEquivalentTypes(string_type, arg.m_Type, qual, ref, - pointer) && - (Cpp::IsEquivalentTypes(char_pointer_type, fn_arg_type, qual, - ref, pointer)) && - (Cpp::IsPointerType( - interop::GetNonReferenceType(interop::ResolveType(fn_arg_type))))) { + } else if (IsEquivalentTypes(string_type, arg.m_Type, qual, ref, + pointer) && + (IsEquivalentTypes(char_pointer_type, fn_arg_type, qual, ref, + pointer)) && + (Cpp::IsPointerType(interop::GetNonReferenceType( + interop::ResolveType(fn_arg_type))))) { // promote std::string to char* old_type = arg.m_Type; arg.m_Type = char_pointer_type.data; - } else if (Cpp::IsEquivalentTypes(string_type, arg.m_Type, qual, ref, - pointer) && - (Cpp::IsEquivalentTypes(char_type, fn_arg_type, qual, ref, - pointer))) { + } else if (IsEquivalentTypes(string_type, arg.m_Type, qual, ref, + pointer) && + (IsEquivalentTypes(char_type, fn_arg_type, qual, ref, + pointer))) { // promote std::string to char // is this fine? old_type = arg.m_Type; arg.m_Type = char_type.data; - } else if (Cpp::IsEquivalentTypes(string_type, fn_arg_type, qual, ref, - pointer) && - (Cpp::IsEquivalentTypes(char_pointer_type, arg.m_Type, qual, - ref, pointer)) && - (Cpp::IsPointerType( - interop::GetNonReferenceType(interop::ResolveType(arg.m_Type))))) { + } else if (IsEquivalentTypes(string_type, fn_arg_type, qual, ref, + pointer) && + (IsEquivalentTypes(char_pointer_type, arg.m_Type, qual, ref, + pointer)) && + (Cpp::IsPointerType(interop::GetNonReferenceType( + interop::ResolveType(arg.m_Type))))) { // promote char* to std::string old_type = arg.m_Type; arg.m_Type = string_type.data; - } else if (Cpp::IsEquivalentTypes(string_type, arg.m_Type, qual, ref, - pointer) && - (Cpp::IsEquivalentTypes(wchar_pointer_type, fn_arg_type, qual, - ref, pointer)) && - (Cpp::IsPointerType( - interop::GetNonReferenceType(interop::ResolveType(fn_arg_type))))) { + } else if (IsEquivalentTypes(string_type, arg.m_Type, qual, ref, + pointer) && + (IsEquivalentTypes(wchar_pointer_type, fn_arg_type, qual, ref, + pointer)) && + (Cpp::IsPointerType(interop::GetNonReferenceType( + interop::ResolveType(fn_arg_type))))) { // promote std::string to wchar_t* old_type = arg.m_Type; arg.m_Type = wchar_pointer_type.data; - } else if (Cpp::IsEquivalentTypes(wstring_type, fn_arg_type, qual, ref, - pointer) && - (Cpp::IsEquivalentTypes(wchar_pointer_type, arg.m_Type, qual, - ref, pointer)) && - (Cpp::IsPointerType( - interop::GetNonReferenceType(interop::ResolveType(arg.m_Type))))) { + } else if (IsEquivalentTypes(wstring_type, fn_arg_type, qual, ref, + pointer) && + (IsEquivalentTypes(wchar_pointer_type, arg.m_Type, qual, ref, + pointer)) && + (Cpp::IsPointerType(interop::GetNonReferenceType( + interop::ResolveType(arg.m_Type))))) { // promote wchar_t* to std::wstring old_type = arg.m_Type; arg.m_Type = wstring_type.data; - // } else if (Cpp::IsEquivalentTypes(string_type, arg.m_Type, qual, ref, - // pointer) && (Cpp::IsEquivalentTypes(wstring_type, fn_arg_type, qual, + // } else if (IsEquivalentTypes(string_type, arg.m_Type, qual, ref, + // pointer) && (IsEquivalentTypes(wstring_type, fn_arg_type, qual, // ref, pointer))) { // // promote std::string to std::wstring // old_type = arg.m_Type; // arg.m_Type = wstring_type.data; - // } else if (Cpp::IsEquivalentTypes(char_pointer_type, arg.m_Type, - // qual, ref, pointer) && (Cpp::IsEquivalentTypes(wstring_type, + // } else if (IsEquivalentTypes(char_pointer_type, arg.m_Type, + // qual, ref, pointer) && (IsEquivalentTypes(wstring_type, // fn_arg_type, qual, ref, pointer)) && // (Cpp::IsPointerType(Cpp::GetNonReferenceType(interop::ResolveType(arg.m_Type)))) // && From 6c0cdfa9e5d5bec4a3326091820b564ebbbdf902 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Tue, 13 Oct 2026 17:11:26 +0530 Subject: [PATCH 13/71] [cppjit] `BestOverloadFunctionMatch` loop optimization (cherry picked from commit e24c182) --- .../cppjit/src/interop/interop_wrapper.cxx | 36 ++++++++++--------- bindings/pyroot/cppjit/test/test_overloads.py | 22 ++++++++++++ 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx index e1c8e777e6095..d0c195e754eda 100644 --- a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx +++ b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx @@ -1885,7 +1885,7 @@ bool IsTemplateParamEquivalentTypes(interop::TCppType_t typ1, namespace { interop::TCppMethod_t BestOverloadFunctionMatch( const std::vector& candidates, - std::vector& arg_types, + const std::vector& arg_types, const std::vector& templ_params, std::vector& ambiguous_candidates, interop::TCppScope_t parent_scope, bool is_operator) { @@ -1917,8 +1917,13 @@ interop::TCppMethod_t BestOverloadFunctionMatch( else potential_cppmeth_ambiguous = true; + // promote the arguments towards the parameters of fn, then resolve the + // overload once with all of them promoted + // ???: Can this cause ambiguity + std::vector promoted_arg_types = arg_types; + bool promoted = false; size_t I = 0; - for (auto& arg : arg_types) { + for (auto& arg : promoted_arg_types) { if (I == 0 && skipThis) { // ignore the this parameter I++; @@ -2052,23 +2057,20 @@ interop::TCppMethod_t BestOverloadFunctionMatch( old_type = nullptr; } } - if (old_type) { - if (!cppmeth) { - // FIXME: this call should happen outside the inner for loop? - // FIXME: How to handle ambiguous_candidates in this case? - cppmeth = Cpp::BestOverloadFunctionMatch( - candidates, {}, arg_types, ambiguous_candidates, is_operator); - } else if (interop::TCppMethod_t new_meth = - Cpp::BestOverloadFunctionMatch(candidates, {}, arg_types, - ambiguous_candidates, - is_operator); - new_meth && new_meth != cppmeth) { - return nullptr; // ambiguous, raise diagnostic - } - arg.m_Type = old_type.data; - } + if (old_type) + promoted = true; I++; } + if (!promoted) + continue; + + // FIXME: How to handle ambiguous_candidates in this case? + interop::TCppMethod_t new_meth = Cpp::BestOverloadFunctionMatch( + candidates, {}, promoted_arg_types, ambiguous_candidates, is_operator); + if (!cppmeth) + cppmeth = new_meth; + else if (new_meth && new_meth != cppmeth) + return nullptr; // ambiguous, raise diagnostic } // proper argument type match failed diff --git a/bindings/pyroot/cppjit/test/test_overloads.py b/bindings/pyroot/cppjit/test/test_overloads.py index 5125853c29379..110e94bd6993b 100644 --- a/bindings/pyroot/cppjit/test/test_overloads.py +++ b/bindings/pyroot/cppjit/test/test_overloads.py @@ -477,3 +477,25 @@ def constness(name): assert constness("static_fn") == { "static int OverloadConstness::Probe::static_fn(int x)": False } + + def test18_promote_multiple_arguments(self): + """Verify that an overload needing several argument promotions resolves.""" + + import cppjit + + cppjit.cppdef(""" + namespace MultiPromotion { + std::string join(char* a, char* b) { return std::string(a) + b; } + std::string join(int a, int b) { return std::to_string(a + b); } + std::string join_mixed(char* a, int b) { return a + std::to_string(b); } + std::string join_mixed(int a, char* b) { return std::to_string(a) + b; } + }""") + + ns = cppjit.gbl.MultiPromotion + + # both str arguments have to be promoted to char* at the same time + assert ns.join("ab", "cd") == "abcd" + assert ns.join(1, 2) == "3" + # a single promotion + assert ns.join_mixed("ab", 1) == "ab1" + assert ns.join_mixed(1, "ab") == "1ab" From edfff1c9fed69694cff0c759c3601093a169c08a Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Tue, 13 Oct 2026 17:33:45 +0530 Subject: [PATCH 14/71] [cppjit] fix cases where `ambiguous_candidates` have duplicate entries (cherry picked from commit 6f4b666) --- .../cppjit/src/interop/interop_wrapper.cxx | 21 ++++++++++++++++--- bindings/pyroot/cppjit/test/test_overloads.py | 20 ++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx index d0c195e754eda..6d35a7718758c 100644 --- a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx +++ b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx @@ -1891,6 +1891,15 @@ interop::TCppMethod_t BestOverloadFunctionMatch( interop::TCppScope_t parent_scope, bool is_operator) { std::lock_guard Lock(InterOpMutex); + auto remove_duplicate_methods = [](std::vector& methods) { + std::set seen; + methods.erase(std::remove_if(methods.begin(), methods.end(), + [&seen](interop::TCppMethod_t m) { + return !seen.insert(m.data).second; + }), + methods.end()); + }; + interop::TCppMethod_t cppmeth = Cpp::BestOverloadFunctionMatch( candidates, templ_params, arg_types, ambiguous_candidates, is_operator); if (cppmeth) @@ -2064,13 +2073,17 @@ interop::TCppMethod_t BestOverloadFunctionMatch( if (!promoted) continue; - // FIXME: How to handle ambiguous_candidates in this case? interop::TCppMethod_t new_meth = Cpp::BestOverloadFunctionMatch( candidates, {}, promoted_arg_types, ambiguous_candidates, is_operator); if (!cppmeth) cppmeth = new_meth; - else if (new_meth && new_meth != cppmeth) - return nullptr; // ambiguous, raise diagnostic + else if (new_meth && new_meth != cppmeth) { + // ambiguous + ambiguous_candidates.push_back(cppmeth); + ambiguous_candidates.push_back(new_meth); + remove_duplicate_methods(ambiguous_candidates); + return nullptr; + } } // proper argument type match failed @@ -2078,6 +2091,8 @@ interop::TCppMethod_t BestOverloadFunctionMatch( if (!cppmeth && !potential_cppmeth_ambiguous && !Cpp::IsTemplatedFunction(potential_cppmeth)) cppmeth = potential_cppmeth; + if (!cppmeth) + remove_duplicate_methods(ambiguous_candidates); return cppmeth; } } // namespace diff --git a/bindings/pyroot/cppjit/test/test_overloads.py b/bindings/pyroot/cppjit/test/test_overloads.py index 110e94bd6993b..b3d98e4b2c68a 100644 --- a/bindings/pyroot/cppjit/test/test_overloads.py +++ b/bindings/pyroot/cppjit/test/test_overloads.py @@ -499,3 +499,23 @@ def test18_promote_multiple_arguments(self): # a single promotion assert ns.join_mixed("ab", 1) == "ab1" assert ns.join_mixed(1, "ab") == "1ab" + + def test19_ambiguous_set_has_no_duplicates(self): + """Verify that an ambiguous call reports each candidate only once.""" + + import cppjit + + cppjit.cppdef(""" + namespace DupAmbiguity { + int f(char* a, double b) { return 1; } + int f(char* a, float b) { return 2; } + }""") + + # the str argument needs a promotion to char*, which is retried for + # each candidate, and every retry is ambiguous on the int argument + with raises(cppjit.OverloadAmbiguityException) as exc: + cppjit.gbl.DupAmbiguity.f("x", 1) + + msg = str(exc.value) + assert msg.count("double b)") == 1 + assert msg.count("float b)") == 1 From c53429c9a8f68a2585066800b7822fa378779637 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Tue, 13 Oct 2026 18:12:24 +0530 Subject: [PATCH 15/71] [cppjit] Remove dead code * Remove SelectAndForward from TemplateProxy SelectAndForward forwarded a template proxy call to one of its overload sets: tpp_vectorcall tried the non-templated overloads (implicit conversions allowed), then the known templated ones and finally the low-priority ones (both without implicit conversions). "Adopt Clang-driven overload resolution from CppInterOp" replaced that cascade with a single Clang-driven selection and its three callers went away with it; the static function has had no caller since (clang reports it with -Wunused-function). * Remove CPPDataMember_NewConstant CPPDataMember_NewConstant built a static, constant data member descriptor from a raw name and address, via the matching CPPDataMember::Set(scope, name, address) overload. Nothing calls it (it was already unused at 3df629f): every data member descriptor is created by CPPDataMember_New from a reflected variable, including enum constants. Remove the factory and the Set overload only it used. * Remove Utility::PyErr_Occurred_WithGIL It re-acquired the GIL around PyErr_Occurred() for callers that might have released it. Nothing calls it (it was already unused at 3df629f); the executors that release the GIL (GILControl in Executors.cxx) restore it before checking for errors. * remove unused call context enums based on new overload resolution * Remove the write-only kHaveImplicit call flag Converters set CallContext::kHaveImplicit when they rejected an argument that an implicit conversion could have accepted. The old overload resolution in mp_vectorcall read it through HaveImplicit() to decide whether to retry all overloads in a second round with kAllowImplicit (CPPOverload.cxx at 3df629f). "Adopt Clang-driven overload resolution from CppInterOp" removed that second round, so HaveImplicit() lost its only caller and the flag has been written but never read since. Remove the four writes in Converters.cxx, HaveImplicit() and the enumerator. * Remove the unused is_conversion_operator computation "Adopt Clang-driven overload resolution from CppInterOp" made mp_vectorcall compute whether any overload in the set is a conversion operator, but the result is never read (unlike is_operator, which is passed on to BestOverloadFunctionMatch). Remove it together with CPPMethod::IsConversionOperator, which it was the only caller of. * Drop the unused include from Converters.cxx Converters.cxx used std::regex only for s_fnptr, which recognized function-pointer spellings in CreateConverter. "Adopt Clang-driven overload resolution from CppInterOp" replaced that string matching with type queries and removed s_fnptr; nothing in the file uses any more. * Remove unreachable statements after return/throw * Remove SmartPtrConverter's never-overridden GetAddressSpecialCase SmartPtrConverter declares its own virtual GetAddressSpecialCase that always returns false, and no class derives from SmartPtrConverter, so the special-case branch in SmartPtrConverter::SetArg (meant for e.g. passing nullptr to a smart pointer reference) can never be taken. It was already dead at 3df629f. Remove the hook, the branch and the TODO that discussed it; non-instances are rejected as before. * Drop the redundant unicodeobject.h include from TemplateProxy.cxx "Adopt Clang-driven overload resolution from CppInterOp" added a direct line above) already includes; nothing else in the file needed it. * removed commented code (cherry picked from commit 25f0ed3) --- .../pyroot/cppjit/src/cpyrt/CPPDataMember.cxx | 11 ------ .../pyroot/cppjit/src/cpyrt/CPPDataMember.h | 11 ------ bindings/pyroot/cppjit/src/cpyrt/CPPMethod.h | 3 -- .../pyroot/cppjit/src/cpyrt/CPPOverload.cxx | 4 -- bindings/pyroot/cppjit/src/cpyrt/CPPScope.cxx | 2 - .../pyroot/cppjit/src/cpyrt/CallContext.h | 12 ------ .../pyroot/cppjit/src/cpyrt/Converters.cxx | 19 +-------- .../cppjit/src/cpyrt/DeclareConverters.h | 2 - .../pyroot/cppjit/src/cpyrt/LowLevelViews.cxx | 17 ++++---- .../pyroot/cppjit/src/cpyrt/TemplateProxy.cxx | 39 ------------------- .../cppjit/src/cpyrt/TupleOfInstances.cxx | 3 -- bindings/pyroot/cppjit/src/cpyrt/Utility.cxx | 13 ------- bindings/pyroot/cppjit/src/cpyrt/Utility.h | 3 -- 13 files changed, 8 insertions(+), 131 deletions(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPDataMember.cxx b/bindings/pyroot/cppjit/src/cpyrt/CPPDataMember.cxx index 582d1dddffa6d..e737c18154c0b 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPDataMember.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPDataMember.cxx @@ -382,17 +382,6 @@ void cpyrt::CPPDataMember::Set(interop::TCppScope_t scope, fDescription = PyUnicode_FromString(name.c_str()); } -//----------------------------------------------------------------------------- -void cpyrt::CPPDataMember::Set(interop::TCppScope_t scope, - const std::string& name, void* address) { - fEnclosingScope = scope; - fDescription = PyUnicode_FromString(name.c_str()); - fOffset = (intptr_t)address; - fFlags = kIsStaticData | kIsConstData; - fConverter = CreateConverter("internal_enum_type_t"); - fFullType = "unsigned int"; -} - //----------------------------------------------------------------------------- void* cpyrt::CPPDataMember::GetAddress(CPPInstance* pyobj) { // class attributes, global properties diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPDataMember.h b/bindings/pyroot/cppjit/src/cpyrt/CPPDataMember.h index d3b0d49ac7f61..bb8973114ea89 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPDataMember.h +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPDataMember.h @@ -14,7 +14,6 @@ class CPPInstance; class CPPDataMember { public: void Set(interop::TCppScope_t scope, interop::TCppScope_t var); - void Set(interop::TCppScope_t scope, const std::string& name, void* address); std::string GetName(); void* GetAddress(CPPInstance* pyobj /* owner */); @@ -57,16 +56,6 @@ inline CPPDataMember* CPPDataMember_New(interop::TCppScope_t scope, return pyprop; } -inline CPPDataMember* CPPDataMember_NewConstant(interop::TCppScope_t scope, - const std::string& name, - void* address) { - // Create an initialize a new property descriptor, given the C++ datum. - CPPDataMember* pyprop = (CPPDataMember*)CPPDataMember_Type.tp_new( - &CPPDataMember_Type, nullptr, nullptr); - pyprop->Set(scope, name, address); - return pyprop; -} - } // namespace cppjit::cpyrt #endif // !CPYRT_CPPDATAMEMBER_H diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPMethod.h b/bindings/pyroot/cppjit/src/cpyrt/CPPMethod.h index 85f66c66f4955..a3e7ccf3c1275 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPMethod.h +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPMethod.h @@ -75,9 +75,6 @@ class CPPMethod : public PyCallable { } // extra info methods bool IsOperator() { return interop::IsOperator(fMethod.data); } - bool IsConversionOperator() { - return interop::IsConversionOperator(fMethod.data); - } bool IsStaticMethod() { return interop::IsStaticMethod(fMethod); } public: diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx index 3a4ae0c6def95..25efe5e19b44c 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx @@ -606,14 +606,10 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, std::vector overloads; bool is_operator = false; - bool is_conversion_operator = false; bool is_static = false; for (auto i : pymeth->fMethodInfo->fMethods) { if (is_operator || dynamic_cast(i)->IsOperator()) is_operator = true; - if (is_conversion_operator || - dynamic_cast(i)->IsConversionOperator()) - is_conversion_operator = true; if (is_static || dynamic_cast(i)->IsStaticMethod()) is_static = true; overloads.push_back(i->GetMethod()); diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPScope.cxx b/bindings/pyroot/cppjit/src/cpyrt/CPPScope.cxx index 26b47450e7dec..fde38d0d8a5b5 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPScope.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPScope.cxx @@ -590,7 +590,6 @@ static PyObject* meta_reflex(CPPScope* klass, PyObject* args) { if (klass->fFlags & CPPScope::kIsNamespace) Py_RETURN_TRUE; Py_RETURN_FALSE; - break; case interop::Reflex::IS_AGGREGATE: // this is not the strict C++ definition of aggregates, but is closer to // what is needed for Numba and C calling conventions (TODO: probably have @@ -599,7 +598,6 @@ static PyObject* meta_reflex(CPPScope* klass, PyObject* args) { !interop::HasVirtualDestructor(klass->fCppType)) Py_RETURN_TRUE; Py_RETURN_FALSE; - break; } PyErr_Format(PyExc_ValueError, "unsupported reflex request %d or format %d", diff --git a/bindings/pyroot/cppjit/src/cpyrt/CallContext.h b/bindings/pyroot/cppjit/src/cpyrt/CallContext.h index cc3bf232ee10d..4c263a90241e6 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CallContext.h +++ b/bindings/pyroot/cppjit/src/cpyrt/CallContext.h @@ -30,12 +30,8 @@ struct CallContext { enum ECallFlags { kNone = 0x000000, - // UNUSED with new overload resolution - // kIsSorted = 0x000001, // if method overload priority - // determined kIsCreator = 0x000002, // if method creates python-owned objects kIsConstructor = 0x000004, // if method is a C++ constructor - kHaveImplicit = 0x000008, // indicate that implicit converters are available kAllowImplicit = 0x000010, // indicate that implicit conversions are allowed kNoImplicit = 0x000020, // disable implicit to prevent recursion kCallDirect = 0x000040, // call wrapped method directly, no inheritance @@ -52,8 +48,6 @@ struct CallContext { kUseFFI = 0x010000, // not implemented kIsPseudoFunc = 0x020000, // internal, used for introspection kUseStrict = 0x040000, // if method applies strict memory policy - kIsMethod = 0x080000, // If this is a class method - kIsSatticMethod = 0x100000, // If this is a static class method }; // Policies about memory handling and signal safety @@ -113,12 +107,6 @@ inline bool IsConstructor(uint64_t flags) { return flags & CallContext::kIsConstructor; } -inline bool HaveImplicit(CallContext* ctxt) { - return ctxt ? (!(ctxt->fFlags & CallContext::kNoImplicit) && - (ctxt->fFlags & CallContext::kHaveImplicit)) - : false; -} - inline bool AllowImplicit(CallContext* ctxt) { return ctxt ? (!(ctxt->fFlags & CallContext::kNoImplicit) && (ctxt->fFlags & CallContext::kAllowImplicit)) diff --git a/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx b/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx index 4a2265eaf9a1e..2433dfe223405 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx @@ -26,7 +26,6 @@ using namespace cppjit; #include #include #include // for wstring_convert -#include #include #include // for ptrdiff_t #include @@ -359,8 +358,6 @@ static inline bool ImplicitBool(PyObject* pyobject, cpyrt::CallContext* ctxt) { if (!AllowImplicit(ctxt) && (PyBool_Check(pyobject) || (IsCPPEnumInstance(pyobject) && !NoImplicit(ctxt)))) { - if (!NoImplicit(ctxt)) - ctxt->fFlags |= CallContext::kHaveImplicit; return false; } return true; @@ -369,8 +366,6 @@ static inline bool ImplicitBool(PyObject* pyobject, cpyrt::CallContext* ctxt) { static inline bool StrictBool(PyObject* pyobject, cpyrt::CallContext* ctxt) { using namespace cppjit::cpyrt; if (!AllowImplicit(ctxt) && !PyBool_Check(pyobject)) { - if (!NoImplicit(ctxt)) - ctxt->fFlags |= CallContext::kHaveImplicit; return false; } return true; @@ -538,8 +533,6 @@ static inline cpyrt::CPPInstance* ConvertImplicit(interop::TCppScope_t klass, PyTypeObject* pytype = (PyTypeObject*)Py_TYPE(pyobject); if (!(pytype == &PyList_Type || pytype == &PyTuple_Type)) { // || !CPPInstance_Check(pyobject))) { - if (!NoImplicit(ctxt)) - ctxt->fFlags |= CallContext::kHaveImplicit; return nullptr; } } @@ -1321,8 +1314,6 @@ bool cpyrt::EnumConverter::SetArg(PyObject* pyobject, Parameter& para, PyErr_Clear(); } if (!exact) { - if (!NoImplicit(ctxt)) - ctxt->fFlags |= CallContext::kHaveImplicit; return false; } // the base integer converter defers enum instances, so lift the gate @@ -3145,16 +3136,8 @@ bool cpyrt::SmartPtrConverter::SetArg(PyObject* pyobject, Parameter& para, CallContext* ctxt) { char typeCode = fIsRef ? 'p' : 'V'; - if (!CPPInstance_Check(pyobject)) { - // TODO: not sure how this is correct for pass-by-ref nor does it help with - // implicit conversions for pass-by-value - if (fIsRef && GetAddressSpecialCase(pyobject, para.fValue.fVoidp)) { - para.fTypeCode = typeCode; // allow special cases such as nullptr - return true; - } - + if (!CPPInstance_Check(pyobject)) return false; - } CPPInstance* pyobj = (CPPInstance*)pyobject; interop::TCppScope_t oisa = pyobj->ObjectIsA(); diff --git a/bindings/pyroot/cppjit/src/cpyrt/DeclareConverters.h b/bindings/pyroot/cppjit/src/cpyrt/DeclareConverters.h index c339463464ade..34f45ec4f2310 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/DeclareConverters.h +++ b/bindings/pyroot/cppjit/src/cpyrt/DeclareConverters.h @@ -504,8 +504,6 @@ class SmartPtrConverter : public Converter { std::string GetFailureMsg() override { return "[SmartPtrConverter]"; }; protected: - virtual bool GetAddressSpecialCase(PyObject*, void*&) { return false; } - interop::TCppScope_t fSmartPtrType; interop::TCppScope_t fUnderlyingType; bool fKeepControl; diff --git a/bindings/pyroot/cppjit/src/cpyrt/LowLevelViews.cxx b/bindings/pyroot/cppjit/src/cpyrt/LowLevelViews.cxx index 94e06850af62d..f3afd73c4dcc6 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/LowLevelViews.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/LowLevelViews.cxx @@ -864,18 +864,15 @@ static PyObject* ll_array(cpyrt::LowLevelView* self, PyObject* args, Py_DECREF(npfrombuf); return view; - - } else { // copy requested - PyObject* npcopy = PyObject_GetAttr(npmod, cpyrt::PyStrings::gCopy); - PyObject* newarr = - PyObject_CallFunctionObjArgs(npcopy, (PyObject*)self, nullptr); - Py_DECREF(npcopy); - - return newarr; } - // never get here - return nullptr; + // copy requested + PyObject* npcopy = PyObject_GetAttr(npmod, cpyrt::PyStrings::gCopy); + PyObject* newarr = + PyObject_CallFunctionObjArgs(npcopy, (PyObject*)self, nullptr); + Py_DECREF(npcopy); + + return newarr; } //--------------------------------------------------------------------------- diff --git a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx index d368c9d10d152..81d068de76f23 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx @@ -1,6 +1,5 @@ // Bindings #include "cpyrt.h" -#include "unicodeobject.h" using namespace cppjit; #include "CPPClassMethod.h" @@ -74,18 +73,6 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, (void)addedSelf; } - // adjust arguments for self if this is a rebound global function - // bool isNS = (((CPPScope*)fTI->fPyClass)->fFlags & CPPScope::kIsNamespace); - // if (!fTI->fIsConstructor && !isNS && cpyrt_PyArgs_GET_SIZE(args, nargsf) && - // \ - // (!fSelf || - // (fSelf == Py_None && - // !interop::IsStaticTemplate(((CPPScope*)fTI->fPyClass)->fCppType, - // fname)))) { - // args += 1; - // nargsf -= 1; - // } - Py_ssize_t argc = cpyrt_PyArgs_GET_SIZE(args, nargsf); if (argc != 0) { PyObject* tpArgs = PyTuple_New(skipSelf ? argc - 1 : argc); @@ -502,32 +489,6 @@ static inline void UpdateDispatchMap(TemplateProxy* pytmpl, bool use_targs, v.push_back(std::make_pair(sighash, pymeth)); } -static inline PyObject* -SelectAndForward(TemplateProxy* pytmpl, CPPOverload* pymeth, - cpyrt_PyArgs_t args, size_t nargsf, PyObject* kwds, - bool implicitOkay, bool use_targs, uint64_t sighash, - std::vector& errors) { - // Forward a call to known overloads, if any. - if (pymeth->HasMethods()) { - PyObject* pycall = CPPOverload_Type.tp_descr_get( - (PyObject*)pymeth, pytmpl->fSelf, (PyObject*)&CPPOverload_Type); - - if (!implicitOkay) - ((CPPOverload*)pycall)->fFlags |= CallContext::kNoImplicit; - - // now call the method with the arguments (loops internally) - PyObject* result = cpyrt_tp_call(pycall, args, nargsf, kwds); - Py_DECREF(pycall); - if (result) { - UpdateDispatchMap(pytmpl, use_targs, sighash, pymeth); - TPPCALL_RETURN; - } - Utility::FetchError(errors); - } - - return nullptr; -} - static inline PyObject* CallMethodImp(TemplateProxy* pytmpl, PyObject*& pymeth, cpyrt_PyArgs_t args, size_t nargsf, PyObject* kwds, bool impOK, diff --git a/bindings/pyroot/cppjit/src/cpyrt/TupleOfInstances.cxx b/bindings/pyroot/cppjit/src/cpyrt/TupleOfInstances.cxx index db31fa36b927d..23353aec3dd90 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/TupleOfInstances.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/TupleOfInstances.cxx @@ -204,9 +204,6 @@ PyObject* TupleOfInstances_New(interop::TCppObject_t address, return arr; } - - // never get here - return nullptr; } //= cpyrt custom tuple-like array type ==================================== diff --git a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx index 1a8312fa4d129..ed2d5250b617a 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx @@ -974,8 +974,6 @@ static bool AddTypeName(std::vector& types, PyObject* tn, throw std::runtime_error( "This path is not yet implemented (AddTypeName) \n"); - return true; - } else PyErr_Clear(); } @@ -1614,17 +1612,6 @@ cpyrt::Utility::PyOperators::~PyOperators() { Py_XDECREF(fHash); } -//---------------------------------------------------------------------------- -PyObject* cpyrt::Utility::PyErr_Occurred_WithGIL() { - // Re-acquire the GIL before calling PyErr_Occurred() in case it has been - // released; note that the p2.2 code assumes that there are no callbacks in - // C++ to python (or at least none returning errors). - PythonGILRAII python_gil_raii; - PyObject* e = PyErr_Occurred(); - - return e; -} - //---------------------------------------------------------------------------- cpyrt::Utility::PyError_t cpyrt::Utility::FetchPyError() { // create a PyError_t RAII object that will capture and store the exception diff --git a/bindings/pyroot/cppjit/src/cpyrt/Utility.h b/bindings/pyroot/cppjit/src/cpyrt/Utility.h index d80dedb59ba2e..4858341364484 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Utility.h +++ b/bindings/pyroot/cppjit/src/cpyrt/Utility.h @@ -109,9 +109,6 @@ struct PyOperators { std::string ClassName(PyObject* pyobj); bool IsSTLIterator(const std::string& classname); -// for threading: save call to PyErr_Occurred() -PyObject* PyErr_Occurred_WithGIL(); - // helpers for collecting/maintaining python exception data struct PyError_t { struct PyObjectDeleter { From d508a1a46a8d8884b753b1dd9804fc4a59f97627 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Wed, 14 Oct 2026 13:09:21 +0530 Subject: [PATCH 16/71] [cppjit] fix compiler-research/cppjit#151 (cherry picked from commit 425cada) --- .../pyroot/cppjit/src/cpyrt/CPPOverload.h | 17 +++- bindings/pyroot/cppjit/src/cpyrt/Utility.cxx | 76 ++++++++------- bindings/pyroot/cppjit/src/cpyrt/Utility.h | 1 + bindings/pyroot/cppjit/test/test_templates.py | 96 ++++++++++++++++++- 4 files changed, 154 insertions(+), 36 deletions(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.h b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.h index 7d17e00f02d5c..cfefe4dfe47de 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.h +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.h @@ -3,6 +3,7 @@ // Bindings #include "PyCallable.h" +#include "Utility.h" #include "cppjit_interop.h" // Standard @@ -111,10 +112,20 @@ inline uint64_t HashSignature(cpyrt_PyArgs_t args, size_t nargsf) { #else hash += (uint64_t)(Py_REFCNT(pyobj) == 1 ? 1 : 0); #endif - if (CPPOverload_CheckExact(pyobj)) { + if (PyCallable_Check(pyobj)) { static std::hash hash_string; - CPPOverload* overload = (CPPOverload*)pyobj; - hash += hash_string(overload->fMethodInfo->fName); + if (CPPOverload_CheckExact(pyobj)) { + CPPOverload* overload = (CPPOverload*)pyobj; + hash += hash_string(overload->fMethodInfo->fName); + } else { + // This is a python function. Hash based on the function annotation if + // avaiable, otherwise the object itself. + std::string sig = Utility::GetPyCallableAnnotation(pyobj); + if (!sig.empty()) + hash += hash_string(sig); + else + hash += (uint64_t)pyobj; + } } hash += (hash << 10); hash ^= (hash >> 6); diff --git a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx index ed2d5250b617a..7f0798c3e25f0 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx @@ -423,6 +423,45 @@ static inline std::string AnnotationAsText(PyObject* pyobj) { return PyUnicode_AsUTF8(pyobj); } +std::string cpyrt::Utility::GetPyCallableAnnotation(PyObject* pyobj) { + assert(PyCallable_Check(pyobj)); + + PyObject* annot = PyObject_GetAttr(pyobj, PyStrings::gAnnotations); + if (!annot) { + PyErr_Clear(); + return {}; + } + + if (!PyDict_Check(annot)) { + PyErr_Clear(); + Py_DECREF(annot); + return {}; + } + + PyObject* ret = PyDict_GetItemString(annot, "return"); + if (!ret) { + Py_DECREF(annot); + return {}; + } + + // dict is ordered, with the last value being the return type + std::ostringstream tpn; + tpn << (CPPScope_Check(ret) ? ClassName(ret) : AnnotationAsText(ret)) + << " (*)("; + + PyObject* values = PyDict_Values(annot); + for (Py_ssize_t i = 0; i < (PyList_GET_SIZE(values) - 1); ++i) { + if (i) + tpn << ", "; + PyObject* item = PyList_GET_ITEM(values, i); + tpn << (CPPScope_Check(item) ? full_scope(ClassName(item)) : AnnotationAsText(item)); + } + tpn << ')'; + Py_DECREF(values); + Py_DECREF(annot); + return tpn.str(); +} + bool cpyrt::Utility::AddTypeName(std::string& tmpl_name, PyObject* tn, PyObject* arg, cpyrt::Utility::ArgPreference pref, @@ -638,38 +677,11 @@ bool cpyrt::Utility::AddTypeName(std::string& tmpl_name, PyObject* tn, if (arg && PyCallable_Check(arg)) { // annotated/typed Python function - PyObject* annot = PyObject_GetAttr(arg, PyStrings::gAnnotations); - if (annot) { - if (PyDict_Check(annot)) { - PyObject* ret = PyDict_GetItemString(annot, "return"); - if (ret) { - // dict is ordered, with the last value being the return type - std::ostringstream tpn; - tpn << (CPPScope_Check(ret) ? ClassName(ret) : AnnotationAsText(ret)) - << " (*)("; - - PyObject* values = PyDict_Values(annot); - for (Py_ssize_t i = 0; i < (PyList_GET_SIZE(values) - 1); ++i) { - if (i) - tpn << ", "; - PyObject* item = PyList_GET_ITEM(values, i); - tpn << (CPPScope_Check(item) ? full_scope(ClassName(item)) - : AnnotationAsText(item)); - } - Py_DECREF(values); - - tpn << ')'; - tmpl_name.append(tpn.str()); - - Py_DECREF(annot); - return true; - - } else - PyErr_Clear(); - } - Py_DECREF(annot); - } else - PyErr_Clear(); + std::string sig = GetPyCallableAnnotation(arg); + if (!sig.empty()) { + tmpl_name.append(sig); + return true; + } // ctypes function pointer PyObject* argtypes = nullptr; diff --git a/bindings/pyroot/cppjit/src/cpyrt/Utility.h b/bindings/pyroot/cppjit/src/cpyrt/Utility.h index 4858341364484..312ad6f39af67 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Utility.h +++ b/bindings/pyroot/cppjit/src/cpyrt/Utility.h @@ -37,6 +37,7 @@ PyCallable* FindBinaryOperator( // helper for template classes and methods enum ArgPreference { kNone, kPointer, kReference, kValue }; +std::string GetPyCallableAnnotation(PyObject* pyobj); bool AddTypeName(std::string& tmpl_name, PyObject* tn, PyObject* arg, ArgPreference pref, int* pcnt = nullptr); std::string ConstructTemplateArgs(PyObject* pyname, PyObject* tpArgs, diff --git a/bindings/pyroot/cppjit/test/test_templates.py b/bindings/pyroot/cppjit/test/test_templates.py index eedff095f7079..cbbe8ce2938d9 100644 --- a/bindings/pyroot/cppjit/test/test_templates.py +++ b/bindings/pyroot/cppjit/test/test_templates.py @@ -1466,7 +1466,101 @@ def test39_monkey_patching_template_proxy(self): assert a.m([1, 2, 3]) assert not a.m(42) - def test40_instantiation_failure_error_message(self): + def test40_annotated_callables_select_instantiation(self): + """Annotated Python callables each deduce their own instantiation""" + + import cppjit + + cppjit.cppdef("""\ + #include + namespace AnnotatedCallables { + template + auto call(F f) { return f(2.5); } + + int which(int (*f)(int)) { return 1000 + f(2); } + int which(int (*f)(float)) { return 2000 + f(2.5f); } + + double via_std_function(std::function f) { return f(3); } + template + double via_std_function_t(std::function f, T x) { return f(x); } + + void run(void (*f)(int), int x) { f(x); } + + struct Holder { + Holder(std::function f) : value(f(4)) {} + double value; + }; + double via_holder(const Holder& h) { return h.value; } + }""") + + ns = cppjit.gbl.AnnotatedCallables + + def A(x: int) -> int: + return x * 10 + + def B(x: float) -> int: + return int(x * 2) + + def C(x: float) -> float: + return x * 2.0 + + def D(x): + return x + + class K: + def m(self, x: float) -> float: + return x * 3 + + assert ns.call(A) == 20 + + # an unannotated callable cannot be wrapped, as the signature is not + # avaiable on both C++ nor Python side + with raises(Exception): + ns.call(D) + + # the first instantiation (from A) used to be reused for B, which was + # then called with its argument truncated to int; repeat to also cover + # the memoized dispatch + for _ in range(2): + assert ns.call(A) == 20 + assert ns.call(B) == 5 + assert ns.call(C) == 5.0 + assert ns.call(K().m) == 7.5 + + assert ns.call["int (*)(float)"](B) == 5 + + # non-template overloads likewise go by the annotations + for _ in range(2): + assert ns.which(A) == 1020 + assert ns.which(B) == 2005 + + # std::function takes any callable whose arguments and result convert + def F(x: int) -> float: + return x * 1.5 + + assert ns.via_std_function(F) == 4.5 + assert ns.via_std_function(A) == 30.0 + + # also when selecting among known instantiations, which disallows + # implicit conversions + assert ns.via_std_function_t["int"](F, 2) == 3.0 + assert ns.via_std_function_t(F, 2) == 3.0 + assert ns.via_std_function_t(A, 2) == 20.0 + + # or when converting implicitly through a constructor + assert ns.via_holder(F) == 6.0 + assert ns.via_holder(A) == 40.0 + + # a None annotation declares a void return + seen = [] + + def G(x: int) -> None: + seen.append(x) + + ns.run(G, 7) + assert seen == [7] + + def test41_instantiation_failure_error_message(self): """Rejected instantiation names the template, not garbage""" import cppjit From 142dbc571fd4d85ebb1fc02cc8ac823dff2b181a Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Wed, 14 Oct 2026 16:18:05 +0530 Subject: [PATCH 17/71] [cppinterop] Pass ambiguous_candidates in the TemplatedOperatorArrow test BestOverloadFunctionMatch now takes an out-parameter for ambiguous candidates. Update the ROOT-only FunctionReflection_TemplatedOperatorArrow test to the new signature. --- .../unittests/CppInterOp/FunctionReflectionTest.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp index bbba8027274ad..c6be579ffab95 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp @@ -2822,7 +2822,9 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ASSERT_EQ(ops.size(), 1); EXPECT_TRUE(Cpp::IsTemplatedFunction(ops[0])); - Cpp::FuncRef Deref = Cpp::BestOverloadFunctionMatch(ops, {}, {}); + std::vector ambiguous_candidates; + Cpp::FuncRef Deref = + Cpp::BestOverloadFunctionMatch(ops, {}, {}, ambiguous_candidates); ASSERT_TRUE(Deref); // The match is an instantiation with the defaulted template parameter, not // the template pattern itself: its return type is concrete. From 2352deacf5ae2eb0613aa2f162f68f80885ef5ce Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 22:34:34 +0530 Subject: [PATCH 18/71] [cppjit] Match the selected overload by canonical declaration `BestOverloadFunctionMatch` returns the declaration that was passed in as a candidate, but `mp_vectorcall` compared it against the *canonical* declaration of each method in the overload. When a function has more than one declaration and the one in the overload is not the first (e.g. ROOT forward-declares functions of autoloaded libraries before the header is parsed), the comparison never matched and the call failed with "Overload Resolution Failed" even though Clang found a unique best match. Compare the canonical declarations on both sides. Assisted-by: claude-code:claude-opus-5-5 --- bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx index 25efe5e19b44c..479e79abbd4e7 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx @@ -830,8 +830,9 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, } } if (meth) { + interop::TCppScope_t canon_meth = interop::GetUnderlyingScope(meth.data); for (auto i : pymeth->fMethodInfo->fMethods) { - if (interop::GetUnderlyingScope(i->GetMethod().data).data == meth) { + if (interop::GetUnderlyingScope(i->GetMethod().data) == canon_meth) { PyObject* result = i->Call(im_self, args, nargsf, kwds, &ctxt); if (result) { // success: update the dispatch map for subsequent calls From 42d2c348a480f096bfa337cf7a9e8f1a7bf106ff Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 22:36:15 +0530 Subject: [PATCH 19/71] [cppjit] Use the C++ element type name of low-level views in overloads `AddTypeName` spells a `LowLevelView` argument with the view's `fCppTypeName`, which was set to the converter name of the element type. For (un)signed char and std::byte views that name is the internal converter alias `UCharAsInt`/`SCharAsInt`, which is not a C++ type, so the overload resolution could never match e.g. `uint8_t* arr[8]` for an array created with `cppjit.ll.array_new["uint8_t*"]`. Record the C++ spelling of the element type instead; the converter name is unchanged. Fixes test_regression.py::test37_array_of_pointers_argument. Assisted-by: claude-code:claude-opus-5-5 --- bindings/pyroot/cppjit/src/cpyrt/LowLevelViews.cxx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/LowLevelViews.cxx b/bindings/pyroot/cppjit/src/cpyrt/LowLevelViews.cxx index f3afd73c4dcc6..e94005dd228ad 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/LowLevelViews.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/LowLevelViews.cxx @@ -1064,6 +1064,16 @@ template <> struct typecode_traits> { static constexpr const char* name = "std::complex"; }; +// C++ spelling of the element type +template const char* cpp_type_name() { + return typecode_traits::name; +} +template <> const char* cpp_type_name() { return "signed char"; } +template <> const char* cpp_type_name() { + return "unsigned char"; +} +template <> const char* cpp_type_name() { return "std::byte"; } + } // unnamed namespace //--------------------------------------------------------------------------- @@ -1092,7 +1102,7 @@ CreateLowLevelViewT(T* address, cpyrt::cdims_t shape, PyObject* args = PyTuple_New(0); LowLevelView* llp = (LowLevelView*)LowLevelView_Type.tp_new( &LowLevelView_Type, args, nullptr); - llp->fCppTypeName = (format ? "void" : typecode_traits::name); + llp->fCppTypeName = (format ? "void" : cpp_type_name()); Py_DECREF(args); Py_buffer& view = llp->fBufInfo; From 83ea4bc470614db1109bfe796ac2b906c140d2f1 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 22:36:55 +0530 Subject: [PATCH 20/71] [cppjit] Spell ctypes arguments as their C types in overload prototypes `AddTypeName` had no case for ctypes objects and fell back to the Python type's `__name__`, putting non-existent C++ types such as `c_void_p` or `c_int` into the prototype handed to Clang. Overload resolution then only succeeded through the argument-count fallback, and always failed for templates and for overload sets with several candidates of the same arity, e.g. `cppjit.ll.array_delete["void"](ctypes.c_void_p())` or ROOT's `TTree::Branch(name, ctypes.c_void_p(buf), leaflist)`. A ctypes object is a C object owned by Python, so spell it as an lvalue of its C type: `c_int` -> `int&`, `c_void_p` -> `void*&`, `c_char_p` -> `char*&`, `POINTER(c_int)()` -> `int*&`. An lvalue also binds to pointer parameters through the existing reference-to-pointer promotion, which keeps passing e.g. a `c_int` to an `int*` working. Arrays are spelled as a pointer to their elements and `ctypes.byref(obj)` as a pointer to `obj`. Fixes test_lowlevel.py::test17_array_delete_multidim. Assisted-by: claude-code:claude-opus-5-5 --- bindings/pyroot/cppjit/src/cpyrt/Utility.cxx | 163 +++++++++++++++++++ bindings/pyroot/cppjit/test/test_lowlevel.py | 47 ++++++ 2 files changed, 210 insertions(+) diff --git a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx index 7f0798c3e25f0..a1e11daa7ec2b 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx @@ -462,6 +462,165 @@ std::string cpyrt::Utility::GetPyCallableAnnotation(PyObject* pyobj) { return tpn.str(); } +namespace { +struct CTypesTypes { + PyObject* fSimple = nullptr; // ctypes._SimpleCData + PyObject* fPointer = nullptr; // ctypes._Pointer + PyObject* fArray = nullptr; // ctypes.Array + PyObject* fCArg = nullptr; // type of ctypes.byref(...) +}; + +const CTypesTypes& GetCTypesTypes() { + // kept alive for the lifetime of the module + static CTypesTypes types = []() { + CTypesTypes t; + PyObject* ctypes = PyImport_ImportModule("ctypes"); + if (!ctypes) { + PyErr_Clear(); + return t; + } + t.fSimple = PyObject_GetAttrString(ctypes, "_SimpleCData"); + t.fPointer = PyObject_GetAttrString(ctypes, "_Pointer"); + t.fArray = PyObject_GetAttrString(ctypes, "Array"); + PyObject* cint = PyObject_GetAttrString(ctypes, "c_int"); + PyObject* byref = PyObject_GetAttrString(ctypes, "byref"); + if (cint && byref) { + PyObject* i = PyObject_CallNoArgs(cint); + PyObject* ref = i ? PyObject_CallOneArg(byref, i) : nullptr; + if (ref) { + t.fCArg = (PyObject*)Py_TYPE(ref); + Py_INCREF(t.fCArg); + } + Py_XDECREF(ref); + Py_XDECREF(i); + } + Py_XDECREF(byref); + Py_XDECREF(cint); + Py_DECREF(ctypes); + PyErr_Clear(); + return t; + }(); + return types; +} + +inline bool IsSubType(PyObject* tn, PyObject* base) { + return base && PyType_Check(tn) && + PyType_IsSubtype((PyTypeObject*)tn, (PyTypeObject*)base); +} + +bool CTypesCppName(std::string& name, PyObject* tn) { + const CTypesTypes& ct = GetCTypesTypes(); + if (IsSubType(tn, ct.fSimple)) { + PyObject* pytc = + PyObject_GetAttr(tn, cppjit::cpyrt::PyStrings::gCTypesType); + const char* tc = + pytc && PyUnicode_Check(pytc) ? PyUnicode_AsUTF8(pytc) : nullptr; + const char* cname = nullptr; + switch (tc ? tc[0] : '\0') { + case '?': + cname = "bool"; + break; + case 'c': + cname = "char"; + break; + case 'u': + cname = "wchar_t"; + break; + case 'b': + cname = "signed char"; + break; + case 'B': + cname = "unsigned char"; + break; + case 'h': + cname = "short"; + break; + case 'H': + cname = "unsigned short"; + break; + case 'i': + cname = "int"; + break; + case 'I': + cname = "unsigned int"; + break; + case 'l': + cname = "long"; + break; + case 'L': + cname = "unsigned long"; + break; + case 'q': + cname = "long long"; + break; + case 'Q': + cname = "unsigned long long"; + break; + case 'f': + cname = "float"; + break; + case 'd': + cname = "double"; + break; + case 'g': + cname = "long double"; + break; + case 'z': + cname = "char*"; + break; + case 'Z': + cname = "wchar_t*"; + break; + case 'P': + cname = "void*"; + break; + } + Py_XDECREF(pytc); + PyErr_Clear(); + if (!cname) + return false; + name.append(cname); + return true; + } + if (IsSubType(tn, ct.fPointer) || IsSubType(tn, ct.fArray)) { + // the _type_ of pointers and arrays is the ctypes type of the elements + PyObject* elem = + PyObject_GetAttr(tn, cppjit::cpyrt::PyStrings::gCTypesType); + bool ok = elem && CTypesCppName(name, elem); + Py_XDECREF(elem); + PyErr_Clear(); + if (ok) + name.push_back('*'); + return ok; + } + return false; +} + +// Spell a ctypes object arg of type tn: it is a C object owned by Python, so +// an lvalue of its C type (e.g. c_int -> int&, POINTER(c_int)() -> int*&). +// Arrays are spelled as pointers to their first element, and the result of +// ctypes.byref(obj) as a pointer to obj. +bool AddCTypesTypeName(std::string& name, PyObject* tn, PyObject* arg) { + const CTypesTypes& ct = GetCTypesTypes(); + if (IsSubType(tn, ct.fArray)) + return CTypesCppName(name, tn); + if (ct.fCArg && tn == ct.fCArg) { + PyObject* obj = PyObject_GetAttrString(arg, "_obj"); + bool ok = obj && CTypesCppName(name, (PyObject*)Py_TYPE(obj)); + Py_XDECREF(obj); + PyErr_Clear(); + if (ok) + name.push_back('*'); + return ok; + } + if (CTypesCppName(name, tn)) { + name.push_back('&'); + return true; + } + return false; +} +} // unnamed namespace + bool cpyrt::Utility::AddTypeName(std::string& tmpl_name, PyObject* tn, PyObject* arg, cpyrt::Utility::ArgPreference pref, @@ -580,6 +739,10 @@ bool cpyrt::Utility::AddTypeName(std::string& tmpl_name, PyObject* tn, Py_XDECREF(array_module); Py_XDECREF(array_type); + // ctypes objects are C objects held by Python + if (arg && AddCTypesTypeName(tmpl_name, tn, arg)) + return true; + if (tn == (PyObject*)&PyList_Type || tn == (PyObject*)&PyTuple_Type) { if (arg && PySequence_Size(arg)) { std::string subtype{"std::initializer_list<"}; diff --git a/bindings/pyroot/cppjit/test/test_lowlevel.py b/bindings/pyroot/cppjit/test/test_lowlevel.py index d741f7f07179f..917ed650b3b7e 100644 --- a/bindings/pyroot/cppjit/test/test_lowlevel.py +++ b/bindings/pyroot/cppjit/test/test_lowlevel.py @@ -779,6 +779,53 @@ def test18_array_delete_fixed(self): ctypes.cast(ctypes.c_void_p(addr), ctypes.POINTER(ctypes.c_int)) ) + def test19_ctypes_overloads(self): + """ctypes objects select the matching overload or template""" + + import ctypes + + import cppjit + + cppjit.cppdef("""\ + namespace CTypesOverloads { + int f(int& i) { i = 42; return 1; } + int f(double& d) { d = 4.2; return 2; } + int f(signed char& c) { c = 7; return 3; } + int p(int* i) { *i = 43; return 1; } + int p(double* d) { *d = 4.3; return 2; } + int pp(int** i) { static int v = 5; *i = &v; return 1; } + int pp(double** d) { return 2; } + template int t(T*) { return (int)sizeof(T); } + long v(void* x) { return (long)x; } + long v(double) { return -1; } + int a(int* x) { return x[1]; } + int a(double* x) { return -1; } + int s(char* c) { return c[0]; } + int s(int) { return -1; } + }""") + + ns = cppjit.gbl.CTypesOverloads + + i = ctypes.c_int(0) + assert ns.f(i) == 1 and i.value == 42 + d = ctypes.c_double(0) + assert ns.f(d) == 2 and d.value == 4.2 + b = ctypes.c_byte(0) + assert ns.f(b) == 3 and b.value == 7 + + i = ctypes.c_int(0) + assert ns.p(i) == 1 and i.value == 43 + d = ctypes.c_double(0) + assert ns.p(ctypes.byref(d)) == 2 and d.value == 4.3 + + ip = ctypes.POINTER(ctypes.c_int)() + assert ns.pp(ip) == 1 and ip[0] == 5 + + assert ns.t(ctypes.c_double(0)) == 8 + assert ns.v(ctypes.c_void_p(0x1234)) == 0x1234 + assert ns.a((ctypes.c_int * 3)(1, 2, 3)) == 2 + assert ns.s(ctypes.c_char_p(b"A")) == ord("A") + class TestMULTIDIMARRAYS: def setup_class(cls): From 37f5201a2656ddae4512f0ce21bd5f50ef779241 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 22:58:06 +0530 Subject: [PATCH 21/71] [cppjit] Spell the object of a call through a smart pointer as the pointee A smart pointer proxy behaves as the object it points to: its Python type is the pointee class, so the methods found on it are those of the pointee (the methods of the smart pointer itself are reached through `__smartptr__()`). The implicit object argument in the overload prototype was however spelled as the smart pointer type (e.g. `std::unique_ptr&, int` for `p.get(1)`), so Clang found no viable candidate whenever the method was overloaded, and the call only worked when the argument-count fallback happened to pick a unique method. Add a `smart_as_pointee` option to `Utility::AddTypeName`, which spells a smart pointer proxy as an lvalue of its pointee, and use it for the object of bound calls of normal and templated methods, and for the first argument of unbound calls of non-static methods (e.g. `A.get(p, 1)`), which is the object as well. Fixes e.g. `RDataFrame.Define()` on the `std::unique_ptr` returned by `ROOT.RDF.FromNumpy()`, and `RFile.Put()`. Assisted-by: claude-code:claude-opus-5-5 --- .../pyroot/cppjit/src/cpyrt/CPPOverload.cxx | 15 +++++++++- .../pyroot/cppjit/src/cpyrt/TemplateProxy.cxx | 6 ++-- bindings/pyroot/cppjit/src/cpyrt/Utility.cxx | 12 ++++++-- bindings/pyroot/cppjit/src/cpyrt/Utility.h | 5 +++- .../pyroot/cppjit/test/test_cpp11features.py | 28 +++++++++++++++++++ 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx index 479e79abbd4e7..370a88fc26333 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx @@ -618,10 +618,18 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, std::string proto = ""; if (im_self && !(ctxt.fFlags & CallContext::kIsConstructor)) { PyObject* self = (PyObject*)im_self; + // methods reached through a smart pointer proxy are those of the pointee + // (the methods of the smart pointer itself are reached through + // __smartptr__()), so the object is the pointee [[maybe_unused]] auto res = - AddTypeName(proto, (PyObject*)Py_TYPE(self), self, Utility::kNone); + AddTypeName(proto, (PyObject*)Py_TYPE(self), self, Utility::kNone, + nullptr, /*smart_as_pointee=*/true); assert(res); } + // in an unbound call of non-static methods, the first argument is the object + const bool self_is_first_arg = !im_self && !IsConstructor(ctxt.fFlags) && + !is_static && !overloads.empty() && + interop::IsNonStaticMethod(overloads.front()); ctxt.fFlags |= CallContext::kAllowImplicit; { @@ -761,6 +769,11 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, } if (!proto.empty()) proto += ", "; + if (i == 0 && self_is_first_arg) { + AddTypeName(proto, typ, obj, Utility::kNone, nullptr, + /*smart_as_pointee=*/true); + continue; + } if (!AddTypeName(proto, typ, obj, Utility::kNone)) { proto += "__cppjit_internal::UnknownType"; // break; // ???: do we need to raise error here diff --git a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx index 81d068de76f23..3a95172e3b3ed 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx @@ -5,6 +5,7 @@ using namespace cppjit; #include "CPPClassMethod.h" #include "CPPConstructor.h" #include "CPPFunction.h" +#include "CPPInstance.h" #include "CPPMethod.h" #include "CPPOverload.h" #include "PyCallable.h" @@ -67,8 +68,9 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, bool skipSelf = fTI->fIsConstructor && (!fSelf || fSelf == Py_None); if (fSelf && (fSelf != Py_None) && (!fTI->fIsConstructor)) { PyObject* self = (PyObject*)fSelf; - bool addedSelf = - AddTypeName(proto, (PyObject*)Py_TYPE(self), self, Utility::kNone); + bool addedSelf = Utility::AddTypeName(proto, (PyObject*)Py_TYPE(self), self, + Utility::kNone, nullptr, + /*smart_as_pointee=*/true); assert(addedSelf); (void)addedSelf; } diff --git a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx index a1e11daa7ec2b..7423617ce796a 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx @@ -623,8 +623,8 @@ bool AddCTypesTypeName(std::string& name, PyObject* tn, PyObject* arg) { bool cpyrt::Utility::AddTypeName(std::string& tmpl_name, PyObject* tn, PyObject* arg, - cpyrt::Utility::ArgPreference pref, - int* pcnt) { + cpyrt::Utility::ArgPreference pref, int* pcnt, + bool smart_as_pointee) { // Determine the appropriate C++ type for a given Python type; this is a // helper because it can recurse if the type is list or tuple and needs // matching on std::vector. @@ -796,7 +796,13 @@ bool cpyrt::Utility::AddTypeName(std::string& tmpl_name, PyObject* tn, if (arg) { // try to specialize the type match for the given object CPPInstance* pyobj = (CPPInstance*)arg; - if (CPPInstance_Check(pyobj)) { + if (CPPInstance_Check(pyobj) && smart_as_pointee && pyobj->IsSmart()) { + // the Python type of a smart pointer proxy is the pointee class + tmpl_name.append( + full_scope(interop::GetScopedFinalName(((CPPClass*)tn)->fCppType))); + tmpl_name.push_back('&'); + resolved = true; + } else if (CPPInstance_Check(pyobj)) { auto cpp_type = interop::GetScopedFinalName( pyobj->IsSmart() ? SMART_TYPE(pyobj) : ((CPPClass*)tn)->fCppType); tmpl_name.append(full_scope(cpp_type)); diff --git a/bindings/pyroot/cppjit/src/cpyrt/Utility.h b/bindings/pyroot/cppjit/src/cpyrt/Utility.h index 312ad6f39af67..9dfed05ca7654 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Utility.h +++ b/bindings/pyroot/cppjit/src/cpyrt/Utility.h @@ -38,8 +38,11 @@ PyCallable* FindBinaryOperator( // helper for template classes and methods enum ArgPreference { kNone, kPointer, kReference, kValue }; std::string GetPyCallableAnnotation(PyObject* pyobj); +// smart_as_pointee: spell a smart pointer proxy as (an lvalue of) the object it +// points to rather than as the smart pointer bool AddTypeName(std::string& tmpl_name, PyObject* tn, PyObject* arg, - ArgPreference pref, int* pcnt = nullptr); + ArgPreference pref, int* pcnt = nullptr, + bool smart_as_pointee = false); std::string ConstructTemplateArgs(PyObject* pyname, PyObject* tpArgs, PyObject* args = nullptr, ArgPreference = kNone, int argoff = 0, diff --git a/bindings/pyroot/cppjit/test/test_cpp11features.py b/bindings/pyroot/cppjit/test/test_cpp11features.py index 53c5cdf3f7dfb..6db5e6612d6bd 100644 --- a/bindings/pyroot/cppjit/test/test_cpp11features.py +++ b/bindings/pyroot/cppjit/test/test_cpp11features.py @@ -695,3 +695,31 @@ def test21_smart_ptr_downcast(self): gbl.pass_val_overloaded(gbl.create_unique_ptr_to_derived()) == "PubDerivedTestSmartPtr" ) + + def test22_smart_ptr_overloaded_methods(self): + """Overloaded and templated methods called through a smart pointer""" + + import cppjit + + cppjit.cppdef("""\ + namespace SmartPtrMethods { + struct Tok { int i = 0; }; + struct Obj { + int get(int) { return 1; } + int get(const std::string&) { return 2; } + int get(Tok) { return 3; } + template int tget(const std::string&, T) { return 4; } + int tget(const std::string&, const std::string&) { return 5; } + }; + std::unique_ptr make_unique() { return std::make_unique(); } + std::shared_ptr make_shared() { return std::make_shared(); } + }""") + + ns = cppjit.gbl.SmartPtrMethods + for p in (ns.make_unique(), ns.make_shared()): + assert p.get(1) == 1 + assert p.get("x") == 2 + assert p.get(ns.Tok()) == 3 + assert p.tget("x", 1.0) == 4 + assert ns.Obj.get(p, 1) == 1 + assert ns.Obj.tget(p, "x", 1.0) == 4 From 650bbe444ccedf9d04904780260d31c1de58396a Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:04:21 +0530 Subject: [PATCH 22/71] [cppjit] Spell numpy scalars and arrays as their C types in overload prototypes numpy scalars and arrays fell back to the Python type's `__name__` in `AddTypeName`, i.e. the non-existent C++ types `float64` or `ndarray`, so overload resolution only succeeded through the argument-count fallback. Overloaded functions such as `TProfile::Fill(Double_t, Double_t)` vs. `Fill(const char*, Double_t)` (hsimple.py) or `TGraph(Int_t, const Double_t*, const Double_t*)` failed. Spell a numpy scalar as a value of the C type of its dtype, and an array as a pointer to its elements, the equivalent of a C array decaying into a pointer (as is already done for `array.array`). This does not allow implicit copies of arrays into e.g. a `std::vector`. numpy is only looked up if it was already imported. Assisted-by: claude-code:claude-opus-5-5 --- bindings/pyroot/cppjit/src/cpyrt/Utility.cxx | 92 ++++++++++++++++++++ bindings/pyroot/cppjit/test/test_lowlevel.py | 28 ++++++ 2 files changed, 120 insertions(+) diff --git a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx index 7423617ce796a..83fcd29b6a7e5 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx @@ -619,6 +619,94 @@ bool AddCTypesTypeName(std::string& name, PyObject* tn, PyObject* arg) { } return false; } + +// C++ type for a numpy dtype character code +const char* NumpyCharToCpp(char tc) { + switch (tc) { + case '?': + return "bool"; + case 'b': + return "signed char"; + case 'B': + return "unsigned char"; + case 'h': + return "short"; + case 'H': + return "unsigned short"; + case 'i': + return "int"; + case 'I': + return "unsigned int"; + case 'l': + return "long"; + case 'L': + return "unsigned long"; + case 'q': + return "long long"; + case 'Q': + return "unsigned long long"; + case 'f': + return "float"; + case 'd': + return "double"; + case 'g': + return "long double"; + case 'F': + return "std::complex"; + case 'D': + return "std::complex"; + case 'G': + return "std::complex"; + } + return nullptr; +} + +// Spell a numpy scalar as a value of its C type, and a numpy array as a +// pointer to its elements (the equivalent of a C array decaying into a +// pointer). +bool AddNumpyTypeName(std::string& name, PyObject* tn, PyObject* arg) { + // kept alive for the module lifetime + static PyObject* np_generic = nullptr; + static PyObject* np_ndarray = nullptr; + if (!np_generic) { + PyObject* modname = PyUnicode_FromString("numpy"); + // numpy is only looked up if it was already imported. + PyObject* numpy = PyImport_GetModule(modname); + Py_DECREF(modname); + if (!numpy) { + PyErr_Clear(); + return false; + } + np_generic = PyObject_GetAttrString(numpy, "generic"); + np_ndarray = PyObject_GetAttrString(numpy, "ndarray"); + Py_DECREF(numpy); + if (!np_generic || !np_ndarray) { + Py_CLEAR(np_generic); + Py_CLEAR(np_ndarray); + PyErr_Clear(); + return false; + } + } + bool is_array = IsSubType(tn, np_ndarray); + if (!is_array && !IsSubType(tn, np_generic)) + return false; + + const char* cname = nullptr; + PyObject* dtype = PyObject_GetAttrString(arg, "dtype"); + PyObject* dchar = dtype ? PyObject_GetAttrString(dtype, "char") : nullptr; + if (dchar && PyUnicode_Check(dchar)) + cname = NumpyCharToCpp(PyUnicode_AsUTF8(dchar)[0]); + Py_XDECREF(dchar); + Py_XDECREF(dtype); + PyErr_Clear(); + if (!cname) + return false; + + name.append(cname); + if (is_array) + name.push_back('*'); + return true; +} } // unnamed namespace bool cpyrt::Utility::AddTypeName(std::string& tmpl_name, PyObject* tn, @@ -743,6 +831,10 @@ bool cpyrt::Utility::AddTypeName(std::string& tmpl_name, PyObject* tn, if (arg && AddCTypesTypeName(tmpl_name, tn, arg)) return true; + // numpy scalars and arrays + if (arg && AddNumpyTypeName(tmpl_name, tn, arg)) + return true; + if (tn == (PyObject*)&PyList_Type || tn == (PyObject*)&PyTuple_Type) { if (arg && PySequence_Size(arg)) { std::string subtype{"std::initializer_list<"}; diff --git a/bindings/pyroot/cppjit/test/test_lowlevel.py b/bindings/pyroot/cppjit/test/test_lowlevel.py index 917ed650b3b7e..ffe918326af18 100644 --- a/bindings/pyroot/cppjit/test/test_lowlevel.py +++ b/bindings/pyroot/cppjit/test/test_lowlevel.py @@ -826,6 +826,34 @@ def test19_ctypes_overloads(self): assert ns.a((ctypes.c_int * 3)(1, 2, 3)) == 2 assert ns.s(ctypes.c_char_p(b"A")) == ord("A") + def test20_numpy_overloads(self): + """numpy scalars and arrays select the matching overload""" + + try: + import numpy as np + except ImportError: + skip("numpy is not installed") + + import cppjit + + cppjit.cppdef("""\ + namespace NumpyOverloads { + int f(double, double) { return 1; } + int f(const char*, double) { return 2; } + double a(int n, const double* x) { double s = 0; for (int i = 0; i < n; ++i) s += x[i]; return s; } + double a(int n, const float* x) { return -1.; } + int v(const std::vector& v) { return (int)v.size(); } + int v(int) { return -1; } + }""") + + ns = cppjit.gbl.NumpyOverloads + assert ns.f(np.float64(1.0), np.float64(2.0)) == 1 + assert ns.a(3, np.array([1.0, 2.0, 3.0])) == 6.0 + assert ns.a(2, np.array([1.0, 2.0], dtype=np.float32)) == -1.0 + # no implicit copy of an array into a std::vector + with raises(cppjit.OverloadResolutionException): + ns.v(np.array([1.0, 2.0])) + class TestMULTIDIMARRAYS: def setup_class(cls): From 9c31ea15ce46c53ebba61b5ab123197e6f3aa739 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:14:47 +0530 Subject: [PATCH 23/71] [cppjit] Derive OverloadResolutionException from TypeError Calling a Python callable with arguments of unsuitable types raises a TypeError, and code using the bindings relies on that, e.g. ROOT's distributed RDataFrame validates its constructor arguments with `except TypeError`, as do several pythonizations that probe overloads. The new OverloadResolutionException (and OverloadAmbiguityException) derived directly from Exception, so these errors escaped such handlers. Make it a subclass of TypeError; code that catches the specific exception is unaffected. Assisted-by: claude-code:claude-opus-5-5 --- bindings/pyroot/cppjit/src/cpyrt/cpyrtModule.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/cpyrtModule.cxx b/bindings/pyroot/cppjit/src/cpyrt/cpyrtModule.cxx index e6706b8771b49..191c15c296c6b 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/cpyrtModule.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/cpyrtModule.cxx @@ -1110,7 +1110,7 @@ PyObject* Init() { PyErr_NewException((char*)"cppjit.ll.AbortSignal", cppfatal, nullptr); PyModule_AddObject(gThisModule, (char*)"AbortSignal", gAbrtException); gOverloadResolutionException = PyErr_NewException( - (char*)"cppjit.OverloadResolutionException", nullptr, nullptr); + (char*)"cppjit.OverloadResolutionException", PyExc_TypeError, nullptr); PyModule_AddObject(gThisModule, (char*)"OverloadResolutionException", gOverloadResolutionException); gOverloadAmbiguityException = From 8f2427feddb15530eaba41b6b4463306e267c9cc Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:16:02 +0530 Subject: [PATCH 24/71] [cppjit] Spell an empty list argument as an initializer_list `AddTypeName` appended nothing for an empty Python list or tuple, so the overload prototype became malformed (e.g. `std::string, ` for `RDataFrame("tree", [])`) and the overload resolution failed outright. Spell it as `std::initializer_list<__cppjit_internal::UnknownType>`: like for a non-empty list, the overload resolution promotes this to the `std::initializer_list` parameter of a candidate. Assisted-by: claude-code:claude-opus-5-5 --- bindings/pyroot/cppjit/src/cpyrt/Utility.cxx | 3 +++ bindings/pyroot/cppjit/test/test_overloads.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx index 83fcd29b6a7e5..3bfe217681918 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx @@ -851,6 +851,9 @@ bool cpyrt::Utility::AddTypeName(std::string& tmpl_name, PyObject* tn, if (!ret) { return false; } + } else if (arg) { + // the element type of an empty list is unknown + tmpl_name.append("std::initializer_list<__cppjit_internal::UnknownType>"); } return true; diff --git a/bindings/pyroot/cppjit/test/test_overloads.py b/bindings/pyroot/cppjit/test/test_overloads.py index b3d98e4b2c68a..420b4e67227fa 100644 --- a/bindings/pyroot/cppjit/test/test_overloads.py +++ b/bindings/pyroot/cppjit/test/test_overloads.py @@ -519,3 +519,22 @@ def test19_ambiguous_set_has_no_duplicates(self): msg = str(exc.value) assert msg.count("double b)") == 1 assert msg.count("float b)") == 1 + + def test20_empty_list_argument(self): + """An empty list selects an initializer_list overload""" + + import cppjit + + cppjit.cppdef("""\ + namespace EmptyListArg { + int f(const std::string&, std::initializer_list l) { return (int)l.size(); } + int f(const std::string&, const std::string&) { return -1; } + int g(std::initializer_list l) { return (int)l.size(); } + int g(double) { return -1; } + }""") + + ns = cppjit.gbl.EmptyListArg + assert ns.f("t", []) == 0 + assert ns.f("t", ["a"]) == 1 + assert ns.g([]) == 0 + assert ns.g(()) == 0 From f76470af27f67b60d9f13ed2bdad0ee20eb93473 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:40:33 +0530 Subject: [PATCH 25/71] [cppjit] Resolve __overload__ of templated method sets with the object `TemplateProxy.__overload__(signature)` resolved the signature through `GetMethodTemplate` without the object argument, although the candidates of a class are non-static methods that take it first. Clang therefore used the first parameter type as the object (crashing in Sema for a non-class type), only templates were considered, and when nothing was found NULL was returned without setting an exception. Add `interop::GetMethodBySignature`, which resolves the non-static methods of the set with an object of the class prepended to the signature, and static methods and functions without it (so that no candidate gets an argument of the wrong kind as its object), sharing the candidate lookup and instantiation with `GetMethodTemplate`. Use it in `__overload__`, including the non-templated overloads in the selection by signature, and raise a LookupError if no overload matches. Assisted-by: claude-code:claude-opus-5-5 --- .../pyroot/cppjit/src/cpyrt/TemplateProxy.cxx | 21 ++- .../cppjit/src/interop/cppjit_interop.h | 9 ++ .../cppjit/src/interop/interop_wrapper.cxx | 137 ++++++++++++------ bindings/pyroot/cppjit/test/test_overloads.py | 25 ++++ 4 files changed, 144 insertions(+), 48 deletions(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx index 3a95172e3b3ed..a89323655d6b6 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx @@ -822,16 +822,19 @@ static PyObject* tpp_overload(TemplateProxy* pytmpl, PyObject* args) { } scope = ((CPPClass*)pytmpl->fTI->fPyClass)->fCppType; - cppmeth = interop::GetMethodTemplate(scope, pytmpl->fTI->fCppName, - proto.substr(1, proto.size() - 2), - ambiguous_candidates); // ???: include_non_templated + cppmeth = interop::GetMethodBySignature(scope, pytmpl->fTI->fCppName, + proto.substr(1, proto.size() - 2), + ambiguous_candidates, + /*include_non_templated=*/true); } else if (PyArg_ParseTuple(args, const_cast("ss:__overload__"), &sigarg, &tmplarg)) { scope = ((CPPClass*)pytmpl->fTI->fPyClass)->fCppType; std::string full_name = std::string(pytmpl->fTI->fCppName) + "<" + tmplarg + ">"; - cppmeth = interop::GetMethodTemplate(scope, full_name, sigarg, ambiguous_candidates); // ???: include_non_templated + cppmeth = interop::GetMethodBySignature(scope, full_name, sigarg, + ambiguous_candidates, + /*include_non_templated=*/false); } else if (PyArg_ParseTuple(args, const_cast("O|i:__overload__"), &sigarg_tuple, &want_const)) { PyErr_Clear(); @@ -861,9 +864,10 @@ static PyObject* tpp_overload(TemplateProxy* pytmpl, PyObject* args) { proto.push_back('>'); scope = ((CPPClass*)pytmpl->fTI->fPyClass)->fCppType; - cppmeth = interop::GetMethodTemplate(scope, pytmpl->fTI->fCppName, - proto.substr(1, proto.size() - 2), - ambiguous_candidates); + cppmeth = interop::GetMethodBySignature(scope, pytmpl->fTI->fCppName, + proto.substr(1, proto.size() - 2), + ambiguous_candidates, + /*include_non_templated=*/true); } else { PyErr_Format(PyExc_TypeError, "Unexpected arguments to __overload__"); return nullptr; @@ -871,6 +875,9 @@ static PyObject* tpp_overload(TemplateProxy* pytmpl, PyObject* args) { // else attempt instantiation if (!cppmeth) { + if (!PyErr_Occurred()) + PyErr_Format(PyExc_LookupError, "signature \"%s\" not found", + sigarg ? sigarg : proto.c_str()); return nullptr; } diff --git a/bindings/pyroot/cppjit/src/interop/cppjit_interop.h b/bindings/pyroot/cppjit/src/interop/cppjit_interop.h index 9fc5e75e32586..150f4ac351ab8 100644 --- a/bindings/pyroot/cppjit/src/interop/cppjit_interop.h +++ b/bindings/pyroot/cppjit/src/interop/cppjit_interop.h @@ -329,6 +329,15 @@ TCppMethod_t GetMethodTemplate(TCppScope_t scope, const std::string& name, const std::string& proto, std::vector& ambiguous_candidates, bool include_non_templated = false); +// Select the overload of the (templated) method set name of scope that takes +// the parameters in the comma-separated signature sig (without the object of +// non-static methods), instantiating it as necessary. +RPY_EXPORTED +TCppMethod_t +GetMethodBySignature(TCppScope_t scope, const std::string& name, + const std::string& sig, + std::vector& ambiguous_candidates, + bool include_non_templated = false); RPY_EXPORTED bool IsNonStaticMethod(TCppMethod_t func); RPY_EXPORTED diff --git a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx index 6d35a7718758c..b381c2eceeb0b 100644 --- a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx +++ b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx @@ -2097,39 +2097,47 @@ interop::TCppMethod_t BestOverloadFunctionMatch( } } // namespace -interop::TCppMethod_t interop::GetMethodTemplate( - TCppScope_t scope, const std::string& name, const std::string& proto, - std::vector& ambiguous_candidates, - bool include_non_templated) { - std::string pureName; - std::string explicit_params; - - if ((name.find("operator<") != 0) && (name.find('<') != std::string::npos)) { - pureName = name.substr(0, name.find('<')); - size_t start = name.find('<'); - size_t end = name.rfind('>'); - explicit_params = name.substr(start + 1, end - start - 1); - } else { - pureName = name; - } - +namespace { +// Split \c name into the name proper and its explicit template arguments (empty +// if there are none), e.g. "f" into "f" and "int, double". +std::pair SplitTemplateName(const std::string& name) { + size_t start = name.find('<'); + if (name.find("operator<") == 0 || start == std::string::npos) + return {name, ""}; + size_t end = name.rfind('>'); + return {name.substr(0, start), name.substr(start + 1, end - start - 1)}; +} + +// Find templated methods of \c name in \c scope. +// If \c include_non_templated is true, +// then non-templated overloads are also included. +std::vector +GetMethodCandidates(interop::TCppScope_t scope, const std::string& name, + bool include_non_templated) { std::lock_guard Lock(InterOpMutex); - std::vector unresolved_candidate_methods; - { - std::lock_guard Lock(InterOpMutex); - Cpp::GetClassTemplatedMethods(pureName, scope, - unresolved_candidate_methods); - if (include_non_templated) { - auto fns = Cpp::GetFunctionsUsingName(scope, pureName); - for (auto i : fns) - unresolved_candidate_methods.push_back(i); - } + std::vector candidates; + Cpp::GetClassTemplatedMethods(name, scope, candidates); + if (include_non_templated) { + auto fns = Cpp::GetFunctionsUsingName(scope, name); + for (auto i : fns) + candidates.push_back(i); } - if (unresolved_candidate_methods.empty() && name.find("operator") == 0) { + if (candidates.empty() && name.find("operator") == 0) { // try operators - interop::GetClassOperators(scope, pureName, unresolved_candidate_methods); + interop::GetClassOperators(scope, name, candidates); } + return candidates; +} + +// Select (instantiating as necessary) the best of the candidates for the +// argument types in proto and the explicit template arguments. +interop::TCppMethod_t ResolveMethodTemplate( + interop::TCppScope_t scope, + const std::vector& candidates, + const std::string& proto, const std::string& explicit_params, + std::vector& ambiguous_candidates) { + std::lock_guard Lock(InterOpMutex); // cpyrt assumes that we attempt instantiation here std::vector arg_types; @@ -2138,30 +2146,77 @@ interop::TCppMethod_t interop::GetMethodTemplate( interop::AppendTypesSlow(explicit_params, templ_params, scope, true); std::vector candidate_methods; - for (auto fn : unresolved_candidate_methods) + for (auto fn : candidates) if (!interop::IsDeletedMethod(fn)) candidate_methods.push_back(fn); - interop::TCppMethod_t cppmeth = nullptr; - { - cppmeth = ::BestOverloadFunctionMatch(candidate_methods, arg_types, - templ_params, ambiguous_candidates, - scope, /*is_operator=*/false); - } + interop::TCppMethod_t cppmeth = ::BestOverloadFunctionMatch( + candidate_methods, arg_types, templ_params, ambiguous_candidates, scope, + /*is_operator=*/false); if (cppmeth) return cppmeth; - if (unresolved_candidate_methods.size() == 1 && !templ_params.empty()) { - cppmeth = Cpp::InstantiateTemplate( - TCppScope_t(unresolved_candidate_methods[0].data), - templ_params.data(), templ_params.size(), - /*instantiate_body=*/false) + if (candidates.size() == 1 && !templ_params.empty()) { + cppmeth = Cpp::InstantiateTemplate(interop::TCppScope_t(candidates[0].data), + templ_params.data(), templ_params.size(), + /*instantiate_body=*/false) .data; } - return TCppMethod_t(cppmeth.data); + return interop::TCppMethod_t(cppmeth.data); // if it fails, use Sema to propogate info about why it failed (DeductionInfo) } +} // namespace + +interop::TCppMethod_t interop::GetMethodTemplate( + TCppScope_t scope, const std::string& name, const std::string& proto, + std::vector& ambiguous_candidates, + bool include_non_templated) { + auto [pure_name, explicit_params] = SplitTemplateName(name); + std::vector candidates = GetMethodCandidates( + scope, pure_name, include_non_templated && explicit_params.empty()); + return ResolveMethodTemplate(scope, candidates, proto, explicit_params, + ambiguous_candidates); +} + +interop::TCppMethod_t interop::GetMethodBySignature( + TCppScope_t scope, const std::string& name, const std::string& sig, + std::vector& ambiguous_candidates, + bool include_non_templated) { + auto [pure_name, explicit_params] = SplitTemplateName(name); + std::vector candidates = GetMethodCandidates( + scope, pure_name, include_non_templated && explicit_params.empty()); + + // The signature lists the parameters only. Non-static methods are called on + // an object, which is the first argument of the overload resolution, while + // static methods and functions don't take it, so resolve them separately. + std::vector on_object, others; + for (auto fn : candidates) + (interop::IsNonStaticMethod(fn) && !interop::IsConstructor(fn) ? on_object + : others) + .push_back(fn); + + interop::TCppMethod_t method = nullptr; + if (!on_object.empty()) { + std::string self_sig = "::" + interop::GetScopedFinalName(scope) + "&"; + if (!sig.empty()) + self_sig += ", " + sig; + method = ResolveMethodTemplate(scope, on_object, self_sig, explicit_params, + ambiguous_candidates); + } + if (!others.empty()) { + interop::TCppMethod_t other = ResolveMethodTemplate( + scope, others, sig, explicit_params, ambiguous_candidates); + if (method && other && method != other) { + ambiguous_candidates.push_back(method); + ambiguous_candidates.push_back(other); + return nullptr; + } + if (!method) + method = other; + } + return method; +} interop::TCppMethod_t interop::BestOverloadFunctionMatch( const std::vector& candidates, diff --git a/bindings/pyroot/cppjit/test/test_overloads.py b/bindings/pyroot/cppjit/test/test_overloads.py index 420b4e67227fa..d939df580db5e 100644 --- a/bindings/pyroot/cppjit/test/test_overloads.py +++ b/bindings/pyroot/cppjit/test/test_overloads.py @@ -538,3 +538,28 @@ def test20_empty_list_argument(self): assert ns.f("t", ["a"]) == 1 assert ns.g([]) == 0 assert ns.g(()) == 0 + + def test21_overload_by_signature_on_method_set(self): + """__overload__ on a set of templated and non-templated methods""" + + import cppjit + + cppjit.cppdef("""\ + namespace OverloadBySignature { + typedef int Int_type; + struct Tree { + template int Branch(const char* name, U* obj, Int_type bufsize = 32000) { return 1; } + int Branch(const char* folder, Int_type bufsize = 32000, Int_type split = 99) { return 3; } + static int Branch(double) { return 4; } + }; + }""") + + t = cppjit.gbl.OverloadBySignature.Tree() + # the signature doesn't match the declaration textually (typedef), so + # it goes through the overload resolution + m = t.Branch.__overload__("const char*, int, int") + assert m(t, "a", 0, 99) == 3 + # a static method doesn't take the object + assert t.Branch.__overload__("double")(1.0) == 4 + with raises(LookupError): + t.Branch.__overload__("double, double, double, double") From 45b1427f92b3733bd10c7069dfe989b1551c4e43 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:57:25 +0530 Subject: [PATCH 26/71] [cppjit] Bind an object of another class to T&& through a temporary When the overload resolution selects a function taking `T&&` for a proxy of a class U that converts to T (e.g. `std::vector::push_back` given an `Axis`, with `Variant(const Axis&)`), C++ initializes the parameter with a temporary T. `InstanceMoveConverter` instead treated the U proxy as the object to move and rejected it as not being an rvalue. Go through the implicit conversion (which creates the movable temporary) when the proxy is not of class T or a class derived from it. Smart pointers are still matched by the reference converter. Fixes e.g. `RDataFrame.Hist([axis1, axis2], ...)` and `RNTupleWriter.Recreate([("x", "float")], ...)`. It also makes `boost::any::operator=` work for an lvalue (test_boost.py test02): the forwarding-reference `operator=(ValueType&&)` still fails to instantiate for it, but the move assignment `operator=(any&&)` now takes a temporary `any` converted from it, as C++ would. Assisted-by: claude-code:claude-opus-5-5 --- .../pyroot/cppjit/src/cpyrt/Converters.cxx | 18 +++++++++++++++++- bindings/pyroot/cppjit/test/test_boost.py | 6 ------ bindings/pyroot/cppjit/test/test_overloads.py | 19 +++++++++++++++++++ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx b/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx index 2433dfe223405..1d2c47f0b3397 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx @@ -2530,7 +2530,23 @@ bool cpyrt::InstanceMoveConverter::SetArg(PyObject* pyobject, Parameter& para, // convert to C++ instance&&, set arg for call CPPInstance* pyobj = GetCppInstance(pyobject, fClass, true /* accept_rvalue */); - if (!pyobj || (pyobj->fFlags & CPPInstance::kIsLValue)) { + // An object of a class other than fClass (or one derived from it) binds + // through a temporary of fClass converted from it, as in C++. + // + // Example: + // struct Axis {}; + // struct Variant { Variant(const Axis& a); }; + // void take(Variant&&); + // + // Python + // a = gbl.Axis() + // gbl.take(a) # fine, as `a` is converted to temporary Variant + bool other_class = false; + if (pyobj && !pyobj->IsSmart()) { + interop::TCppScope_t cls = pyobj->ObjectIsA(); + other_class = !(cls && interop::IsSubclass(cls, fClass)); + } + if (!pyobj || (pyobj->fFlags & CPPInstance::kIsLValue) || other_class) { // implicit conversion is fine as the temporary by definition is moveable return (bool)ConvertImplicit(fClass, pyobject, para, ctxt); } diff --git a/bindings/pyroot/cppjit/test/test_boost.py b/bindings/pyroot/cppjit/test/test_boost.py index 79a43cae07e7e..157c066ffb448 100644 --- a/bindings/pyroot/cppjit/test/test_boost.py +++ b/bindings/pyroot/cppjit/test/test_boost.py @@ -58,12 +58,6 @@ def test01_any_class(self): run=False, reason="invalid reads in the JITed boost constructors under valgrind", ) - @mark.xfail( - reason="New Overload Resolution: a forwarding-reference template " - "operator= (any::operator=(ValueType&&)) fails to instantiate for " - "lvalue arguments, and the non-template move-assign fallback rightly " - "rejects an lvalue; pending" - ) def test02_any_usage(self): """boost::any assignment and casting""" diff --git a/bindings/pyroot/cppjit/test/test_overloads.py b/bindings/pyroot/cppjit/test/test_overloads.py index d939df580db5e..be22e182307b2 100644 --- a/bindings/pyroot/cppjit/test/test_overloads.py +++ b/bindings/pyroot/cppjit/test/test_overloads.py @@ -563,3 +563,22 @@ def test21_overload_by_signature_on_method_set(self): assert t.Branch.__overload__("double")(1.0) == 4 with raises(LookupError): t.Branch.__overload__("double, double, double, double") + + def test22_rvalue_reference_from_other_class(self): + """An object of another class binds to T&& through a temporary""" + + import cppjit + + cppjit.cppdef("""\ + namespace RvalueFromOther { + struct Axis { int n = 3; }; + struct Variant { Variant(const Axis& a) : n(a.n) {} int n; }; + int take(Variant&& v) { return v.n; } + }""") + + ns = cppjit.gbl.RvalueFromOther + a = ns.Axis() + assert ns.take(a) == 3 + v = cppjit.gbl.std.vector[ns.Variant]() + v.push_back(a) + assert len(v) == 1 and v[0].n == 3 From 3ea362201f88c1c285ed5907e2b392652d8c21d1 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 00:29:09 +0530 Subject: [PATCH 27/71] [cppjit] Return enum instances for data members of enum type Data members of enum type (static or not, e.g. `std::ios_base::ate`) were converted with the converter of the underlying integer type and thus returned as plain Python integers, while enum constants are instances of the Python enum class. Passed on to a function, a plain integer is spelled as `int` in the overload prototype and doesn't convert to the enum, so e.g. `std.ostringstream("(", std.ios_base.ate)` failed (and only worked in other environments through the argument-count fallback). Keep the enum type for data members, so that they get an EnumConverter, and let its FromMemory() return an instance of the enum class, which is looked up once in the enclosing scope. Fixes test_templates.py::test04_variadic_function in ROOT. Assisted-by: claude-code:claude-opus-5-5 --- .../pyroot/cppjit/src/cpyrt/CPPDataMember.cxx | 3 +- .../pyroot/cppjit/src/cpyrt/Converters.cxx | 44 ++++++++++++++++--- .../cppjit/src/cpyrt/DeclareConverters.h | 10 ++++- bindings/pyroot/cppjit/test/test_overloads.py | 27 ++++++++++++ 4 files changed, 75 insertions(+), 9 deletions(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPDataMember.cxx b/bindings/pyroot/cppjit/src/cpyrt/CPPDataMember.cxx index e737c18154c0b..8aef40d8d01af 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPDataMember.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPDataMember.cxx @@ -359,8 +359,7 @@ void cpyrt::CPPDataMember::Set(interop::TCppScope_t scope, type = interop::GetDatamemberType(fScope); fFullType = interop::GetTypeAsString(type); - // Get the integer type if it's an enum - if (interop::IsEnumType(type)) + if (interop::IsEnumType(type) && fFullType == "std::byte") type = interop::ResolveType(type); if (interop::IsConstVar(fScope)) diff --git a/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx b/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx index 1d2c47f0b3397..6eb5828043cda 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Converters.cxx @@ -1296,7 +1296,30 @@ bool cpyrt::ULLongConverter::ToMemory(PyObject* value, void* address, } //---------------------------------------------------------------------------- -cpyrt::EnumConverter::~EnumConverter() { DestroyConverter(fBase); } +cpyrt::EnumConverter::~EnumConverter() { + DestroyConverter(fBase); + Py_XDECREF(fEnumType); +} + +PyObject* cpyrt::EnumConverter::GetEnumType() { + // the Python class of the enum, an attribute of its enclosing scope + if (fEnumType) + return fEnumType; + + PyObject* pyparent = CreateScopeProxy(interop::GetParentScope(fEnumScope)); + if (pyparent) { + PyObject* pyenum = PyObject_GetAttrString( + pyparent, interop::GetFinalName(fEnumScope).c_str()); + if (pyenum && PyType_Check(pyenum) && + PyObject_HasAttr(pyenum, PyStrings::gUnderlying)) + fEnumType = pyenum; + else + Py_XDECREF(pyenum); + Py_DECREF(pyparent); + } + PyErr_Clear(); + return fEnumType; +} bool cpyrt::EnumConverter::SetArg(PyObject* pyobject, Parameter& para, CallContext* ctxt) { @@ -1324,7 +1347,19 @@ bool cpyrt::EnumConverter::SetArg(PyObject* pyobject, Parameter& para, } PyObject* cpyrt::EnumConverter::FromMemory(void* address) { - return fBase->FromMemory(address); + PyObject* pyval = fBase->FromMemory(address); + PyTypeObject* pyenum = (PyTypeObject*)GetEnumType(); + if (!pyval || !pyenum || !PyObject_TypeCheck(pyval, pyenum->tp_base)) + return pyval; + PyObject* args = PyTuple_Pack(1, pyval); + PyObject* result = pyenum->tp_base->tp_new(pyenum, args, nullptr); + Py_DECREF(args); + if (!result) { + PyErr_Clear(); + return pyval; + } + Py_DECREF(pyval); + return result; } bool cpyrt::EnumConverter::ToMemory(PyObject* value, void* address, @@ -3746,9 +3781,8 @@ cppjit::cpyrt::CreateConverter(interop::TCppType_t type, cdims_t dims) { h = gConvFactories.find( interop::GetTypeAsString(interop::ResolveType(resolvedType))); if (h != gConvFactories.end()) - return new EnumConverter( - (h->second)(dims), - interop::GetScopedFinalName(interop::GetScopeFromType(type))); + return new EnumConverter((h->second)(dims), + interop::GetScopeFromType(type)); } // a full, qualified matching converter is preferred diff --git a/bindings/pyroot/cppjit/src/cpyrt/DeclareConverters.h b/bindings/pyroot/cppjit/src/cpyrt/DeclareConverters.h index 34f45ec4f2310..39f594f6d0d32 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/DeclareConverters.h +++ b/bindings/pyroot/cppjit/src/cpyrt/DeclareConverters.h @@ -140,8 +140,9 @@ CPPJIT_DECLARE_REFCONVERTER(LDouble); // 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(Converter* base, interop::TCppScope_t enum_scope) + : fBase(base), fEnumScope(enum_scope), + fEnumName(interop::GetScopedFinalName(enum_scope)) {} ~EnumConverter() override; public: @@ -152,9 +153,14 @@ class EnumConverter : public Converter { bool HasState() override { return true; } std::string GetFailureMsg() override; +private: + PyObject* GetEnumType(); + private: Converter* fBase; + interop::TCppScope_t fEnumScope; std::string fEnumName; + PyObject* fEnumType = nullptr; }; class VoidConverter : public Converter { diff --git a/bindings/pyroot/cppjit/test/test_overloads.py b/bindings/pyroot/cppjit/test/test_overloads.py index be22e182307b2..12cb70a4e2c2c 100644 --- a/bindings/pyroot/cppjit/test/test_overloads.py +++ b/bindings/pyroot/cppjit/test/test_overloads.py @@ -582,3 +582,30 @@ def test22_rvalue_reference_from_other_class(self): v = cppjit.gbl.std.vector[ns.Variant]() v.push_back(a) assert len(v) == 1 and v[0].n == 3 + + def test23_enum_data_member_argument(self): + """Enum data members keep their type when passed on""" + + import cppjit + + cppjit.cppdef("""\ + namespace EnumDataMembers { + enum E { A = 1, B = 2 }; + enum class EC { X = 1, Y = 2 }; + struct S { static const E e = B; static constexpr EC ec = EC::Y; E m = A; }; + int f(E) { return 1; } + int f(const char*) { return 2; } + int g(EC) { return 3; } + int g(const char*) { return 4; } + }""") + + ns = cppjit.gbl.EnumDataMembers + assert type(ns.S.e) is ns.E and ns.S.e == 2 + assert type(ns.S().m) is ns.E and ns.S().m == 1 + assert ns.f(ns.S.e) == 1 + assert ns.f(ns.S().m) == 1 + assert ns.g(ns.S.ec) == 3 + + s = ns.S() + s.m = ns.B + assert s.m == 2 From 245e8023765ae3464055e46fc3903a0bd202feb4 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 00:30:44 +0530 Subject: [PATCH 28/71] [cppjit] Raise OverloadResolutionException when no template overload matches When the overload resolution of a TemplateProxy found no candidate for the arguments, it reported "Failed to instantiate" as a generic TypeError, whereas a CPPOverload raises an OverloadResolutionException in the same situation. Whether a method set goes through a TemplateProxy depends on the presence of templated overloads, e.g. the `std::vector` constructors of the standard library in use, so the exception type differed between environments (test_stltypes.py::test16_vector_construction failed with ROOT's C++20 standard library). Raise an OverloadResolutionException (a TypeError) instead. Assisted-by: claude-code:claude-opus-5-5 --- bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx index a89323655d6b6..81d1329d00a80 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx @@ -371,7 +371,7 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, return pymeth; } - PyErr_Format(PyExc_TypeError, "Failed to instantiate \"%s(%s)\"", + PyErr_Format(gOverloadResolutionException, "Failed to instantiate \"%s(%s)\"", fname.c_str(), proto.c_str()); return nullptr; } From eef84508018b356a685d56bb0729bd1c8e9a44ac Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 00:45:52 +0530 Subject: [PATCH 29/71] [cppjit] Don't leave an error set when a vector iterator has no data pointer `vector_iter` asks `__real_data()` for the data pointer to iterate quickly and indexes the vector if there is none. `__real_data()` can fail, though: for `std::vector`, `data()` returns a `char*`, which is converted to a Python str and fails if the bytes up to the next null (the data isn't null-terminated) aren't valid UTF-8. That error was left set, so iterating such a vector randomly failed with "returned a result with an exception set". Clear the error when no data pointer could be obtained. Assisted-by: claude-code:claude-opus-5-5 --- bindings/pyroot/cppjit/src/cpyrt/Pythonize.cxx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bindings/pyroot/cppjit/src/cpyrt/Pythonize.cxx b/bindings/pyroot/cppjit/src/cpyrt/Pythonize.cxx index 02ad193ae39a8..13badcd92c2e9 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Pythonize.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Pythonize.cxx @@ -761,6 +761,13 @@ static PyObject* vector_iter(PyObject* v) { ? ((CPPInstance*)pydata)->GetObjectRaw() : nullptr; Py_XDECREF(pydata); + // __real_data() may fail: e.g. for std::vector, data() returns a + // char*, which is converted to a Python str and so fails if the bytes up + // to the next null (the data isn't null-terminated) aren't valid UTF-8. + // Without a data pointer, the iterator indexes the vector instead, so + // don't leave the error set. + if (!vi->vi_data) + PyErr_Clear(); } else { PyErr_Clear(); From cd61cbbb06dabadd6a2fe69a0e766bf80c3dd5d8 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 00:50:00 +0530 Subject: [PATCH 30/71] [cppjit] Fail GetMethodTemplate if the argument types can't be resolved `GetMethodTemplate` ignored the result of `AppendTypesSlow`. If the argument types could not be resolved, the overload was selected with fewer (or no) argument types. If an explicit template argument could not be resolved, it was replaced by `__cppjit_internal::UnknownType`, and the template was instantiated with it. E.g. `entry._GetPtr["AtlasLikeDataVector"]` in ROOT's REntry pythonization, called before the dictionary of that class was loaded, instantiated `REntry::GetPtr`. That threw a type mismatch at run time instead of failing the lookup, so the pythonization's fallback to the alternative class names never kicked in. Fail the lookup if `AppendTypesSlow` fails for the argument types or the explicit template arguments, and don't replace unknown explicit template arguments. (An unknown type among the argument types is still spelled as UnknownType, which just makes the candidates non-viable.) The other callers of `AppendTypesSlow` already check its result. Assisted-by: claude-code:claude-opus-5-5 --- .../cppjit/src/interop/interop_wrapper.cxx | 9 +++++++-- bindings/pyroot/cppjit/test/test_overloads.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx index b381c2eceeb0b..1c9fced0f2205 100644 --- a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx +++ b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx @@ -2142,8 +2142,13 @@ interop::TCppMethod_t ResolveMethodTemplate( // cpyrt assumes that we attempt instantiation here std::vector arg_types; std::vector templ_params; - interop::AppendTypesSlow(proto, arg_types, scope, true); - interop::AppendTypesSlow(explicit_params, templ_params, scope, true); + if (!proto.empty() && interop::AppendTypesSlow(proto, arg_types, scope, + /*append_unknown=*/true)) + return nullptr; + if (!explicit_params.empty() && + interop::AppendTypesSlow(explicit_params, templ_params, scope, + /*append_unknown=*/false)) + return nullptr; std::vector candidate_methods; for (auto fn : candidates) diff --git a/bindings/pyroot/cppjit/test/test_overloads.py b/bindings/pyroot/cppjit/test/test_overloads.py index 12cb70a4e2c2c..5a85ececda2e8 100644 --- a/bindings/pyroot/cppjit/test/test_overloads.py +++ b/bindings/pyroot/cppjit/test/test_overloads.py @@ -609,3 +609,20 @@ def test23_enum_data_member_argument(self): s = ns.S() s.m = ns.B assert s.m == 2 + + def test24_unknown_explicit_template_argument(self): + """An unresolvable explicit template argument is an error""" + + import cppjit + + cppjit.cppdef("""\ + namespace UnknownTemplateArg { + struct Entry { + template int size(int) { return (int)sizeof(T); } + }; + }""") + + e = cppjit.gbl.UnknownTemplateArg.Entry() + assert e.size["double"](1) == 8 + with raises(TypeError): + e.size["NoSuchType"](1) From d829d6a2e379a886a69a78d03667a8a01a1fe037 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 01:12:43 +0530 Subject: [PATCH 31/71] [cppjit] Match keyword arguments to parameters by name in overload resolution The overload resolution ignored the names of keyword arguments. `TemplateProxy::Instantiate` built the prototype from the positional arguments only, so a method set with templated overloads could select a candidate with too few parameters, which then failed when called with the keywords ("takes at most 3 arguments (6 given)"), e.g. `df.Display(columnList=("x", "y"), nRows=15)` or `xRooNode("RooWorkspace", name=..., title=...)`. `CPPOverload` appended the keyword values in the order given, so `f(1, c="x")` was resolved as `f(1, "x")`, and keywords given in another order than the parameters selected the wrong overload or none. The call itself (`CPPMethod::ProcessKwds`) does map the keywords by name, as cppyy did. Pass the keyword names along with the prototype, whose last arguments are the keyword values, to `BestOverloadFunctionMatch` and `GetMethodTemplate`. Each candidate then gets its own argument list: the positional arguments, then the keyword values at the positions of their parameters, with skipped defaulted parameters in between taking an argument of their own type. Candidates that lack a name, would get an argument twice, or would have to skip a required parameter (or one whose type depends on the template parameters) are not viable. Candidates with the same argument list are resolved together; among the best candidates of the different lists, the one that skips the fewest parameters wins. The fallback to a candidate by argument count is only used if no list has a match. If the names fit no candidate at all, the resolution falls back to the keyword values in the order given, so that the call reports the unexpected (or repeated) keyword argument as before. Assisted-by: claude-code:claude-opus-5-5 --- .../pyroot/cppjit/src/cpyrt/CPPOverload.cxx | 19 +- .../pyroot/cppjit/src/cpyrt/TemplateProxy.cxx | 29 +-- .../pyroot/cppjit/src/cpyrt/TemplateProxy.h | 4 +- .../cppjit/src/interop/cppjit_interop.h | 8 +- .../cppjit/src/interop/interop_wrapper.cxx | 169 ++++++++++++++++-- bindings/pyroot/cppjit/test/test_overloads.py | 47 +++++ 6 files changed, 242 insertions(+), 34 deletions(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx index 370a88fc26333..5cc7a6d0c7eed 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx @@ -785,17 +785,22 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, proto += ", "; PyObject* obj = cpyrt_PyArgs_GET_ITEM(args, i + nArgs); PyObject* typ = (PyObject*)Py_TYPE(obj); - if (!AddTypeName(proto, typ, obj, Utility::kNone)) { + if (!AddTypeName(proto, typ, obj, Utility::kNone)) proto += "__cppjit_internal::UnknownType"; - break; // ???: do we need to raise error here - } } } } + // the values of keyword arguments, at the end of proto, are matched to the + // parameters by name + std::vector kw_names; + for (Py_ssize_t i = 0; kwds && i < PyTuple_GET_SIZE(kwds); i++) + kw_names.push_back(PyUnicode_AsUTF8(PyTuple_GET_ITEM(kwds, i))); + std::vector ambiguous_candidates; interop::TCppMethod_t meth = interop::BestOverloadFunctionMatch( - overloads, proto, ambiguous_candidates, /*TODO:*/ nullptr, is_operator); + overloads, proto, ambiguous_candidates, /*TODO:*/ nullptr, is_operator, + kw_names); if (!meth && (ctxt.fFlags & CallContext::kIsConstructor)) { // this might be a POD class/struct // and might need the self* for the overload resolution @@ -809,9 +814,9 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, PyObject* typ = (PyObject*)Py_TYPE(obj); AddTypeName(self_type_name, typ, nullptr, Utility::kNone); proto = self_type_name + "**, " + proto; - meth = interop::BestOverloadFunctionMatch(overloads, proto, - ambiguous_candidates, - /*TODO:*/ nullptr, is_operator); + meth = interop::BestOverloadFunctionMatch( + overloads, proto, ambiguous_candidates, /*TODO:*/ nullptr, + is_operator, kw_names); } } // Subscript tie-break: cppjit maps both operator[] and operator() into the diff --git a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx index 81d1329d00a80..b13406c699e43 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx @@ -61,7 +61,8 @@ void TemplateProxy::AdoptMethod(PyCallable* pc) { PyObject* TemplateProxy::Instantiate(const std::string& fname, cpyrt_PyArgs_t args, size_t nargsf, Utility::ArgPreference pref, int* pcnt, - bool include_non_templated) { + bool include_non_templated, + PyObject* kwnames) { // Instantiate (and cache) templated methods, return method if any std::string proto = ""; std::string protoPair = ""; @@ -75,7 +76,10 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, (void)addedSelf; } - Py_ssize_t argc = cpyrt_PyArgs_GET_SIZE(args, nargsf); + // the values of keyword arguments follow the positional ones; they are + // matched to the parameters by name in the overload resolution + Py_ssize_t argc = cpyrt_PyArgs_GET_SIZE(args, nargsf) + + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0); if (argc != 0) { PyObject* tpArgs = PyTuple_New(skipSelf ? argc - 1 : argc); for (Py_ssize_t i = skipSelf ? 1 : 0, j = 0; i < argc; ++i, ++j) { @@ -231,15 +235,19 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, // the following causes instantiation as necessary interop::TCppScope_t scope = ((CPPClass*)fTI->fPyClass)->fCppType; + std::vector kw_names; + for (Py_ssize_t i = 0; kwnames && i < PyTuple_GET_SIZE(kwnames); i++) + kw_names.push_back(PyUnicode_AsUTF8(PyTuple_GET_ITEM(kwnames, i))); std::vector ambiguous_candidates; - interop::TCppMethod_t cppmeth = interop::GetMethodTemplate( - scope, fname, proto, ambiguous_candidates, include_non_templated); + interop::TCppMethod_t cppmeth = + interop::GetMethodTemplate(scope, fname, proto, ambiguous_candidates, + include_non_templated, kw_names); if (!cppmeth && !protoPair.empty() && protoPair != proto) { // retry with 2-tuples typed as std::pair instead of std::initializer_list ambiguous_candidates.clear(); cppmeth = interop::GetMethodTemplate(scope, fname, protoPair, ambiguous_candidates, - include_non_templated); + include_non_templated, kw_names); if (cppmeth) proto = protoPair; } @@ -270,8 +278,9 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, pos = proto.find("initializer_list", pos + 6); } - interop::TCppMethod_t m2 = interop::GetMethodTemplate( - scope, fname, proto, ambiguous_candidates, include_non_templated); + interop::TCppMethod_t m2 = + interop::GetMethodTemplate(scope, fname, proto, ambiguous_candidates, + include_non_templated, kw_names); if (m2 && m2 != cppmeth) { // replace if the new method with vector was found; otherwise just // continue with the previously found method with initializer_list. @@ -630,8 +639,8 @@ static PyObject* tpp_vectorcall(TemplateProxy* pytmpl, PyObject* const* args, PyErr_Clear(); // not cached or failed call; try instantiation - pymeth = pytmpl->Instantiate(PyUnicode_AsUTF8(pyfullname), args, - nargsf, Utility::kNone, nullptr, false); + pymeth = pytmpl->Instantiate(PyUnicode_AsUTF8(pyfullname), args, nargsf, + Utility::kNone, nullptr, false, kwds); if (pymeth) { // attempt actual call; same as above, allow implicit conversion of // arguments @@ -659,7 +668,7 @@ static PyObject* tpp_vectorcall(TemplateProxy* pytmpl, PyObject* const* args, // case 2: create a template instantiation int pcnt = 0; // FIXME: this is unused and can be removed pymeth = pytmpl->Instantiate(pytmpl->fTI->fCppName, args, nargsf, - Utility::kReference, &pcnt, true); + Utility::kReference, &pcnt, true, kwds); if (pymeth) { // attempt actual call; even if argument based, allow implicit conversions, // for example for non-template arguments diff --git a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.h b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.h index d2a926959594d..48f8773540302 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.h +++ b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.h @@ -63,8 +63,8 @@ class TemplateProxy { void AdoptMethod(PyCallable* pc); PyObject* Instantiate(const std::string& fname, cpyrt_PyArgs_t tmplArgs, size_t nargsf, Utility::ArgPreference, - int* pcnt = nullptr, - bool include_non_templated = false); + int* pcnt = nullptr, bool include_non_templated = false, + PyObject* kwnames = nullptr); private: // private, as the python C-API will handle creation TemplateProxy() = delete; diff --git a/bindings/pyroot/cppjit/src/interop/cppjit_interop.h b/bindings/pyroot/cppjit/src/interop/cppjit_interop.h index 150f4ac351ab8..d1d375708b22c 100644 --- a/bindings/pyroot/cppjit/src/interop/cppjit_interop.h +++ b/bindings/pyroot/cppjit/src/interop/cppjit_interop.h @@ -325,10 +325,13 @@ bool IsTemplatedMethod(TCppMethod_t method); RPY_EXPORTED bool IsStaticTemplate(TCppScope_t scope, const std::string& name); RPY_EXPORTED +// The arguments in proto end with the values of the keyword arguments named +// kw_names (if any), which are matched to the parameters by name. TCppMethod_t GetMethodTemplate(TCppScope_t scope, const std::string& name, const std::string& proto, std::vector& ambiguous_candidates, - bool include_non_templated = false); + bool include_non_templated = false, + const std::vector& kw_names = {}); // Select the overload of the (templated) method set name of scope that takes // the parameters in the comma-separated signature sig (without the object of // non-static methods), instantiating it as necessary. @@ -344,7 +347,8 @@ RPY_EXPORTED TCppMethod_t BestOverloadFunctionMatch( const std::vector& candidates, const std::string& proto, std::vector& ambiguous_candidates, - TCppScope_t parent_scope = nullptr, bool is_operator = false); + TCppScope_t parent_scope = nullptr, bool is_operator = false, + const std::vector& kw_names = {}); RPY_EXPORTED bool IsOperator(TCppScope_t scope); RPY_EXPORTED diff --git a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx index 1c9fced0f2205..d314c6180af7b 100644 --- a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx +++ b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx @@ -1888,7 +1888,8 @@ interop::TCppMethod_t BestOverloadFunctionMatch( const std::vector& arg_types, const std::vector& templ_params, std::vector& ambiguous_candidates, - interop::TCppScope_t parent_scope, bool is_operator) { + interop::TCppScope_t parent_scope, bool is_operator, + bool count_fallback = true) { std::lock_guard Lock(InterOpMutex); auto remove_duplicate_methods = [](std::vector& methods) { @@ -2088,13 +2089,152 @@ interop::TCppMethod_t BestOverloadFunctionMatch( // proper argument type match failed // trying improper argument length match - if (!cppmeth && !potential_cppmeth_ambiguous && + if (!cppmeth && count_fallback && !potential_cppmeth_ambiguous && !Cpp::IsTemplatedFunction(potential_cppmeth)) cppmeth = potential_cppmeth; if (!cppmeth) remove_duplicate_methods(ambiguous_candidates); return cppmeth; } + +// Overload resolution for a call with keyword arguments, whose values are the +// last kw_names.size() entries of arg_types. As in the call itself +// (CPPMethod::ProcessKwds), the keyword arguments are matched to the +// parameters by name, so each candidate gets its own argument list: the +// positional arguments, then the keyword values at the positions of their +// parameters, with the skipped (defaulted) parameters in between taking an +// argument of their own type. Candidates that lack one of the names, would get +// an argument twice, or would skip a required parameter (or one of a dependent +// type) are not viable. The candidates with the same argument list are +// resolved together; of the best candidates of the different lists, the one +// that skips the fewest parameters wins (it is ambiguous on a tie). Only if no +// list has a match, the fallback to a candidate by argument count is used. If +// the names fit no candidate, resolve with the keyword values in the order +// given, so that the call reports the unexpected or repeated keyword. +interop::TCppMethod_t BestOverloadFunctionMatch( + const std::vector& candidates, + const std::vector& arg_types, + const std::vector& kw_names, + const std::vector& templ_params, + std::vector& ambiguous_candidates, + interop::TCppScope_t parent_scope, bool is_operator) { + if (kw_names.empty()) + return BestOverloadFunctionMatch(candidates, arg_types, templ_params, + ambiguous_candidates, parent_scope, + is_operator); + if (arg_types.size() < kw_names.size()) + return nullptr; + + std::lock_guard Lock(InterOpMutex); + + struct ArgList { + std::vector fArgs; + size_t fSkipped; + std::vector fCandidates; + }; + auto same_args = [](const std::vector& a, + const std::vector& b) { + return std::equal( + a.begin(), a.end(), b.begin(), b.end(), + [](const Cpp::TemplateArgInfo& x, const Cpp::TemplateArgInfo& y) { + return x.m_Type == y.m_Type && x.m_IntegralValue == y.m_IntegralValue; + }); + }; + + const size_t npos = arg_types.size() - kw_names.size(); + std::vector arg_lists; + for (auto fn : candidates) { + // the object of a method call is the first argument + const size_t self = + interop::IsNonStaticMethod(fn) && !interop::IsConstructor(fn) ? 1 : 0; + const size_t nparams = Cpp::GetFunctionNumArgs(fn); + if (npos < self || npos > nparams + self) + continue; + + std::vector slots(nparams + self, nullptr); + for (size_t i = 0; i < npos; ++i) + slots[i] = &arg_types[i]; + size_t nargs = npos; + bool viable = true; + for (size_t k = 0; viable && k < kw_names.size(); ++k) { + size_t p = 0; + while (p < nparams && Cpp::GetFunctionArgName(fn, p) != kw_names[k]) + ++p; + viable = p < nparams && !slots[p + self]; + if (viable) { + slots[p + self] = &arg_types[npos + k]; + nargs = std::max(nargs, p + self + 1); + } + } + + ArgList list{{}, 0, {fn}}; + const size_t nrequired = Cpp::GetFunctionRequiredArgs(fn); + for (size_t i = 0; viable && i < nargs; ++i) { + if (slots[i]) { + list.fArgs.push_back(*slots[i]); + continue; + } + // a skipped parameter takes its default; spell it as its own type + interop::TCppType_t type = nullptr; + if (i - self >= nrequired) { + type = Cpp::GetFunctionArgType(fn, i - self); + // the parameter type of a template may depend on its parameters + if (type && Cpp::IsTemplatedFunction(fn)) + type = interop::GetType(interop::GetTypeAsString(type)); + } + viable = (bool)type; + list.fArgs.emplace_back(type.data); + list.fSkipped += 1; + } + if (!viable) + continue; + + auto same = std::find_if(arg_lists.begin(), arg_lists.end(), + [&](const ArgList& other) { + return same_args(other.fArgs, list.fArgs); + }); + if (same != arg_lists.end()) + same->fCandidates.push_back(fn); + else + arg_lists.push_back(std::move(list)); + } + + if (arg_lists.empty()) + return BestOverloadFunctionMatch(candidates, arg_types, templ_params, + ambiguous_candidates, parent_scope, + is_operator); + + interop::TCppMethod_t best = nullptr; + std::vector best_tied, not_resolved; + for (bool count_fallback : {false, true}) { + size_t best_skipped = 0; + for (const ArgList& list : arg_lists) { + std::vector ambiguous; + interop::TCppMethod_t method = BestOverloadFunctionMatch( + list.fCandidates, list.fArgs, templ_params, ambiguous, parent_scope, + is_operator, count_fallback); + if (!method) { + if (count_fallback) + not_resolved.insert(not_resolved.end(), ambiguous.begin(), + ambiguous.end()); + } else if (!best || list.fSkipped < best_skipped) { + best = method; + best_skipped = list.fSkipped; + best_tied = {method}; + } else if (list.fSkipped == best_skipped && method != best) { + best_tied.push_back(method); + } + } + if (best) + break; + } + if (best && best_tied.size() == 1) + return best; + const auto& ambiguous = best ? best_tied : not_resolved; + ambiguous_candidates.insert(ambiguous_candidates.end(), ambiguous.begin(), + ambiguous.end()); + return nullptr; +} } // namespace namespace { @@ -2132,11 +2272,13 @@ GetMethodCandidates(interop::TCppScope_t scope, const std::string& name, // Select (instantiating as necessary) the best of the candidates for the // argument types in proto and the explicit template arguments. -interop::TCppMethod_t ResolveMethodTemplate( - interop::TCppScope_t scope, - const std::vector& candidates, - const std::string& proto, const std::string& explicit_params, - std::vector& ambiguous_candidates) { +interop::TCppMethod_t +ResolveMethodTemplate(interop::TCppScope_t scope, + const std::vector& candidates, + const std::string& proto, + const std::string& explicit_params, + std::vector& ambiguous_candidates, + const std::vector& kw_names = {}) { std::lock_guard Lock(InterOpMutex); // cpyrt assumes that we attempt instantiation here @@ -2156,8 +2298,8 @@ interop::TCppMethod_t ResolveMethodTemplate( candidate_methods.push_back(fn); interop::TCppMethod_t cppmeth = ::BestOverloadFunctionMatch( - candidate_methods, arg_types, templ_params, ambiguous_candidates, scope, - /*is_operator=*/false); + candidate_methods, arg_types, kw_names, templ_params, + ambiguous_candidates, scope, /*is_operator=*/false); if (cppmeth) return cppmeth; @@ -2176,12 +2318,12 @@ interop::TCppMethod_t ResolveMethodTemplate( interop::TCppMethod_t interop::GetMethodTemplate( TCppScope_t scope, const std::string& name, const std::string& proto, std::vector& ambiguous_candidates, - bool include_non_templated) { + bool include_non_templated, const std::vector& kw_names) { auto [pure_name, explicit_params] = SplitTemplateName(name); std::vector candidates = GetMethodCandidates( scope, pure_name, include_non_templated && explicit_params.empty()); return ResolveMethodTemplate(scope, candidates, proto, explicit_params, - ambiguous_candidates); + ambiguous_candidates, kw_names); } interop::TCppMethod_t interop::GetMethodBySignature( @@ -2227,7 +2369,8 @@ interop::TCppMethod_t interop::BestOverloadFunctionMatch( const std::vector& candidates, const std::string& proto, std::vector& ambiguous_candidates, - TCppScope_t parent_scope, bool is_operator) { + TCppScope_t parent_scope, bool is_operator, + const std::vector& kw_names) { std::lock_guard Lock(InterOpMutex); std::vector arg_types; if (!proto.empty() && @@ -2237,7 +2380,7 @@ interop::TCppMethod_t interop::BestOverloadFunctionMatch( for (auto fn : candidates) if (!interop::IsDeletedMethod(fn)) non_deleted.push_back(fn); - return ::BestOverloadFunctionMatch(non_deleted, arg_types, {}, + return ::BestOverloadFunctionMatch(non_deleted, arg_types, kw_names, {}, ambiguous_candidates, parent_scope, is_operator); } diff --git a/bindings/pyroot/cppjit/test/test_overloads.py b/bindings/pyroot/cppjit/test/test_overloads.py index 5a85ececda2e8..19832d5b4bf28 100644 --- a/bindings/pyroot/cppjit/test/test_overloads.py +++ b/bindings/pyroot/cppjit/test/test_overloads.py @@ -626,3 +626,50 @@ def test24_unknown_explicit_template_argument(self): assert e.size["double"](1) == 8 with raises(TypeError): e.size["NoSuchType"](1) + + def test25_keyword_arguments(self): + """Keyword arguments are matched to the parameters by name""" + + import cppjit + + cppjit.cppdef("""\ + namespace KwArgs { + int f(int a, int b = 5, int c = 7) { return 100*a + 10*b + c; } + int f(const std::string&) { return -1; } + int g(int x) { return 1; } + int g(double y) { return 2; } + int h(int a, double b = 0., const char* c = "") { return 1; } + int h(int a, const char* c) { return 2; } + struct S { + template int g(const char*, T* add, void* p = nullptr) { return 1; } + int g(const char*, void* add, void* p, int x, bool y) { return 2; } + template int t(T a, int b = 1, int c = 2) { return 10*b + c; } + int t(const std::string&, int c) { return -1; } + }; + }""") + + ns = cppjit.gbl.KwArgs + # in and out of order, and skipping a defaulted parameter + assert ns.f(1, b=2, c=3) == 123 + assert ns.f(1, c=3, b=2) == 123 + assert ns.f(1, c=3) == 153 + assert ns.f(c=3, a=1) == 153 + # the name selects the overload + assert ns.g(x=1) == 1 + assert ns.g(y=1) == 2 + # the overload that skips fewer defaulted parameters wins + assert ns.h(1, c="x") == 2 + assert ns.h(1, b=2.0, c="x") == 1 + # unknown names and arguments given twice + with raises(TypeError, match="unexpected keyword argument"): + ns.f(1, d=3) + with raises(TypeError, match="multiple values"): + ns.f(1, a=3) + + # sets of templated and non-templated methods + s = ns.S() + assert s.g("n", cppjit.nullptr, p=cppjit.nullptr, x=1, y=True) == 2 + assert s.g("n", cppjit.nullptr, y=True, x=1, p=cppjit.nullptr) == 2 + assert s.t(1.5, c=3) == 13 + assert s.t(1.5, c=3, b=2) == 23 + assert s.t("a", c=3) == -1 From b1f43d7c97ddd9a1cfbcc17afbd117f0eca44e83 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 01:52:11 +0530 Subject: [PATCH 32/71] [cppjit] Honour the silent flag of interop::Compile again `interop::Compile` ignored its `silent` argument and always declared the code non-silently. Code that is compiled tentatively and may legitimately fail, like the aggregate-initialization pseudo-constructors generated in Pythonize (e.g. for unions or classes with enum constants), then went through the interpreter's regular error path: besides printing compiler errors, with ROOT/cling this unloads the failed transaction, and ROOT's unloading callbacks (TClass::GetListOfEnums -> TListOfEnums::Load) start a nested transaction, which aborts with assertions enabled ("Must not nest within unloading transaction"). The CI runs with assertions, where test_datatypes, test_lowlevel, test_templates, test_regression and test_fragile crashed. Pass the silent flag on to Cpp::Declare again. Assisted-by: claude-code:claude-opus-5-5 --- bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx index d314c6180af7b..51af7d29b18bd 100644 --- a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx +++ b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx @@ -307,7 +307,7 @@ static inline char* cppstring_to_cstring(const std::string& cppstr) { bool interop::Compile(const std::string& code, bool silent) { std::lock_guard Lock(InterOpMutex); // Declare returns an enum which equals 0 on success - return !Cpp::Declare(code.c_str(), false); + return !Cpp::Declare(code.c_str(), silent); } void interop::LockInterpreter() { InterOpMutex.lock(); } From 1434944947199977b1ae3336411e16c0e0e16afb Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Tue, 20 Oct 2026 20:13:06 +0530 Subject: [PATCH 33/71] [cppjit] Update tests tags --- bindings/pyroot/cppjit/test/test_eigen.py | 12 ++++++------ bindings/pyroot/cppjit/test/test_fragile.py | 2 +- bindings/pyroot/cppjit/test/test_leakcheck.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bindings/pyroot/cppjit/test/test_eigen.py b/bindings/pyroot/cppjit/test/test_eigen.py index fc896deafd0d5..45a3b9eec77d6 100644 --- a/bindings/pyroot/cppjit/test/test_eigen.py +++ b/bindings/pyroot/cppjit/test/test_eigen.py @@ -28,12 +28,12 @@ def setup_class(cls): warnings.simplefilter("ignore") cppjit.include("Eigen/Dense") - @mark.xfail( - reason="New Overload Resolution: operators a using-declaration imports " - "(Eigen brings the DenseBase subscript operators into Matrix that way) " - "are enumerated as using-shadows since CppInterOp #995, and " - "Cpp::BestOverloadFunctionMatch skips shadow candidates; pending" - ) + # @mark.xfail( + # reason="New Overload Resolution: operators a using-declaration imports " + # "(Eigen brings the DenseBase subscript operators into Matrix that way) " + # "are enumerated as using-shadows since CppInterOp #995, and " + # "Cpp::BestOverloadFunctionMatch skips shadow candidates; pending" + # ) def test01_simple_matrix_and_vector(self): """Basic creation of an Eigen::Matrix and Eigen::Vector""" diff --git a/bindings/pyroot/cppjit/test/test_fragile.py b/bindings/pyroot/cppjit/test/test_fragile.py index e82c9bd654e01..b48f35e2dce74 100644 --- a/bindings/pyroot/cppjit/test/test_fragile.py +++ b/bindings/pyroot/cppjit/test/test_fragile.py @@ -656,7 +656,7 @@ def test25_cppdef_error_reporting(self): int add42(int i) { return i + 42; } }""") - @mark.xfail(condition=IS_CLANG_REPL, reason="Fails with ClangRepl (passes with LLVM 23)") + # @mark.xfail(condition=IS_CLANG_REPL, reason="Fails with ClangRepl (passes with LLVM 23)") @mark.xfail( run=False, reason="rolling back a failed declaration asserts in ROOT's unload " diff --git a/bindings/pyroot/cppjit/test/test_leakcheck.py b/bindings/pyroot/cppjit/test/test_leakcheck.py index 4e77c3adde50a..9d735f385eb5d 100644 --- a/bindings/pyroot/cppjit/test/test_leakcheck.py +++ b/bindings/pyroot/cppjit/test/test_leakcheck.py @@ -10,7 +10,7 @@ nopsutil = True -@mark.skip(reason="disabled due to its sporadic nature, especially fragile on VMs") +# @mark.skip(reason="disabled due to its sporadic nature, especially fragile on VMs") class TestLEAKCHECK: def setup_class(cls): import psutil From 5f3178550c8a8a4585b5b35fad2c55c7f7660eb1 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Wed, 21 Oct 2026 19:34:26 +0530 Subject: [PATCH 34/71] [cppjit] Spell a pointer proxy as an lvalue when it is the object of a call A proxy that refers to a pointer (e.g. a pointer data member, or a `T*&` return value) is spelled `T*` in the overload prototype. As the object of a method call, `T*` made Clang's `TryObjectArgumentInitialization` assert (`FromClassification.isLValue()`) in builds with assertions, as an object argument has to be of class type; the object of `ptr->f()` is `*ptr`, an lvalue. The caller has to pass the object as C++ sees it, so generalise the `smart_as_pointee` option of `Utility::AddTypeName` into `as_object`: spell the object of a call as an lvalue of its class, both for smart pointer proxies (as the pointee) and for proxies referring to a pointer. This replaces the CppInterOp change that treated pointers to classes as lvalue objects in `BestOverloadFunctionMatch`. Assisted-by: claude-code:claude-opus-5-5 --- .../pyroot/cppjit/src/cpyrt/CPPOverload.cxx | 4 ++-- .../pyroot/cppjit/src/cpyrt/TemplateProxy.cxx | 2 +- bindings/pyroot/cppjit/src/cpyrt/Utility.cxx | 8 ++++--- bindings/pyroot/cppjit/src/cpyrt/Utility.h | 8 ++++--- bindings/pyroot/cppjit/test/test_overloads.py | 22 +++++++++++++++++++ 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx index 5cc7a6d0c7eed..5285d14ba5301 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx @@ -623,7 +623,7 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, // __smartptr__()), so the object is the pointee [[maybe_unused]] auto res = AddTypeName(proto, (PyObject*)Py_TYPE(self), self, Utility::kNone, - nullptr, /*smart_as_pointee=*/true); + nullptr, /*as_object=*/true); assert(res); } // in an unbound call of non-static methods, the first argument is the object @@ -771,7 +771,7 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, proto += ", "; if (i == 0 && self_is_first_arg) { AddTypeName(proto, typ, obj, Utility::kNone, nullptr, - /*smart_as_pointee=*/true); + /*as_object=*/true); continue; } if (!AddTypeName(proto, typ, obj, Utility::kNone)) { diff --git a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx index b13406c699e43..d49d3d44f034e 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx @@ -71,7 +71,7 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, PyObject* self = (PyObject*)fSelf; bool addedSelf = Utility::AddTypeName(proto, (PyObject*)Py_TYPE(self), self, Utility::kNone, nullptr, - /*smart_as_pointee=*/true); + /*as_object=*/true); assert(addedSelf); (void)addedSelf; } diff --git a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx index 3bfe217681918..a0ecea4975a4a 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Utility.cxx @@ -712,7 +712,7 @@ bool AddNumpyTypeName(std::string& name, PyObject* tn, PyObject* arg) { bool cpyrt::Utility::AddTypeName(std::string& tmpl_name, PyObject* tn, PyObject* arg, cpyrt::Utility::ArgPreference pref, int* pcnt, - bool smart_as_pointee) { + bool as_object) { // Determine the appropriate C++ type for a given Python type; this is a // helper because it can recurse if the type is list or tuple and needs // matching on std::vector. @@ -891,8 +891,10 @@ bool cpyrt::Utility::AddTypeName(std::string& tmpl_name, PyObject* tn, if (arg) { // try to specialize the type match for the given object CPPInstance* pyobj = (CPPInstance*)arg; - if (CPPInstance_Check(pyobj) && smart_as_pointee && pyobj->IsSmart()) { - // the Python type of a smart pointer proxy is the pointee class + if (CPPInstance_Check(pyobj) && as_object && + (pyobj->IsSmart() || (pyobj->fFlags & CPPInstance::kIsReference))) { + // the Python type of a smart pointer or pointer proxy is the pointee + // class, and the object of a call through either is an lvalue tmpl_name.append( full_scope(interop::GetScopedFinalName(((CPPClass*)tn)->fCppType))); tmpl_name.push_back('&'); diff --git a/bindings/pyroot/cppjit/src/cpyrt/Utility.h b/bindings/pyroot/cppjit/src/cpyrt/Utility.h index 9dfed05ca7654..9acea7005c0b4 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Utility.h +++ b/bindings/pyroot/cppjit/src/cpyrt/Utility.h @@ -38,11 +38,13 @@ PyCallable* FindBinaryOperator( // helper for template classes and methods enum ArgPreference { kNone, kPointer, kReference, kValue }; std::string GetPyCallableAnnotation(PyObject* pyobj); -// smart_as_pointee: spell a smart pointer proxy as (an lvalue of) the object it -// points to rather than as the smart pointer +// as_object: spell arg as the object of a method call, which is an lvalue of +// its class: a smart pointer proxy as the object it points to rather than as +// the smart pointer, and a pointer proxy (e.g. as returned by a function +// returning a pointer) as the object it points to, as for ptr->f() bool AddTypeName(std::string& tmpl_name, PyObject* tn, PyObject* arg, ArgPreference pref, int* pcnt = nullptr, - bool smart_as_pointee = false); + bool as_object = false); std::string ConstructTemplateArgs(PyObject* pyname, PyObject* tpArgs, PyObject* args = nullptr, ArgPreference = kNone, int argoff = 0, diff --git a/bindings/pyroot/cppjit/test/test_overloads.py b/bindings/pyroot/cppjit/test/test_overloads.py index 19832d5b4bf28..eba4398c30bb0 100644 --- a/bindings/pyroot/cppjit/test/test_overloads.py +++ b/bindings/pyroot/cppjit/test/test_overloads.py @@ -673,3 +673,25 @@ def test25_keyword_arguments(self): assert s.t(1.5, c=3) == 13 assert s.t(1.5, c=3, b=2) == 23 assert s.t("a", c=3) == -1 + + def test26_method_call_on_pointer_member(self): + """Overloaded methods called through a pointer data member""" + + import cppjit + + cppjit.cppdef("""\ + namespace PtrObject { + struct S { + int f(int) { return 1; } + int f(double) { return 2; } + }; + struct Holder { S* fPtr = new S{}; }; + }""") + + ns = cppjit.gbl.PtrObject + h = ns.Holder() + # the proxy of the data member refers to the pointer; the object of the + # call is the S it points to, an lvalue + assert h.fPtr.f(1) == 1 + assert h.fPtr.f(1.5) == 2 + assert ns.S.f(h.fPtr, 1) == 1 From 123f44629cc4aebcb8b12d9f916efef31e6985a2 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Wed, 21 Oct 2026 19:35:29 +0530 Subject: [PATCH 35/71] [cppjit] Spell the object of unbound templated method calls as an object Python calls a method through the method-descriptor protocol, i.e. it calls the class's TemplateProxy unbound, with the object as the first argument (e.g. `p.__assign__(x)`). `TemplateProxy::Instantiate` spelled that argument like any other one: a smart pointer proxy as the smart pointer type and a proxy referring to a pointer as `T*`. Instantiating `std::string::operator=` for the `std::shared_ptr` that REntry hands out for a string field therefore failed whenever the call wasn't served from the dispatch cache, and a pointer object can trip Clang's object-argument assertion. In an unbound call of a method of a class, spell the first argument, if it is an instance of that class, as the object of the call (an lvalue of the class, as for bound calls). Assisted-by: claude-code:claude-opus-5-5 --- .../pyroot/cppjit/src/cpyrt/TemplateProxy.cxx | 19 +++++++++++++ .../pyroot/cppjit/test/test_cpp11features.py | 28 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx index d49d3d44f034e..72678c0aa07e0 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/TemplateProxy.cxx @@ -67,6 +67,14 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, std::string proto = ""; std::string protoPair = ""; bool skipSelf = fTI->fIsConstructor && (!fSelf || fSelf == Py_None); + // In an unbound call of a method of a class (as Python does for obj.f(...), + // calling the method with the object as first argument), the first argument + // is the object if it is an instance of that class. + const bool unboundMethodCall = (!fSelf || fSelf == Py_None) && + !fTI->fIsConstructor && + CPPScope_Check(fTI->fPyClass) && + !(((CPPScope*)fTI->fPyClass)->fFlags & + CPPScope::kIsNamespace); if (fSelf && (fSelf != Py_None) && (!fTI->fIsConstructor)) { PyObject* self = (PyObject*)fSelf; bool addedSelf = Utility::AddTypeName(proto, (PyObject*)Py_TYPE(self), self, @@ -87,6 +95,17 @@ PyObject* TemplateProxy::Instantiate(const std::string& fname, bool bArgSet = false; + // the object of an unbound method call + if (i == 0 && unboundMethodCall && CPPInstance_Check(itemi) && + interop::IsSubclass(((CPPClass*)Py_TYPE(itemi))->fCppType, + ((CPPClass*)fTI->fPyClass)->fCppType)) { + std::string selfname; + Utility::AddTypeName(selfname, (PyObject*)Py_TYPE(itemi), itemi, + Utility::kNone, nullptr, /*as_object=*/true); + PyTuple_SET_ITEM(tpArgs, j, PyUnicode_FromString(selfname.c_str())); + bArgSet = true; + } + // special case for arrays if (TemplateProxy_CheckExact(itemi)) { TemplateProxy* tn = (TemplateProxy*)itemi; diff --git a/bindings/pyroot/cppjit/test/test_cpp11features.py b/bindings/pyroot/cppjit/test/test_cpp11features.py index 6db5e6612d6bd..d9a02d8ce3999 100644 --- a/bindings/pyroot/cppjit/test/test_cpp11features.py +++ b/bindings/pyroot/cppjit/test/test_cpp11features.py @@ -723,3 +723,31 @@ def test22_smart_ptr_overloaded_methods(self): assert p.tget("x", 1.0) == 4 assert ns.Obj.get(p, 1) == 1 assert ns.Obj.tget(p, "x", 1.0) == 4 + + def test23_templated_method_through_smart_ptr(self): + """Templated methods called through smart pointers and pointer members""" + + import cppjit + + cppjit.cppdef("""\ + namespace SmartPtrTemplates { + struct Obj { + template int tget(T) { return 1; } + int tget(const std::string&, int) { return 2; } + }; + struct Holder { Obj* fPtr = new Obj{}; }; + std::shared_ptr make_string() { return std::make_shared("a"); } + std::shared_ptr make_obj() { return std::make_shared(); } + }""") + + ns = cppjit.gbl.SmartPtrTemplates + # Python calls methods unbound, with the object as first argument + for i in range(3): + s = ns.make_string() + s.__assign__("b%d" % i) + assert s == "b%d" % i + for p in (ns.make_obj(), ns.Holder().fPtr): + for _ in range(2): + assert p.tget(1.5) == 1 + assert p.tget("x", 1) == 2 + assert ns.Obj.tget(p, 1.5) == 1 From a3035ad0a0ed59f6ff116cf2395e0564f18ad291 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Wed, 21 Oct 2026 19:48:01 +0530 Subject: [PATCH 36/71] [cppjit] Don't mix static and non-static methods called on an object When a set of static and non-static methods was called on an object (a bound call, or Python's unbound call with the object as first argument, e.g. `emitter.Connect(...)` on `TQObject::Connect`), the object was the first argument of the overload resolution for all candidates. For the static ones it was taken as an ordinary argument, which Clang can't mix with method candidates and which made `TryObjectArgumentInitialization` assert in builds with assertions. Resolve with the kind of methods that fits the call instead: the non-static methods if there is an object (bound, or the first argument is an instance of their class), the static methods otherwise. Calling a static method of such a set on an object thus fails cleanly; it isn't supported, but no longer crashes. This replaces the CppInterOp change that passed the object to static candidates of a set called on an object. Assisted-by: claude-code:claude-opus-5-5 --- .../pyroot/cppjit/src/cpyrt/CPPOverload.cxx | 30 +++++++++++++++++++ bindings/pyroot/cppjit/test/test_overloads.py | 27 +++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx index 5285d14ba5301..90a6382659e6d 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/CPPOverload.cxx @@ -615,6 +615,36 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, overloads.push_back(i->GetMethod()); } + // In a call of a set of static and non-static methods on an object (bound, + // or unbound with the object as first argument, as Python makes for + // obj.f(...)), the object is an argument for the non-static methods only, + // so they can't be resolved together with the static ones. Pick the kind of + // methods by the object: if there is one, the non-static methods, otherwise + // the static ones. + if (is_static && !IsConstructor(ctxt.fFlags)) { + std::vector non_static, static_methods; + for (auto m : overloads) + (interop::IsNonStaticMethod(m) ? non_static : static_methods) + .push_back(m); + if (!non_static.empty()) { + bool has_object = (bool)im_self; + PyObject* first = PyVectorcall_NARGS(nargsf) > 0 + ? cpyrt_PyArgs_GET_ITEM(args, 0) + : nullptr; + if (!has_object && first && CPPInstance_Check(first)) { + interop::TCppScope_t cls = ((CPPClass*)Py_TYPE(first))->fCppType; + for (auto m : non_static) { + if (interop::IsSubclass(cls, interop::GetParentScope(m.data))) { + has_object = true; + break; + } + } + } + overloads = has_object ? non_static : static_methods; + is_static = !has_object; + } + } + std::string proto = ""; if (im_self && !(ctxt.fFlags & CallContext::kIsConstructor)) { PyObject* self = (PyObject*)im_self; diff --git a/bindings/pyroot/cppjit/test/test_overloads.py b/bindings/pyroot/cppjit/test/test_overloads.py index eba4398c30bb0..93b029e96b250 100644 --- a/bindings/pyroot/cppjit/test/test_overloads.py +++ b/bindings/pyroot/cppjit/test/test_overloads.py @@ -695,3 +695,30 @@ def test26_method_call_on_pointer_member(self): assert h.fPtr.f(1) == 1 assert h.fPtr.f(1.5) == 2 assert ns.S.f(h.fPtr, 1) == 1 + + def test27_static_and_non_static_methods(self): + """Sets of static and non-static methods don't crash the resolution""" + + import cppjit + + cppjit.cppdef("""\ + namespace MixStatic { + struct S { + int f(int) { return 1; } + int f(const char*, int) { return 2; } + static int f(S*, const char*, int) { return 3; } + static int f(double, double) { return 4; } + }; + }""") + + ns = cppjit.gbl.MixStatic + s = ns.S() + # called on an object: the non-static methods + assert s.f(1) == 1 + assert s.f("a", 1) == 2 + # called through the class without an object: the static methods + assert ns.S.f(1.0, 2.0) == 4 + # a static method called on an object would get the object as well; + # that isn't supported, but must fail cleanly + with raises(TypeError): + s.f(1.0, 2.0) From 548aff36cf930744b46681e0380941046f418565 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Wed, 21 Oct 2026 19:51:40 +0530 Subject: [PATCH 37/71] [cppjit] Pass the object as the class declaring the candidate methods Clang takes the class of the object as the acting context of method templates, but the class declaring the method for the other methods. For methods of a base class called on a derived object, the implicit object parameter of a method template thus matched exactly while the one of a non-template method needed a derived-to-base conversion, so a template won over a non-template that C++ prefers, or overloads that C++ ranks by their other arguments became ambiguous, e.g. `RDataFrame.Snapshot(tree, file, ("col",))`, inherited from `RInterface`. The caller has to pass the arguments as C++ sees them: if all method candidates are declared in one base class of the object's class, pass the object as that class, so that all candidates see the same object. Methods brought into the class of the object by a using-declaration are members of that class for the implicit object parameter (e.g. RVec's begin()), so then the object is passed as it is. This replaces the CppInterOp change that used the declaring class as acting context of method templates. Assisted-by: claude-code:claude-opus-5-5 --- .../cppjit/src/interop/interop_wrapper.cxx | 53 ++++++++++++++++++- bindings/pyroot/cppjit/test/test_overloads.py | 31 +++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx index 51af7d29b18bd..7b64de5b8759d 100644 --- a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx +++ b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx @@ -1883,15 +1883,66 @@ bool IsTemplateParamEquivalentTypes(interop::TCppType_t typ1, } namespace { +// The object of a method call is the first argument. If all method +// candidates are declared in one class that is a base of the class of the +// object, pass the object as that class. Clang takes the class of the object +// as the acting context of method templates, but the class declaring the +// method for the other methods, so that the implicit object parameter of a +// method template of the base matched exactly while the one of a non-template +// method needed a derived-to-base conversion, which made overloads that C++ +// ranks by the other arguments ambiguous (e.g. RDataFrame::Snapshot(), from +// its base RInterface). A method brought into the class of the object by a +// using-declaration is however a member of that class for the purpose of the +// implicit object parameter, so then the object stays as it is. +std::vector +ObjectAsDeclaringClass(const std::vector& candidates, + const std::vector& arg_types) { + if (arg_types.empty()) + return arg_types; + interop::TCppScope_t declaring = nullptr; + for (auto fn : candidates) { + if (!interop::IsNonStaticMethod(fn) || interop::IsConstructor(fn)) + continue; + // a using-shadow is named in the class of the using-declaration + if (interop::GetScopedFinalName(fn.data) != + interop::GetScopedFinalName(interop::GetUnderlyingScope(fn.data))) + return arg_types; + interop::TCppScope_t parent = interop::GetParentScope(fn.data); + if (declaring && parent != declaring) + return arg_types; + declaring = parent; + } + if (!declaring) + return arg_types; + + interop::TCppType_t object = arg_types.front().m_Type; + interop::TCppScope_t object_class = + interop::GetScopeFromType(interop::GetNonReferenceType(object)); + if (!object_class || object_class == declaring || + !interop::IsSubclass(object_class, declaring)) + return arg_types; + + std::vector result = arg_types; + interop::TCppType_t type = Cpp::GetTypeFromScope(declaring); + if (Cpp::GetValueKind(object) != Cpp::ValueKind::None) + type = Cpp::GetReferencedType( + type, Cpp::GetValueKind(object) == Cpp::ValueKind::RValue); + result.front() = Cpp::TemplateArgInfo(type.data); + return result; +} + interop::TCppMethod_t BestOverloadFunctionMatch( const std::vector& candidates, - const std::vector& arg_types, + const std::vector& given_arg_types, const std::vector& templ_params, std::vector& ambiguous_candidates, interop::TCppScope_t parent_scope, bool is_operator, bool count_fallback = true) { std::lock_guard Lock(InterOpMutex); + const std::vector arg_types = + ObjectAsDeclaringClass(candidates, given_arg_types); + auto remove_duplicate_methods = [](std::vector& methods) { std::set seen; methods.erase(std::remove_if(methods.begin(), methods.end(), diff --git a/bindings/pyroot/cppjit/test/test_overloads.py b/bindings/pyroot/cppjit/test/test_overloads.py index 93b029e96b250..c865cb68611e1 100644 --- a/bindings/pyroot/cppjit/test/test_overloads.py +++ b/bindings/pyroot/cppjit/test/test_overloads.py @@ -722,3 +722,34 @@ def test27_static_and_non_static_methods(self): # that isn't supported, but must fail cleanly with raises(TypeError): s.f(1.0, 2.0) + + def test28_base_class_templates_on_derived_object(self): + """Template and non-template methods of a base on a derived object""" + + import cppjit + + cppjit.cppdef("""\ + namespace BaseTemplates { + struct Opts {}; + template + struct Base { + template + int s(std::string_view, std::string_view, const std::vector&, + const Opts& = Opts()) { return 1; } + int s(std::string_view, std::string_view, const std::vector&, + const Opts& = Opts()) { return 2; } + int s(std::string_view, std::string_view, std::string_view = "", + const Opts& = Opts()) { return 3; } + }; + struct P {}; + struct Derived : Base

{}; + struct Using : Base

{ using Base

::s; }; + }""") + + ns = cppjit.gbl.BaseTemplates + # as in C++, the object converts to Base& for all candidates, so the + # non-template is preferred over the equally good template + # (a method brought in by a using-declaration is one of Using itself) + for obj in (ns.Derived(), ns.Base[ns.P](), ns.Using()): + assert obj.s("t", "f", ("x",)) == 2 + assert obj.s("t", "f", ("x",), ns.Opts()) == 2 From 0837038de22c1282b15f1c4b2b8336594fe3473a Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 22:49:05 +0530 Subject: [PATCH 38/71] [RF][PyROOT] Construct RooArgList/RooArgSet from Python lists and tuples With the Clang-driven overload resolution a Python list is no longer treated like a braced-init-list, so neither `RooArgList([x, y])` nor the implicit conversion of a list to a `const RooArgList&` argument (e.g. `RooAddPdf("m", "m", [pdf1, pdf2], [frac])`) find a constructor anymore: the old bindings went through the `std::vector` constructor, which a list can't convert to now (see the corresponding cppjit issue about lists vs. braced-init-lists). Unpack the sequence into the variadic constructor instead, like it was already done for Python sets in the RooArgSet pythonization. Numbers are replaced by RooFit::RooConst() beforehand, as the C++ collections do for doubles, because the variadic constructor that takes a double first is explicit and is thus skipped for implicit conversions. Assisted-by: claude-code:claude-opus-5-5 --- .../ROOT/_pythonization/_roofit/_rooarglist.py | 15 +++++++++++++++ .../ROOT/_pythonization/_roofit/_rooargset.py | 7 ++++++- .../ROOT/_pythonization/_roofit/_utils.py | 18 ++++++++++++++++++ bindings/pyroot/pythonizations/test/roofit.py | 16 ++++++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooarglist.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooarglist.py index 9aa280521a47d..1f6ca28a7065b 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooarglist.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooarglist.py @@ -13,12 +13,27 @@ import operator from ._rooabscollection import RooAbsCollection +from ._utils import _roo_collection_elements class RooArgList(RooAbsCollection): __cpp_name__ = 'RooArgList' + def __init__(self, *args, **kwargs): + """Pythonization of RooArgList constructor to support implicit + conversion from Python lists and tuples. + """ + # A Python list is not a braced-init-list for the overload resolution, + # so it does not convert to the C++ constructors taking an arbitrary + # number of elements. Unpack it into the variadic constructor instead, + # as `RooArgList({a, b})` would do in C++. This is also what the + # implicit conversion of a list to `const RooArgList&` goes through. + if len(args) >= 1 and isinstance(args[0], (list, tuple)): + self._init(*_roo_collection_elements(args[0]), *args[1:], **kwargs) + return + self._init(*args, **kwargs) + def __getitem__(self, key): try: operator.index(key) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooargset.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooargset.py index 9e8f4b9af6a29..35c9044380d53 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooargset.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooargset.py @@ -14,6 +14,7 @@ import warnings from ._rooabscollection import RooAbsCollection +from ._utils import _roo_collection_elements class RooArgSet(RooAbsCollection): @@ -21,7 +22,7 @@ class RooArgSet(RooAbsCollection): def __init__(self, *args, **kwargs): """Pythonization of RooArgSet constructor to support implicit - conversion from Python sets. + conversion from Python sets, lists and tuples. """ # Note: This simple Pythonization caused me days of headache. # Initially, I was also checking of `len(kwargs) == 0`, but it just @@ -42,6 +43,10 @@ def __init__(self, *args, **kwargs): ) self._init(*args[0], **kwargs) return + # Same for lists and tuples, see the RooArgList pythonization. + if len(args) >= 1 and isinstance(args[0], (list, tuple)): + self._init(*_roo_collection_elements(args[0]), *args[1:], **kwargs) + return self._init(*args, **kwargs) def __getitem__(self, key): diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_utils.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_utils.py index 775eae4da1498..88a56a7bb58ac 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_utils.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_utils.py @@ -53,6 +53,24 @@ def getter(k, v): return args, {} +def _roo_collection_elements(seq): + """Helper function to prepare the elements of a Python sequence for the + variadic constructors of RooArgList and RooArgSet. + + Numbers are replaced by RooFit::RooConst(), which is what the C++ + collections do for doubles. This is needed because the constructor taking + a double first is explicit and therefore not usable in implicit + conversions, e.g. of a list of coefficients to a `const RooArgList&`. + """ + import numbers + + import ROOT + + return tuple( + ROOT.RooFit.RooConst(float(e)) if isinstance(e, numbers.Real) and not isinstance(e, bool) else e for e in seq + ) + + def _pack_cmd_args(*args, **kwargs): # Pack command arguments passed into a RooLinkedList. diff --git a/bindings/pyroot/pythonizations/test/roofit.py b/bindings/pyroot/pythonizations/test/roofit.py index b0443f36ac06a..e4c4ad01280dd 100644 --- a/bindings/pyroot/pythonizations/test/roofit.py +++ b/bindings/pyroot/pythonizations/test/roofit.py @@ -291,6 +291,22 @@ def test_conversion_from_python_collection(self): l3 = ROOT.RooArgList([1.0, 2.0, 2.0]) self.assertEqual(len(l3), 3) + def test_implicit_conversion_from_python_collection(self): + # Lists of RooFit objects or of numbers passed where a RooFit + # collection is expected. + x = ROOT.RooRealVar("x", "x", 0.0, 10.0) + a0 = ROOT.RooRealVar("a0", "a0", 0.5, 0.0, 1.0) + frac = ROOT.RooRealVar("frac", "frac", 0.5, 0.0, 1.0) + + poly = ROOT.RooPolynomial("poly", "poly", x, [-0.1, 0.004]) + self.assertEqual(len(poly.coefList()), 2) + + cheb = ROOT.RooChebychev("cheb", "cheb", x, [a0]) + model = ROOT.RooAddPdf("model", "model", [poly, cheb], [frac]) + self.assertEqual(len(model.pdfList()), 2) + + self.assertEqual(len(ROOT.RooArgSet([x, a0])), 2) + # Necessary inside the "eval" call RooArgSet = ROOT.RooArgSet From 9e4505c9e073a887d3b5ba1af97b5ebfb8882df1 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 22:49:22 +0530 Subject: [PATCH 39/71] [RF][PyROOT] Convert lists of RooFit objects in keyword-argument pythonizations A Python list doesn't take part in the overload resolution like a C++ braced-init-list. If several overloads have the same number of parameters, e.g. the `RooAbsPdf::generate(const RooArgSet&, ...)` family, `pdf.generate([x], 1000)` is therefore ambiguous and the implicit conversion of the list to a RooArgSet is never attempted. All pythonized RooFit functions that accept RooCmdArg keyword arguments already pass their positional arguments through `_kwargs_to_roocmdargs`, so convert lists and tuples of RooAbsArgs to a RooArgList and sets to a RooArgSet there. The RooFit collections then convert into each other as in C++. Assisted-by: claude-code:claude-opus-5-5 --- .../ROOT/_pythonization/_roofit/_utils.py | 22 ++++++++++++++++++- bindings/pyroot/pythonizations/test/roofit.py | 8 +++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_utils.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_utils.py index 88a56a7bb58ac..1c27f8ce3fe81 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_utils.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_utils.py @@ -50,7 +50,7 @@ def getter(k, v): if kwargs: args = args + tuple((getter(k, v) for k, v in kwargs.items())) - return args, {} + return _sequences_to_roo_collections(args), {} def _roo_collection_elements(seq): @@ -71,6 +71,26 @@ def _roo_collection_elements(seq): ) +def _sequences_to_roo_collections(args): + """Helper function to convert Python sequences of RooFit objects to RooFit collections. + + The overload resolution does not treat a Python list like a C++ braced + initializer list, so it can't pick an overload taking e.g. a + `const RooArgSet&` for it if there are other candidates with the same + number of arguments. Convert lists and tuples of RooAbsArgs to a + RooArgList and sets to a RooArgSet, which convert to the other collection + types like in C++. + """ + import ROOT + + def convert(arg): + if isinstance(arg, (list, tuple, set)) and arg and all(isinstance(a, ROOT.RooAbsArg) for a in arg): + return ROOT.RooArgSet(arg) if isinstance(arg, set) else ROOT.RooArgList(arg) + return arg + + return tuple(convert(arg) for arg in args) + + def _pack_cmd_args(*args, **kwargs): # Pack command arguments passed into a RooLinkedList. diff --git a/bindings/pyroot/pythonizations/test/roofit.py b/bindings/pyroot/pythonizations/test/roofit.py index e4c4ad01280dd..33a8fbd196538 100644 --- a/bindings/pyroot/pythonizations/test/roofit.py +++ b/bindings/pyroot/pythonizations/test/roofit.py @@ -307,6 +307,14 @@ def test_implicit_conversion_from_python_collection(self): self.assertEqual(len(ROOT.RooArgSet([x, a0])), 2) + def test_python_collection_in_pythonized_function(self): + # RooAbsPdf::generate() has several overloads taking a RooArgSet and a + # second argument, so the list needs to be converted up-front. + x = ROOT.RooRealVar("x", "x", 0.0, 10.0) + gauss = ROOT.RooGaussian("gauss", "gauss", x, ROOT.RooFit.RooConst(5.0), ROOT.RooFit.RooConst(1.0)) + data = gauss.generate([x], 10) + self.assertEqual(data.numEntries(), 10) + # Necessary inside the "eval" call RooArgSet = ROOT.RooArgSet From 56bf82e4b1c5d48856e86f4800e25474bee75ada Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 22:51:00 +0530 Subject: [PATCH 40/71] [RF][PyROOT] Pick RooFit::Components(const char*) for Python strings `RooFit::Components` has a variadic forwarding-reference template and a `const char*` overload. In C++ a string literal selects the non-template overload, but the new overload resolution of cppjit models a Python str that is promoted to a C string as a `char*` rvalue, for which the template `Components(char*&&)` is the better match. The call then fails because the converter for `char*&&` can't take a Python str (see the corresponding cppjit issue). Pythonize `RooFit.Components` to select the `const char*` overload explicitly for a single string argument. This fixes e.g. `pdf.plotOn(frame, Components="bkg")`. Assisted-by: claude-code:claude-opus-5-5 --- .../python/ROOT/_pythonization/_roofit/__init__.py | 2 ++ .../ROOT/_pythonization/_roofit/_rooglobalfunc.py | 14 ++++++++++++++ bindings/pyroot/pythonizations/test/roofit.py | 10 ++++++++++ 3 files changed, 26 insertions(+) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/__init__.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/__init__.py index 59abc3367d0ae..4204ed499a535 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/__init__.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/__init__.py @@ -25,6 +25,7 @@ from ._roodecays import RooBCPEffDecay, RooBCPGenDecay, RooBDecay, RooBMixDecay, RooDecay from ._roogenfitstudy import RooGenFitStudy from ._rooglobalfunc import ( + Components, DataError, FitOptions, Format, @@ -82,6 +83,7 @@ # list of python functions that are used to pythonize RooGlobalFunc function in RooFit python_roofit_functions = [ + Components, DataError, FitOptions, Format, diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooglobalfunc.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooglobalfunc.py index ec3c32a898b27..b13c55897a499 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooglobalfunc.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooglobalfunc.py @@ -192,6 +192,20 @@ def Link(*args, **kwargs): return RooFit._Link(*args, **kwargs) +@cpp_signature("RooFit::Components(const char* compSpec) ;") +def Components(*args, **kwargs): + r"""The Components function is pythonized to always pick the + `Components(const char*)` overload for a Python string, like a string + literal does in C++, and not the variadic template. + """ + from ROOT import RooFit + + if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], str): + return RooFit._Components.__overload__("const char*")(args[0]) + + return RooFit._Components(*args, **kwargs) + + @cpp_signature("RooFit::DataError(Int_t) ;") def DataError(etype): r"""If you want to use the `"None"` enum value to disable error plotting, you diff --git a/bindings/pyroot/pythonizations/test/roofit.py b/bindings/pyroot/pythonizations/test/roofit.py index 33a8fbd196538..e3df8498dda50 100644 --- a/bindings/pyroot/pythonizations/test/roofit.py +++ b/bindings/pyroot/pythonizations/test/roofit.py @@ -737,6 +737,16 @@ def test_minimizer(self): # ROOT-9516 minimizer = ROOT.RooFit.Minimizer("Minuit2", "migrad") # noqa: F841 + def test_components(self): + """RooFit::Components() with a component name and with RooFit objects""" + cmd = ROOT.RooFit.Components("bkg") + self.assertEqual(str(cmd.getString(0)), "bkg") + + x = ROOT.RooRealVar("x", "x", 0.0, 1.0) + gauss = ROOT.RooGaussian("gauss", "gauss", x, ROOT.RooFit.RooConst(0.5), ROOT.RooFit.RooConst(0.1)) + cmd = ROOT.RooFit.Components(gauss) + self.assertEqual(cmd.getSet(0).size(), 1) + class TestRooJSONFactoryWSTool(unittest.TestCase): """ From df889dc398c38ea70a12ebb599770ad531ad10f4 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:06:21 +0530 Subject: [PATCH 41/71] [PyROOT] Allow constructing std::vector and RVec from numpy arrays The Clang-driven overload resolution of cppjit deliberately no longer converts a numpy array into a container, as that is a hidden deep copy: `std.vector["double"](arr)` now fails with `Failed to instantiate "vector(double*)"`. ROOT relies on this explicit construction though, e.g. in the UHI pythonization (`ROOT.std.vector("Double_t")(values)`) and in `AsRVec()` for int8 and string arrays. Pythonize the constructors of std::vector and RVec to accept a numpy array and copy its elements: with a single memory copy for numerical element types (converting the array to the element type first if needed), and element-wise otherwise. Assisted-by: claude-code:claude-opus-5-5 --- .../python/ROOT/_pythonization/_rvec.py | 53 +++++++++++++++++++ .../python/ROOT/_pythonization/_stl_vector.py | 5 +- bindings/pyroot/pythonizations/test/stl.py | 18 +++++++ 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rvec.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rvec.py index 96e2a47e6f561..6e2fe3effec0f 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rvec.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rvec.py @@ -130,6 +130,56 @@ } +def _is_numpy_array(obj): + """Check if obj is a numpy array, without importing numpy.""" + np = sys.modules.get("numpy") + return np is not None and isinstance(obj, np.ndarray) + + +def _fill_from_numpy_array(self, arr): + """Copy the elements of the numpy array arr into the empty std::vector or + RVec self. + + The overload resolution does not convert a numpy array into a container + implicitly, as that would be a hidden deep copy. Constructing a container + from an array is explicit though, so the copy is done here: with a single + memory copy if the element type is a numerical one, converting the array + to that type first if needed, and element-wise otherwise. + """ + import ctypes + + import numpy as np + + import ROOT + + flat = arr.ravel() + value_type = getattr(type(self), "value_type", None) + kind = _array_interface_dtype_map.get(value_type) + if kind in ("i", "u", "f"): + dtype = np.dtype("{}{}".format(kind, ROOT._cppyy.sizeof(value_type))) + src = np.ascontiguousarray(flat, dtype=dtype) + self.resize(src.size) + if src.size: + ctypes.memmove(ROOT._cppyy.ll.addressof(self.data()), src.ctypes.data, src.nbytes) + else: + for elem in flat.tolist(): + self.push_back(elem) + + +def _container_init(self, *args, **kwargs): + """Constructor of std::vector and RVec that also accepts a numpy array.""" + if len(args) == 1 and not kwargs and _is_numpy_array(args[0]): + self._original_init() + _fill_from_numpy_array(self, args[0]) + return + self._original_init(*args, **kwargs) + + +def _add_numpy_array_constructor(klass): + klass._original_init = klass.__init__ + klass.__init__ = _container_init + + def _get_cpp_type_from_numpy_type(dtype): cpptypes = { "i1": "signed char", @@ -258,3 +308,6 @@ def pythonize_rvec(klass, name): # Add numpy array interface add_array_interface_property(klass, name) + + # Allow the construction from a numpy array (by copy) + _add_numpy_array_constructor(klass) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_stl_vector.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_stl_vector.py index a094c0bcf53a8..55a1ee25ccbdd 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_stl_vector.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_stl_vector.py @@ -11,7 +11,7 @@ import sys from . import pythonization -from ._rvec import _array_interface_dtype_map +from ._rvec import _add_numpy_array_constructor, _array_interface_dtype_map def _data_vec_char(self): @@ -64,6 +64,9 @@ def pythonize_stl_vector(klass, name): # Add numpy array interface _add_array_interface_property(klass) + # Allow the construction from a numpy array (by copy) + _add_numpy_array_constructor(klass) + # Inject custom vector::data() value_type = getattr(klass, 'value_type', None) if value_type == 'char': diff --git a/bindings/pyroot/pythonizations/test/stl.py b/bindings/pyroot/pythonizations/test/stl.py index 1292ef9248060..a2b587666fbc8 100644 --- a/bindings/pyroot/pythonizations/test/stl.py +++ b/bindings/pyroot/pythonizations/test/stl.py @@ -170,6 +170,24 @@ def test_tree_with_containers(self): for entry_array_i, entry_root_i in zip(entry_array, entry_root): self.assertEqual(entry_array_i, entry_root_i) + def test_stl_vector_from_numpy(self): + """Constructing a std::vector or RVec from a numpy array copies it""" + try: + import numpy as np + except ImportError: + self.skipTest("numpy is not available") + + arr = np.arange(4.0) + v = ROOT.std.vector["double"](arr) + self.assertEqual(list(v), [0.0, 1.0, 2.0, 3.0]) + arr[0] = 42.0 # it's a copy + self.assertEqual(v[0], 0.0) + + # with conversion of the element type + self.assertEqual(list(ROOT.std.vector["int"](np.arange(3.0))), [0, 1, 2]) + self.assertEqual(list(ROOT.std.vector["bool"](np.array([True, False]))), [True, False]) + self.assertEqual(list(ROOT.VecOps.RVec["signed char"](np.array([1, 2], dtype=np.int8))), [1, 2]) + self.assertEqual(len(ROOT.VecOps.RVec["std::string"](np.array(["a", "b"]))), 2) class STL_set(unittest.TestCase): """ From 2bd0a7510ef2758be8fcc1c30869211f56187f68 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:12:56 +0530 Subject: [PATCH 42/71] [PyROOT] Pass the object address as void* in TClass.DynamicCast() With the Clang-driven overload resolution of cppjit, a proxy no longer converts to a `void*` parameter, just like an object doesn't in C++. The DynamicCast() pythonization passed the proxy of `obj` to `TClass::DynamicCast(const TClass*, void*, Bool_t)`, which then failed to resolve against the `const void*` overload. Pass its address cast to `void*` instead, as was already done for integer addresses. Assisted-by: claude-code:claude-opus-5-5 --- .../pythonizations/python/ROOT/_pythonization/_tclass.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tclass.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tclass.py index 42f1b20323d15..b5b29ded344e1 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tclass.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tclass.py @@ -21,6 +21,10 @@ def _TClass_DynamicCast(self, base, obj, up=True): if isinstance(obj, int): obj = cppyy.ll.cast["void*"](obj) + elif isinstance(obj, cppyy.types.Instance): + # A proxy doesn't convert to void* in the overload resolution (like + # an object doesn't in C++), so pass its address explicitly. + obj = cppyy.ll.cast["void*"](cppyy.addressof(obj)) address = cppyy.addressof(self._TClass__DynamicCast(base, obj, up)) return cppyy.bind_object(address, (base if up else self).GetName()) From 30fa7b913d388c12fd2a88ba5ed7ced1169be5a3 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:12:56 +0530 Subject: [PATCH 43/71] [PyROOT] Pass the dispatcher address as void* receiver in TQObject.Connect() With the Clang-driven overload resolution of cppjit, a proxy no longer converts to a `void*` parameter, just like an object doesn't in C++. The Connect()/Disconnect() pythonizations for Python callables passed the dispatcher proxy as the `void* receiver`, so the overload resolution between e.g. the member and the static `TQObject::Connect` failed. Pass the dispatcher's address cast to `void*` instead. Assisted-by: claude-code:claude-opus-5-5 --- .../python/ROOT/_pythonization/_tqobject.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tqobject.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tqobject.py index 18c6072d47602..860cc963b14e8 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tqobject.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tqobject.py @@ -142,6 +142,14 @@ def wrapper(*args): return wrapper +def _receiver_address(dispatcher): + """The receiver as the void* TQObject expects: a proxy doesn't convert to + void* in the overload resolution (like an object doesn't in C++).""" + import cppyy + + return cppyy.ll.cast["void*"](cppyy.addressof(dispatcher)) + + def _is_new_style_args(args, kwargs): return not kwargs and len(args) == 2 and not isinstance(args[1], str) and callable(args[1]) @@ -162,7 +170,10 @@ def _TQObject_Connect(self, *args, **kwargs): # callable, so tie the wrapper's lifetime to the dispatcher dispatcher._callable = wrapper result = self._OriginalConnect( - signal, type(dispatcher).__cpp_name__, dispatcher, "Dispatch({})".format(", ".join(arg_types)) + signal, + type(dispatcher).__cpp_name__, + _receiver_address(dispatcher), + "Dispatch({})".format(", ".join(arg_types)), ) if result: _dispatchers.setdefault(_connection_key(self, signal, callable_), []).append(dispatcher) @@ -182,7 +193,7 @@ def _TQObject_Disconnect(self, *args, **kwargs): return False result = False for dispatcher in dispatchers: - result = self._OriginalDisconnect(signal, dispatcher) or result + result = self._OriginalDisconnect(signal, _receiver_address(dispatcher)) or result return result return self._OriginalDisconnect(*args, **kwargs) From 42a6c03697f2e380a9567fd3ca85a6aee11adda4 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:12:56 +0530 Subject: [PATCH 44/71] [PyROOT] Annotate Python callables passed to the TF1, TF2 and TF3 constructors cppjit can't know the C++ type of a Python callable without annotations and spells it as `std::nullptr_t` in the overload prototype. The `template TF1(const char*, Func, ...)` constructor then deduces `Func = std::nullptr_t`, wins the overload resolution, and fails to convert the callable (see the corresponding cppjit issue). Wrap an unannotated Python callable in a function annotated with the signature ROOT expects, `double (*)(double*, double*)`, which selects the function-pointer constructor like a real function pointer does in C++. Assisted-by: claude-code:claude-opus-5-5 --- .../python/ROOT/_pythonization/_tf1.py | 31 ++++++++++++++++++- .../python/ROOT/_pythonization/_tf2.py | 3 +- .../python/ROOT/_pythonization/_tf3.py | 3 +- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tf1.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tf1.py index b18401ed0fc65..a7e8615e7479b 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tf1.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tf1.py @@ -107,12 +107,41 @@ def _TF1_EvalPar(self, vars, params): return numpy.frombuffer(out, dtype=numpy.float64, count=nrows) +def _is_python_callable(obj): + """Check if obj is a callable defined in Python (not a C++ one).""" + import ROOT + + if not callable(obj) or isinstance(obj, (str, type, ROOT._cppyy.types.Instance)): + return False + return not type(obj).__module__.startswith(("cppyy", "cppjit")) + + +def _annotate_python_callable(args): + """ + Give a Python callable passed as the function of a TF1, TF2 or TF3 the + signature `double (*)(double*, double*)` that ROOT expects. + + Without annotations the overload resolution can't know the C++ type of a + Python callable, and the constructor template taking an arbitrary + function object would be selected (and then fail to convert it). + """ + if len(args) < 2 or not _is_python_callable(args[1]) or getattr(args[1], "__annotations__", None): + return args + + pycallable = args[1] + + def annotated(x: "double*", params: "double*") -> "double": + return pycallable(x, params) + + return (args[0], annotated) + tuple(args[2:]) + + def _TF1_Constructor(self, *args, **kwargs): """ Forward the arguments to the C++ constructor and retain ownership. This helps avoiding double deletes due to ROOT automatic memory management. """ - self._cpp_constructor(*args, **kwargs) + self._cpp_constructor(*_annotate_python_callable(args), **kwargs) import ROOT ROOT.SetOwnership(self, False) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tf2.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tf2.py index 4cfd54b13e6e4..f1ca591f9adc4 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tf2.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tf2.py @@ -8,6 +8,7 @@ # For the list of contributors see $ROOTSYS/README/CREDITS. # ################################################################################ from . import pythonization +from ._tf1 import _annotate_python_callable def _TF2_constructor(self, *args, **kwargs): @@ -15,7 +16,7 @@ def _TF2_constructor(self, *args, **kwargs): Forward the arguments to the C++ constructor and retain ownership. This helps avoiding double deletes due to ROOT automatic memory management. """ - self._cpp_constructor(*args, **kwargs) + self._cpp_constructor(*_annotate_python_callable(args), **kwargs) import ROOT ROOT.SetOwnership(self, False) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tf3.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tf3.py index 42bce82639922..47005c3d03b97 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tf3.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tf3.py @@ -8,6 +8,7 @@ # For the list of contributors see $ROOTSYS/README/CREDITS. # ################################################################################ from . import pythonization +from ._tf1 import _annotate_python_callable def _TF3_constructor(self, *args, **kwargs): @@ -15,7 +16,7 @@ def _TF3_constructor(self, *args, **kwargs): Forward the arguments to the C++ constructor and retain ownership. This helps avoiding double deletes due to ROOT automatic memory management. """ - self._cpp_constructor(*args, **kwargs) + self._cpp_constructor(*_annotate_python_callable(args), **kwargs) import ROOT ROOT.SetOwnership(self, False) From 61d927616972bb5ee5e65e17edaff71c3e00dece Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:17:09 +0530 Subject: [PATCH 45/71] [PyROOT] Expect std::invalid_argument for an empty RDataFrame file list The test relied on an implementation detail of the old cppyy overload dispatch, which tried every constructor and wrapped the C++ exception of the failed attempts in a generic TypeError. With the Clang-driven overload resolution, `RDataFrame("events", [])` selects the `std::initializer_list` constructor directly, so the actual C++ exception surfaces, like it already did for an explicit `std::vector`. Assisted-by: claude-code:claude-opus-5-5 --- .../pyroot/pythonizations/test/rdataframe_misc.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/bindings/pyroot/pythonizations/test/rdataframe_misc.py b/bindings/pyroot/pythonizations/test/rdataframe_misc.py index 8fdff5d3b58c7..19529c991e296 100644 --- a/bindings/pyroot/pythonizations/test/rdataframe_misc.py +++ b/bindings/pyroot/pythonizations/test/rdataframe_misc.py @@ -53,18 +53,16 @@ def test_empty_filenames(self): """ # LLVM JIT fails to catch exceptions on Windows, so we disable their testing if not platform.system() == "Windows": - # With implicit conversions, cppyy also needs to try dispatching to the various - # constructor overloads. The C++ exception will be thrown, but will be incapsulated - # in a more generic TypeError telling the user that none of the overloads worked - with self.assertRaisesRegex(TypeError, "RDataFrame: empty list of input files."): + # The overload resolution selects a single constructor, so the real C++ + # exception surfaces, whether the list of files is given as a Python + # sequence or as an explicit vector of strings + with self.assertRaisesRegex(ROOT.std.invalid_argument, "RDataFrame: empty list of input files."): ROOT.RDataFrame("events", []) - # When passing explicitly the vector of strings, type dispatching will not be necessary - # and the real C++ exception will immediately surface with self.assertRaisesRegex(ROOT.std.invalid_argument, "RDataFrame: empty list of input files."): ROOT.RDataFrame("events", ROOT.std.vector[ROOT.std.string]()) - with self.assertRaisesRegex(TypeError, "RDataFrame: empty list of input files."): + with self.assertRaisesRegex(ROOT.std.invalid_argument, "RDataFrame: empty list of input files."): ROOT.RDataFrame("events", ()) def _get_rdf(self, dataset): From c3c7385757907647b240e7ba68cca9c5fc261472 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:17:14 +0530 Subject: [PATCH 46/71] [PyROOT] Accept iterators as RDataFrame constructor arguments The old bindings implicitly converted any iterable into a `std::vector`, so e.g. `RDataFrame("tree", (path + f for f in files))` worked (it is used in the df103 and df104 tutorials). The Clang-driven overload resolution of cppjit has no C++ type for a generator. Turn iterators among the arguments of the pythonized RDataFrame constructor into lists, which match the `std::initializer_list` and `std::vector` constructors. Assisted-by: claude-code:claude-opus-5-5 --- .../ROOT/_pythonization/_rdf_namespace.py | 21 ++++++++++++++++++- .../pythonizations/test/rdataframe_misc.py | 12 +++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rdf_namespace.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rdf_namespace.py index 150be14261368..76f2b22fc5a23 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rdf_namespace.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rdf_namespace.py @@ -85,6 +85,25 @@ def fromspec(jsonfile, *args, **kwargs): return fromspec +def _iterators_to_lists(args): + """ + Turn iterators (e.g. generators) among the arguments into lists, which + the overload resolution matches with the std::initializer_list and + std::vector parameters of the RDataFrame constructors, e.g. for + `RDataFrame("tree", (prefix + f for f in files))`. + """ + import collections.abc + + import ROOT + + return tuple( + list(arg) + if isinstance(arg, collections.abc.Iterator) and not isinstance(arg, ROOT._cppyy.types.Instance) + else arg + for arg in args + ) + + def _rdataframe(local_rdf, distributed_rdf): """ Create a callable that correctly dispatches either to the local or @@ -100,7 +119,7 @@ def rdataframe(*args, **kwargs): rdf = distributed_rdf(*args, **kwargs) rnode = ROOT.RDF.AsRNode(rdf._headnode.rdf_node) else: - rdf = local_rdf(*args, **kwargs) + rdf = local_rdf(*_iterators_to_lists(args), **kwargs) rnode = ROOT.RDF.AsRNode(rdf) if args and isinstance(args[0], ROOT.TTree): diff --git a/bindings/pyroot/pythonizations/test/rdataframe_misc.py b/bindings/pyroot/pythonizations/test/rdataframe_misc.py index 19529c991e296..3ff956a4afb91 100644 --- a/bindings/pyroot/pythonizations/test/rdataframe_misc.py +++ b/bindings/pyroot/pythonizations/test/rdataframe_misc.py @@ -84,6 +84,18 @@ def _define_col(self, rdf): def _filter_x(self, rdf): return rdf.Filter("x > 2") + def test_filenames_from_generator(self): + """ + The list of input files can be given as a generator + """ + filename = "rdataframe_misc_generator.root" + ROOT.RDataFrame(5).Define("x", "1.").Snapshot("events", filename) + try: + rdf = ROOT.RDataFrame("events", (f for f in [filename, filename])) + self.assertEqual(rdf.Count().GetValue(), 10) + finally: + os.remove(filename) + def _test_rdf_in_function(self, chain): rdf = ROOT.RDataFrame(chain) meanx = rdf.Mean("x") From 5b2f21b33245c073ed6755465f049d4376e43ae5 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:17:43 +0530 Subject: [PATCH 47/71] [PyROOT] Convert a list of result pointers for RDF.RunGraphs() `ROOT.RDF.RunGraphs([h1, h2])` relied on the old bindings converting a Python list into the `std::vector` parameter, converting each RResultPtr on the way. The Clang-driven overload resolution of cppjit doesn't treat a Python list as a braced-init-list (see the corresponding cppjit issue), so the conversion fails. Build the vector of RResultHandles explicitly in the RunGraphs pythonization. Assisted-by: claude-code:claude-opus-5-5 --- .../ROOT/_pythonization/_rdf_namespace.py | 20 ++++++++++++++++++- .../pythonizations/test/rdataframe_misc.py | 10 ++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rdf_namespace.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rdf_namespace.py index 76f2b22fc5a23..d7c5547acba4a 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rdf_namespace.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rdf_namespace.py @@ -31,6 +31,24 @@ def _create_distributed_module(parent, experimental: bool = False): return ROOT._distrdf.create_distributed_module(parent, experimental) +def _to_result_handles(handles): + """ + Convert a Python sequence of result pointers to the + std::vector taken by RunGraphs. The overload resolution + doesn't treat a Python list like a C++ braced-init-list, so it doesn't + convert the elements to RResultHandle by itself. + """ + import ROOT + + if not isinstance(handles, (list, tuple)): + return handles + vec = ROOT.std.vector["ROOT::RDF::RResultHandle"]() + vec.reserve(len(handles)) + for handle in handles: + vec.push_back(ROOT.RDF.RResultHandle(handle)) + return vec + + def _rungraphs(distrdf_rungraphs, rdf_rungraphs): """ Create a callable that correctly dispatches either to the local or @@ -43,7 +61,7 @@ def rungraphs(handles): if len(handles) > 0 and "_distrdf" in str(type(handles[0])): return distrdf_rungraphs(handles) else: - return rdf_rungraphs(handles) + return rdf_rungraphs(_to_result_handles(handles)) return rungraphs diff --git a/bindings/pyroot/pythonizations/test/rdataframe_misc.py b/bindings/pyroot/pythonizations/test/rdataframe_misc.py index 3ff956a4afb91..900e5a384a52d 100644 --- a/bindings/pyroot/pythonizations/test/rdataframe_misc.py +++ b/bindings/pyroot/pythonizations/test/rdataframe_misc.py @@ -96,6 +96,16 @@ def test_filenames_from_generator(self): finally: os.remove(filename) + def test_rungraphs_with_list(self): + """ + RunGraphs accepts a Python list of result pointers + """ + rdf = ROOT.RDataFrame(10).Define("x", "1.") + h = rdf.Histo1D("x") + s = rdf.Sum("x") + self.assertEqual(ROOT.RDF.RunGraphs([h, s]), 1) + self.assertEqual(s.GetValue(), 10.0) + def _test_rdf_in_function(self, chain): rdf = ROOT.RDataFrame(chain) meanx = rdf.Mean("x") From 49ae252b6fcd5812f5204da9e43af645df096061 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:22:38 +0530 Subject: [PATCH 48/71] [PyROOT] Fill RCsvDS column types element-wise in MakeCSVDataFrame() `MakeCSVDataFrame(..., colTypes={"col": "T"})` assigned the Python dict to the `std::unordered_map` data member of the RCsvDS options, relying on the old bindings converting a dict into a map. The Clang-driven overload resolution of cppjit doesn't model a dict as a braced list of key/value pairs (see the corresponding cppjit issue), so no `operator=` matches anymore. Insert the key/value pairs one by one instead. Assisted-by: claude-code:claude-opus-5-5 --- bindings/pyroot/pythonizations/python/ROOT/_facade.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_facade.py b/bindings/pyroot/pythonizations/python/ROOT/_facade.py index eca347220598c..8003497fd5ce7 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_facade.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_facade.py @@ -430,11 +430,17 @@ def RDF(self): ns = self._fallback_getattr("RDF") def MakeCSVDataFrame(fileName, readHeaders=True, delimiter=",", linesChunkSize=-1, colTypes={}, **kwargs): + import ROOT + options = ns.RCsvDS.ROptions() options.fHeaders = readHeaders options.fDelimiter = delimiter options.fLinesChunkSize = linesChunkSize - options.fColumnTypes = colTypes + # Fill the std::unordered_map element-wise: a + # Python dict doesn't convert to it in the overload resolution + column_type = ROOT.std.pair["std::string", "char"] + for column, column_type_code in colTypes.items(): + options.fColumnTypes.insert(column_type(column, column_type_code)) for key, val in kwargs.items(): structMemberName = "f" + key[0].upper() + key[1:] if hasattr(options, structMemberName): From ccbe2de4e5cc8a0f262b022c3842379d46338172 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:23:49 +0530 Subject: [PATCH 49/71] [PyROOT] Pass the object address as void* in TDirectory.WriteObject() For objects not deriving from TObject, the WriteObject() pythonization calls `WriteObjectAny(const void*, const char*, ...)` with the proxy of the object. With the Clang-driven overload resolution of cppjit a proxy no longer converts to a `void*` parameter, just like an object doesn't in C++, so the call failed to choose between the `const char* classname` and `const TClass*` overloads. Pass the object's address cast to `void*`. Assisted-by: claude-code:claude-opus-5-5 --- .../pythonizations/python/ROOT/_pythonization/_tdirectory.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tdirectory.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tdirectory.py index 9a6ae496922c5..b2357985fe063 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tdirectory.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tdirectory.py @@ -90,7 +90,10 @@ def _TDirectory_WriteObject(self, obj, *args): if isinstance(obj, ROOT.TObject): return self.WriteTObject(obj, *args) - return self.WriteObjectAny(obj, type(obj).__cpp_name__, *args) + # A proxy doesn't convert to the void* parameter in the overload + # resolution (like an object doesn't in C++), so pass its address + address = ROOT._cppyy.ll.cast["void*"](ROOT._cppyy.addressof(obj)) + return self.WriteObjectAny(address, type(obj).__cpp_name__, *args) def _ipython_key_completions_(self): From 55a8ba0fc5a2496843d86cf9d854d62d29c5a5f8 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:23:50 +0530 Subject: [PATCH 50/71] [PyROOT] Call the void* overload explicitly in TTree.SetBranchAddress() For numpy and array.array buffers the SetBranchAddress() pythonization re-implements the template overload by calling `SetBranchAddress(const char*, void*, TBranch**, TClass*, EDataType, bool)` with keyword arguments. With the Clang-driven overload resolution of cppjit, the buffer is spelled as a typed pointer (e.g. `double*`), so the `template SetBranchAddress(const char*, T*, TBranch**)` overload was selected; keyword arguments are moreover not considered when resolving templated methods (see the corresponding cppjit issue). Pass the buffer address as a `void*` and all arguments positionally. Assisted-by: claude-code:claude-opus-5-5 --- .../pythonizations/python/ROOT/_pythonization/_ttree.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_ttree.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_ttree.py index 971d935975031..57f6e4b38bb72 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_ttree.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_ttree.py @@ -340,7 +340,14 @@ def _SetBranchAddress(self, bname, addr, *args, **kwargs): elif "ptr" in kwargs: tbranch_ptr = kwargs["ptr"] - return self._OriginalSetBranchAddress(bname, addr, ptr=tbranch_ptr, realClass=cl, datatype=tp, isptr=False) + # Pass the buffer as the void* the non-template overload takes: as a typed + # pointer, the array would select the template overloads instead. The + # arguments are passed positionally, as the overload resolution doesn't + # match keyword arguments to parameters. + import ctypes + + buf = ctypes.c_void_p(_get_address_of(addr)) + return self._OriginalSetBranchAddress(bname, buf, tbranch_ptr, cl, tp, False) def _get_address_of(obj): From 21c99e240ec0881d996850d76ff7681cd3af9f04 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:40:59 +0530 Subject: [PATCH 51/71] [PyROOT] Select TTree::Branch(const char*, Int_t, Int_t) explicitly `tree.Branch("folder", 0)` fails with the Clang-driven overload resolution of cppjit: the argument promotions make it consider the `template Branch(const char*, T*, ...)` overloads as well and report an ambiguity, while in C++ an int can't deduce a `T*` and the call is unique (see the corresponding cppjit issue). Select the folder overload explicitly in the Branch() pythonization for a string followed by one or two integers. Assisted-by: claude-code:claude-opus-5-5 --- .../python/ROOT/_pythonization/_ttree.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_ttree.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_ttree.py index 57f6e4b38bb72..c13abe9ae2dd7 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_ttree.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_ttree.py @@ -435,6 +435,21 @@ def _try_branch_ptr_to_ptr_overloads(self, args): return self._OriginalBranch(name, class_name, ctypes.c_void_p(buf), *rest) +def _try_branch_folder_overload(self, args): + """Try to match TTree::Branch(const char* folder, Int_t = 32000, Int_t = 99). + + The overload resolution of cppjit considers this call ambiguous with the + `Branch(const char*, T*, ...)` templates, which C++ doesn't (an int can't + deduce a T*), so select the overload explicitly. + """ + if not (2 <= len(args) <= 3) or not isinstance(args[0], str): + return None + if any(not isinstance(arg, int) or isinstance(arg, bool) for arg in args[1:]): + return None + + return self._OriginalBranch.__overload__("const char*, Int_t, Int_t")(self, *args) + + def _Branch(self, *args): """ Pythonization for TTree::Branch. @@ -453,6 +468,7 @@ def _Branch(self, *args): - ( const char*, void*, const char*, Int_t = 32000 ) - ( const char*, const char*, T**, Int_t = 32000, Int_t = 99 ) - ( const char*, T**, Int_t = 32000, Int_t = 99 ) + - ( const char*, Int_t = 32000, Int_t = 99 ) """ if len(args) >= 2: res = _try_branch_leaf_list_overload(self, args) @@ -463,6 +479,10 @@ def _Branch(self, *args): if res is not None: return res + res = _try_branch_folder_overload(self, args) + if res is not None: + return res + # Fall back to the original implementation for the rest of overloads return self._OriginalBranch(*args) From eb4bb9463a058a7b1ce77d987b26adba633696a5 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:44:17 +0530 Subject: [PATCH 52/71] [PyROOT] Pass an explicit T** to BranchPtrToPtr for proxies owning their pointer The Branch() pythonization hands an object proxy to the `BranchPtrToPtr(TTree&, const char*, const char*, T**, ...)` helper and relied on the bindings converting the proxy into the address of the pointer it holds. With the Clang-driven overload resolution of cppjit, a proxy spelled as `T&` doesn't match a `T**` parameter (it doesn't in C++ either); only a reference proxy, spelled as a `T*`, is still promoted to `T**`. Keep passing the proxy, which handles reference proxies to the caller's pointer, and fall back to passing the address of the pointer held by the proxy explicitly as a `T**`. Assisted-by: claude-code:claude-opus-5-5 --- .../python/ROOT/_pythonization/_ttree.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_ttree.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_ttree.py index c13abe9ae2dd7..ccd284c1d736e 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_ttree.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_ttree.py @@ -424,7 +424,15 @@ def _try_branch_ptr_to_ptr_overloads(self, args): # give as a base or equivalent spelling of it. proxy_type = type(address).__cpp_name__ brancher = _lookups().helpers.BranchPtrToPtr[proxy_type] - return brancher(self, name, class_name or proxy_type, address, *rest) + try: + # a reference proxy (to the caller's pointer) is taken as a T* + # and promoted to the T** by the overload resolution + return brancher(self, name, class_name or proxy_type, address, *rest) + except TypeError: + # a proxy owning its pointer doesn't convert to T** in the overload + # resolution, so pass the address of that pointer explicitly + ptr_to_ptr = ROOT._cppyy.ll.cast[proxy_type + "**"](ROOT._cppyy.addressof(address, byref=True)) + return brancher(self, name, class_name or proxy_type, ptr_to_ptr, *rest) buf = _get_address_of(address) if not buf or not class_name: From 314f0b5f3d63fc575138dd05bd37352844adbe06 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:58:36 +0530 Subject: [PATCH 53/71] [RF][PyROOT] Convert lists of RooFit objects in all RooFit constructors A Python list is not treated like a C++ braced-init-list by the Clang-driven overload resolution of cppjit (see the corresponding cppjit issue). If a RooFit class has several constructors with the same number of parameters, e.g. `RooAddPdf(name, title, [pdf1, pdf2], [frac], True)`, `RooHistPdf`, `RooNDKeysPdf`, `RooRealSumPdf` or `RooMultiVarGaussian`, the constructor taking RooFit collections is therefore not found. Wrap the constructors of the RooFit classes that are not pythonized otherwise to convert lists and tuples of RooAbsArgs to RooArgLists and sets to RooArgSets beforehand, like it is already done for the functions with keyword argument pythonizations. Assisted-by: claude-code:claude-opus-5-5 --- .../ROOT/_pythonization/_roofit/__init__.py | 23 +++++++++++++++++++ bindings/pyroot/pythonizations/test/roofit.py | 10 ++++++++ 2 files changed, 33 insertions(+) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/__init__.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/__init__.py index 4204ed499a535..c4bd43d14345f 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/__init__.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/__init__.py @@ -175,12 +175,35 @@ def make_func_name_orig(func_name): return "_" + func_name +def _convert_collections_in_constructor(klass): + """Make the constructor of a RooFit class accept Python sequences of + RooFit objects for RooFit collection parameters. + + A Python list is not treated like a C++ braced-init-list by the overload + resolution, so e.g. `RooAddPdf(name, title, [pdf1, pdf2], [frac], True)` + would not find the constructor taking two `const RooArgList&`. + """ + from ._utils import _sequences_to_roo_collections + + cpp_init = klass.__init__ + + def __init__(self, *args, **kwargs): + cpp_init(self, *_sequences_to_roo_collections(args), **kwargs) + + __init__.__doc__ = cpp_init.__doc__ + klass.__init__ = __init__ + + @pythonization("Roo", is_prefix=True) def pythonize_roofit_class(klass, name): # Parameters: # klass: class to pythonize # name: string containing the name of the class + python_klass = python_classes_dict.get(name) + if python_klass is None or "__init__" not in get_defined_attributes(python_klass): + _convert_collections_in_constructor(klass) + if name not in python_classes_dict: return diff --git a/bindings/pyroot/pythonizations/test/roofit.py b/bindings/pyroot/pythonizations/test/roofit.py index e3df8498dda50..4241e11c6aa13 100644 --- a/bindings/pyroot/pythonizations/test/roofit.py +++ b/bindings/pyroot/pythonizations/test/roofit.py @@ -307,6 +307,16 @@ def test_implicit_conversion_from_python_collection(self): self.assertEqual(len(ROOT.RooArgSet([x, a0])), 2) + def test_python_collection_in_constructor(self): + # RooAddPdf has several constructors with five parameters, so the + # lists need to be converted up-front. + x = ROOT.RooRealVar("x", "x", 0.0, 10.0) + g1 = ROOT.RooGaussian("g1", "g1", x, ROOT.RooFit.RooConst(3.0), ROOT.RooFit.RooConst(1.0)) + g2 = ROOT.RooGaussian("g2", "g2", x, ROOT.RooFit.RooConst(6.0), ROOT.RooFit.RooConst(1.0)) + frac = ROOT.RooRealVar("frac", "frac", 0.5, 0.0, 1.0) + model = ROOT.RooAddPdf("model", "model", [g1, g2], [frac], True) + self.assertEqual(len(model.pdfList()), 2) + def test_python_collection_in_pythonized_function(self): # RooAbsPdf::generate() has several overloads taking a RooArgSet and a # second argument, so the list needs to be converted up-front. From 7b6deb0402472e1e73496af0d6d890e9480334d5 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Fri, 16 Oct 2026 23:59:36 +0530 Subject: [PATCH 54/71] [RF][PyROOT] Convert lists of RooFit objects in tuple keyword arguments A tuple given as keyword argument value, e.g. `pdf.plotOn(frame, VisualizeError=(fitres, [par1, par2], 2))`, is unpacked into the arguments of the RooCmdArg function. A list of RooFit objects in there didn't convert to the `const RooArgSet&` parameter with the Clang-driven overload resolution of cppjit (see the corresponding cppjit issue), so convert it beforehand like the other positional arguments. Assisted-by: claude-code:claude-opus-5-5 --- .../python/ROOT/_pythonization/_roofit/_utils.py | 2 +- bindings/pyroot/pythonizations/test/roofit.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_utils.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_utils.py index 1c27f8ce3fe81..093d1b7a05353 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_utils.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_utils.py @@ -40,7 +40,7 @@ def getter(k, v): # function. If this doesn't succeed, the tuple, list, or dict, # will be passed directly to the function as it's only argument. if isinstance(v, (tuple, list, set)): - return func(*v) + return func(*_sequences_to_roo_collections(v)) elif isinstance(v, (dict,)): return func(**v) except Exception: diff --git a/bindings/pyroot/pythonizations/test/roofit.py b/bindings/pyroot/pythonizations/test/roofit.py index 4241e11c6aa13..8ab36351b81c7 100644 --- a/bindings/pyroot/pythonizations/test/roofit.py +++ b/bindings/pyroot/pythonizations/test/roofit.py @@ -317,6 +317,18 @@ def test_python_collection_in_constructor(self): model = ROOT.RooAddPdf("model", "model", [g1, g2], [frac], True) self.assertEqual(len(model.pdfList()), 2) + def test_python_collection_in_keyword_argument(self): + # The tuple given as a keyword argument is unpacked into the arguments + # of the RooCmdArg function, which can include a list of RooFit objects. + x = ROOT.RooRealVar("x", "x", 0.0, 10.0) + mean = ROOT.RooRealVar("mean", "mean", 5.0, 0.0, 10.0) + gauss = ROOT.RooGaussian("gauss", "gauss", x, mean, ROOT.RooFit.RooConst(1.0)) + data = gauss.generate([x], 100) + res = gauss.fitTo(data, Save=True, PrintLevel=-1) + frame = x.frame() + gauss.plotOn(frame, VisualizeError=(res, [mean], 1)) + self.assertEqual(frame.numItems(), 1) + def test_python_collection_in_pythonized_function(self): # RooAbsPdf::generate() has several overloads taking a RooArgSet and a # second argument, so the list needs to be converted up-front. From c9d25d9697868353a9b0d00e9411019239cf4566 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 00:01:31 +0530 Subject: [PATCH 55/71] [PyROOT] Support TLegend.AddEntry(0, label) for entries without an object Adding a legend entry without an object, e.g. a header line, is commonly written as `legend.AddEntry(0, "label", "")` in Python. With the Clang-driven overload resolution of cppjit, a Python integer no longer converts to a pointer, and a null pointer is ambiguous between the `AddEntry(const TObject*, ...)` and `AddEntry(const char*, ...)` overloads, just like in C++. Add a TLegend pythonization that selects the TObject overload with a null pointer when the object is given as 0, None or ROOT.nullptr. Assisted-by: claude-code:claude-opus-5-5 --- .../pythonizations/python/CMakeLists.txt | 1 + .../python/ROOT/_pythonization/_tlegend.py | 35 +++++++++++++++++++ .../pyroot/pythonizations/test/misc_pyz.py | 19 ++++++++++ 3 files changed, 55 insertions(+) create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tlegend.py diff --git a/bindings/pyroot/pythonizations/python/CMakeLists.txt b/bindings/pyroot/pythonizations/python/CMakeLists.txt index 97027218324f9..75546ab384382 100644 --- a/bindings/pyroot/pythonizations/python/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/python/CMakeLists.txt @@ -122,6 +122,7 @@ set(py_sources ROOT/_pythonization/_th3.py ROOT/_pythonization/_thistpainter.py ROOT/_pythonization/_titer.py + ROOT/_pythonization/_tlegend.py ROOT/_pythonization/_tobject.py ROOT/_pythonization/_tobjstring.py ROOT/_pythonization/_tqobject.py diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tlegend.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tlegend.py new file mode 100644 index 0000000000000..5219affed8810 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tlegend.py @@ -0,0 +1,35 @@ +# Author: Vipul Cariappa 10/2026 + +################################################################################ +# Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. # +# All rights reserved. # +# # +# For the licensing terms see $ROOTSYS/LICENSE. # +# For the list of contributors see $ROOTSYS/README/CREDITS. # +################################################################################ +from . import pythonization + + +def _TLegend_AddEntry(self, *args, **kwargs): + """ + Allow adding an entry without an object with `legend.AddEntry(0, label)` + (or `None` / `ROOT.nullptr` in place of 0). + + A Python integer does not convert to a pointer in the overload resolution, + and a null pointer would be ambiguous between the + `AddEntry(const TObject*, ...)` and `AddEntry(const char*, ...)` + overloads, so the object overload is selected explicitly. + """ + import ROOT + + if args and not kwargs and (args[0] is None or args[0] is ROOT.nullptr or (type(args[0]) is int and args[0] == 0)): + add_entry = self._AddEntry.__overload__("const TObject*, const char*, Option_t*") + return add_entry(ROOT.nullptr, *args[1:]) + + return self._AddEntry(*args, **kwargs) + + +@pythonization("TLegend") +def pythonize_tlegend(klass): + klass._AddEntry = klass.AddEntry + klass.AddEntry = _TLegend_AddEntry diff --git a/bindings/pyroot/pythonizations/test/misc_pyz.py b/bindings/pyroot/pythonizations/test/misc_pyz.py index bac0730468c86..7167f21218974 100644 --- a/bindings/pyroot/pythonizations/test/misc_pyz.py +++ b/bindings/pyroot/pythonizations/test/misc_pyz.py @@ -129,6 +129,25 @@ def test_all(self): self.constructor_twoargs() +class TLegendAddEntry(unittest.TestCase): + """ + Test the pythonization of TLegend::AddEntry + """ + + def test_entry_without_object(self): + leg = ROOT.TLegend(0.1, 0.1, 0.3, 0.3) + for null in (0, None, ROOT.nullptr): + entry = leg.AddEntry(null, "header", "") + self.assertEqual(entry.GetLabel(), "header") + self.assertFalse(entry.GetObject()) + + def test_entry_with_object(self): + leg = ROOT.TLegend(0.1, 0.1, 0.3, 0.3) + h = ROOT.TH1F("h_tlegend_addentry", "", 1, 0, 1) + entry = leg.AddEntry(h, "histogram", "l") + self.assertEqual(entry.GetLabel(), "histogram") + self.assertEqual(entry.GetObject(), h) + class TestTComplexOperators(unittest.TestCase): """ Test for the operators of TComplex: From 842c51b83c62eccf2959d185d1ea8b3e765e21f6 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 00:09:45 +0530 Subject: [PATCH 56/71] [df] Expect std::invalid_argument when merging different RDF results The test relied on an implementation detail of the old cppyy overload dispatch, which tried every overload of `MergeValues` and wrapped the C++ exceptions of the failed attempts in a generic TypeError. With the Clang-driven overload resolution of cppjit the single best overload is called, so the actual `std::invalid_argument` thrown for results of different actions surfaces. Assisted-by: claude-code:claude-opus-5-5 --- tree/dataframe/test/dataframe_merge_results.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tree/dataframe/test/dataframe_merge_results.py b/tree/dataframe/test/dataframe_merge_results.py index 31ab35f2e61b7..47bbea3720366 100644 --- a/tree/dataframe/test/dataframe_merge_results.py +++ b/tree/dataframe/test/dataframe_merge_results.py @@ -11,7 +11,7 @@ class RDataFrameMergeResults(unittest.TestCase): """Various tests for the RMergeableValue family of classes.""" def test_wrong_merge(self): - """Merging results of different operations raises `TypeError`""" + """Merging results of different operations raises `std::invalid_argument`""" df = ROOT.RDataFrame(100) min = df.Min("rdfentry_") @@ -20,7 +20,7 @@ def test_wrong_merge(self): mmin = GetMergeableValue(min) mmax = GetMergeableValue(max) - self.assertRaises(TypeError, MergeValues, mmin, mmax) + self.assertRaises(ROOT.std.invalid_argument, MergeValues, mmin, mmax) def test_pickle_mean(self): """Merge two averages, pickle and merge again.""" From 29f3bc6711345b6944b23cbd5e011d5b36efb5f4 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 00:09:45 +0530 Subject: [PATCH 57/71] [PyROOT][roottest] Expect an error for a call that is ambiguous in C++ `myfunc(C(), Z())` is ambiguous in C++, and the test documented that the old bindings silently called the overload with the highest priority instead ("Would be nice to throw an error in this kind of scenario"). The Clang-driven overload resolution of cppjit reports the ambiguity like C++ does (as an OverloadAmbiguityException, a TypeError), so test for it. Assisted-by: claude-code:claude-opus-5-5 --- roottest/python/cpp/PyROOT_cpptests.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/roottest/python/cpp/PyROOT_cpptests.py b/roottest/python/cpp/PyROOT_cpptests.py index 7bb149f0a3af8..6caf5bc35bbd3 100644 --- a/roottest/python/cpp/PyROOT_cpptests.py +++ b/roottest/python/cpp/PyROOT_cpptests.py @@ -259,13 +259,13 @@ def test2TwoArgumentFunctionAmbiguous(self): """ Test the behaviour of a scenario that would be ambiguous in C++. - In PyROOT, the function with the highest priority in the overload - resolution will be called. Would be nice to throw an error in this kind - of scenario. + Like in C++, where calling myfunc(C(), Z()) is an error ("call to + 'myfunc' is ambiguous"), the overload resolution raises an error. """ - # In C++ calling myfunc(C(), Z()) would throw - # error: call to 'myfunc' is ambiguous - self.assertEqual(ROOT.Cpp4.myfunc(ROOT.Cpp4.C(), ROOT.Cpp4.Z()), 1) + import cppjit + + with self.assertRaises(cppjit.OverloadAmbiguityException): + ROOT.Cpp4.myfunc(ROOT.Cpp4.C(), ROOT.Cpp4.Z()) ## actual test run From 9e76a609b254544223a02f9d83758c35effa82c8 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 00:22:00 +0530 Subject: [PATCH 58/71] [PyROOT] Select TColor::GetColor(Float_t, ...) for float components `TColor.GetColor(0.5, 0.2, 0.1)` is ambiguous with the Clang-driven overload resolution of cppjit, as it is in C++: a Python float is a double, which converts equally well to the Float_t and the Int_t components of the two GetColor() overloads. The old bindings picked the Float_t overload; this is used e.g. in the df105 and df107 tutorials. Pythonize GetColor() to select the Float_t overload explicitly if any of the color components is a Python float. Assisted-by: claude-code:claude-opus-5-5 --- .../python/ROOT/_pythonization/_tcolor.py | 25 +++++++++++++++++++ .../pyroot/pythonizations/test/misc_pyz.py | 14 +++++++++++ 2 files changed, 39 insertions(+) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tcolor.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tcolor.py index 8b68a8824d475..ac7ad9b0874da 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tcolor.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tcolor.py @@ -20,7 +20,32 @@ def _TColor_constructor(self, *args, **kwargs): ROOT.SetOwnership(self, False) +def _TColor_GetColor(*args, **kwargs): + """ + Select TColor::GetColor(Float_t r, Float_t g, Float_t b, Float_t a) if + any of the color components is given as a Python float. + + A Python float is a double, which converts equally well to the Float_t + and the Int_t components of the two GetColor() overloads, so the call + would be ambiguous (as it is in C++). + """ + import ROOT + + get_color = ROOT.TColor._GetColor + if ( + 3 <= len(args) <= 4 + and not kwargs + and all(isinstance(arg, (int, float)) and not isinstance(arg, bool) for arg in args) + and any(isinstance(arg, float) for arg in args[:3]) + ): + return get_color.__overload__("Float_t, Float_t, Float_t, Float_t")(*args) + return get_color(*args, **kwargs) + + @pythonization("TColor") def pythonize_tcolor(klass): klass._cpp_constructor = klass.__init__ klass.__init__ = _TColor_constructor + + klass._GetColor = klass.GetColor + klass.GetColor = staticmethod(_TColor_GetColor) diff --git a/bindings/pyroot/pythonizations/test/misc_pyz.py b/bindings/pyroot/pythonizations/test/misc_pyz.py index 7167f21218974..ea99554ad8403 100644 --- a/bindings/pyroot/pythonizations/test/misc_pyz.py +++ b/bindings/pyroot/pythonizations/test/misc_pyz.py @@ -148,6 +148,20 @@ def test_entry_with_object(self): self.assertEqual(entry.GetLabel(), "histogram") self.assertEqual(entry.GetObject(), h) +class TColorGetColor(unittest.TestCase): + """ + Test the pythonization of TColor::GetColor + """ + + def test_float_and_int_components(self): + c_float = ROOT.TColor.GetColor(1.0, 0.25, 0.0) + color = ROOT.gROOT.GetColor(c_float) + self.assertAlmostEqual(color.GetGreen(), 0.25, places=2) + self.assertEqual(ROOT.TColor.GetColor(1.0, 0.25, 0.0, 1.0), c_float) + + c_int = ROOT.TColor.GetColor(255, 128, 0) + self.assertEqual(ROOT.TColor.GetColor("#ff8000"), c_int) + class TestTComplexOperators(unittest.TestCase): """ Test for the operators of TComplex: From ab081c13968a64543aff46d8bb02db4a5d44eca9 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 00:23:16 +0530 Subject: [PATCH 59/71] [df][PyROOT] Pass column lists as std::vector in the Define/Filter pythonizations When a C++ callable is given to the Define() or Filter() pythonizations, the Python list of column names (empty by default) is passed on to the templated `RInterface::Define(std::string_view, F, const ColumnNames_t&)`. The Clang-driven overload resolution of cppjit doesn't treat a Python list like a C++ braced-init-list (see the corresponding cppjit issue), so e.g. `df.Define("x", ROOT.some_function)` failed to resolve (df042 tutorial). Convert the list of column names to a `std::vector` explicitly. Assisted-by: claude-code:claude-opus-5-5 --- .../python/ROOT/_pythonization/_rdf_pyz.py | 28 ++++++++++++++++--- .../pythonizations/test/rdf_define_pyz.py | 9 ++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rdf_pyz.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rdf_pyz.py index 1801474b712ab..10ca08aed27a9 100755 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rdf_pyz.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rdf_pyz.py @@ -283,6 +283,26 @@ def foo(x, y): cls.called = True +def _column_names(cols): + """ + Convert a Python list of column names to the std::vector of + the RDataFrame interfaces. The overload resolution doesn't treat a list + like a C++ braced-init-list (for an empty one in particular), so for + templated methods like Define it couldn't select the overload taking the + column names. + """ + import ROOT + + names = ROOT.std.vector["std::string"]() + for name in cols: + names.push_back(name) + return names + + +def _column_lists_to_vectors(args): + return tuple(_column_names(arg) if isinstance(arg, list) else arg for arg in args) + + def _PyFilter(rdf, callable_or_str, *args, extra_args={}): """ Filters the entries of RDF according to a given condition. @@ -354,10 +374,10 @@ def x_more_than_y(x): import ROOT if isinstance(func, (ROOT._cppyy.types.Function, ROOT._cppyy.types.Template)): - return rdf._OriginalFilter(_resolve_overload_based_on_cols(func, rdf, col_list), *args) + return rdf._OriginalFilter(_resolve_overload_based_on_cols(func, rdf, col_list), *_column_lists_to_vectors(args)) if isinstance(func, ROOT._cppyy.types.Instance): - return rdf._OriginalFilter(func, *args) + return rdf._OriginalFilter(func, *_column_lists_to_vectors(args)) _WarnOnce.warn() jitter = FunctionJitter(rdf) @@ -419,9 +439,9 @@ def x_scaled(x): import ROOT if isinstance(func, (ROOT._cppyy.types.Function, ROOT._cppyy.types.Template)): - return rdf._OriginalDefine(col_name, _resolve_overload_based_on_cols(func, rdf, cols), cols) + return rdf._OriginalDefine(col_name, _resolve_overload_based_on_cols(func, rdf, cols), _column_names(cols)) if isinstance(func, ROOT._cppyy.types.Instance): - return rdf._OriginalDefine(col_name, func, cols) + return rdf._OriginalDefine(col_name, func, _column_names(cols)) _WarnOnce.warn() jitter = FunctionJitter(rdf) diff --git a/bindings/pyroot/pythonizations/test/rdf_define_pyz.py b/bindings/pyroot/pythonizations/test/rdf_define_pyz.py index 0a45b47df71dc..ea217945a45c9 100644 --- a/bindings/pyroot/pythonizations/test/rdf_define_pyz.py +++ b/bindings/pyroot/pythonizations/test/rdf_define_pyz.py @@ -126,6 +126,15 @@ def test_cpp_free_function(self): """ test_cases = [ + # Free function without arguments (no column list given) + { + "name": "no_arguments", + "decl": "double my_free_function_no_args() { return 42.; }", + "coltype": "double", + "callable": lambda: ROOT.my_free_function_no_args, + "extract_fn": lambda x: x, + "expected_fn": lambda i: 42.0, + }, # Free function with arguments { "name": "input_ULong64_t", From 59ed9301f2125000c7be5992dcab3c1bc53b5871 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 00:24:31 +0530 Subject: [PATCH 60/71] [df][tutorials] Don't pass the number of parameters for TF1 formulas in df101.py The Python version of the tutorial builds the fit functions from formula strings, but passed the number of parameters as fifth argument like the C++ version does for its function pointers. For the formula constructor the fifth parameter is the `TF1::EAddToList` enum class though, and only the lenient int-to-enum conversion of the old bindings let this through. With the Clang-driven overload resolution of cppjit (as in C++) the call fails. Drop the argument: for a formula, the number of parameters is deduced from the formula itself. Assisted-by: claude-code:claude-opus-5-5 --- tutorials/analysis/dataframe/df101_h1Analysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tutorials/analysis/dataframe/df101_h1Analysis.py b/tutorials/analysis/dataframe/df101_h1Analysis.py index 96cb4ad8509a3..d8ef69a714614 100644 --- a/tutorials/analysis/dataframe/df101_h1Analysis.py +++ b/tutorials/analysis/dataframe/df101_h1Analysis.py @@ -63,7 +63,7 @@ def FitAndPlotHdmd(hdmd: ROOT.TH1): # Fit histogram hdmd with function f5 using the loglikelihood option formula = f"{dxbin} * ([0] * pow(x - 0.13957, [1]) + [2] / 2.5066 / [4] * exp(-{xp3} / 2 / [4] / [4]))" - f5 = ROOT.TF1("f5", f"{condition} ? {formula} : 0", 0.139, 0.17, 5) + f5 = ROOT.TF1("f5", f"{condition} ? {formula} : 0", 0.139, 0.17) f5.SetParameters(1000000, 0.25, 2000, 0.1454, 0.001) hdraw.Fit(f5, "lr") @@ -87,7 +87,7 @@ def FitAndPlotH2(h2: ROOT.TH2): formula = f"{dxbin} * ([0] * pow(x - 0.13957, 0.25) + [1] / 2.5066 / {sigma} * exp(-{xp3} / 2 / {sigma} / {sigma}))" print(f"TWO: {condition} ? {formula} : 0") - f2 = ROOT.TF1("f2", f"{condition} ? {formula} : 0", 0.139, 0.17, 2) + f2 = ROOT.TF1("f2", f"{condition} ? {formula} : 0", 0.139, 0.17) f2.SetParameters(10000, 10) h2.FitSlicesX(f2, 0, -1, 1, "qln") From b169a6273ecb08d863bcd9f3e13a42db1366141b Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 00:25:23 +0530 Subject: [PATCH 61/71] [RF][PyROOT] Import a list of objects into a RooWorkspace as a RooArgSet `w.Import([pdf, cat1, cat2])` relied on the old bindings converting the list to the `const RooArgSet&` of `RooWorkspace::import()`, as a braced list does in C++. With the Clang-driven overload resolution of cppjit the list is converted to a RooArgList beforehand (see the RooFit collection pythonizations), for which the derived-to-base conversion to `import(const TObject&)` is the better match, so the list itself was imported as a generic object. Convert a sequence given as first argument to a RooArgSet explicitly. Assisted-by: claude-code:claude-opus-5-5 --- .../ROOT/_pythonization/_roofit/_rooworkspace.py | 6 ++++++ bindings/pyroot/pythonizations/test/roofit.py | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooworkspace.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooworkspace.py index 7c3480a026f98..a5610556c5d89 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooworkspace.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooworkspace.py @@ -125,6 +125,12 @@ def RooWorkspace_import(self, *args, **kwargs): The keywords must correspond to the CmdArg of the `import()` function. """ # Redefinition of `RooWorkspace.import()` for keyword arguments. + import ROOT + + # A sequence of objects to import is a RooArgSet, like `import({a, b})` + # in C++. As a RooArgList, it would select the TObject overload instead. + if args and isinstance(args[0], (list, tuple)): + args = (ROOT.RooArgSet(args[0]),) + args[1:] args, kwargs = _kwargs_to_roocmdargs(*args, **kwargs) return self._import(*args, **kwargs) diff --git a/bindings/pyroot/pythonizations/test/roofit.py b/bindings/pyroot/pythonizations/test/roofit.py index 8ab36351b81c7..7a00e13f635a1 100644 --- a/bindings/pyroot/pythonizations/test/roofit.py +++ b/bindings/pyroot/pythonizations/test/roofit.py @@ -329,6 +329,17 @@ def test_python_collection_in_keyword_argument(self): gauss.plotOn(frame, VisualizeError=(res, [mean], 1)) self.assertEqual(frame.numItems(), 1) + def test_workspace_import_python_collection(self): + # A list of objects is imported as a RooArgSet of objects, not as a + # generic TObject. + x = ROOT.RooRealVar("x", "x", 0.0, 10.0) + gauss = ROOT.RooGaussian("gauss", "gauss", x, ROOT.RooFit.RooConst(5.0), ROOT.RooFit.RooConst(1.0)) + cat = ROOT.RooCategory("cat", "cat") + ws = ROOT.RooWorkspace("ws", "ws") + ws.Import([gauss, cat]) + self.assertEqual(ws.pdf("gauss").GetName(), "gauss") + self.assertEqual(ws.cat("cat").GetName(), "cat") + def test_python_collection_in_pythonized_function(self): # RooAbsPdf::generate() has several overloads taking a RooArgSet and a # second argument, so the list needs to be converted up-front. From 54335a555528d465f10f8188447a016b9df59a25 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 00:31:49 +0530 Subject: [PATCH 62/71] [PyROOT] Allow constructing RVec (and std::vector) from an iterator `RVec["int"](f(x) for x in v)` relied on the old bindings converting any iterable into the container. The Clang-driven overload resolution of cppjit has no C++ type for a generator, and cppjit only pythonizes the std::vector constructor for that. Turn an iterator given to the pythonized RVec and std::vector constructors into a list first. Assisted-by: claude-code:claude-opus-5-5 --- .../python/ROOT/_pythonization/_rvec.py | 21 ++++++++++++++----- bindings/pyroot/pythonizations/test/rvec.py | 4 ++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rvec.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rvec.py index 6e2fe3effec0f..51aef5755db01 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rvec.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rvec.py @@ -167,11 +167,22 @@ def _fill_from_numpy_array(self, arr): def _container_init(self, *args, **kwargs): - """Constructor of std::vector and RVec that also accepts a numpy array.""" - if len(args) == 1 and not kwargs and _is_numpy_array(args[0]): - self._original_init() - _fill_from_numpy_array(self, args[0]) - return + """Constructor of std::vector and RVec that also accepts a numpy array or + an iterator (e.g. a generator) of elements.""" + import collections.abc + + import ROOT + + if len(args) == 1 and not kwargs: + if _is_numpy_array(args[0]): + self._original_init() + _fill_from_numpy_array(self, args[0]) + return + if isinstance(args[0], collections.abc.Iterator) and not isinstance(args[0], ROOT._cppyy.types.Instance): + # an iterator has no C++ type in the overload resolution, unlike + # a list of its elements + self._original_init(list(args[0])) + return self._original_init(*args, **kwargs) diff --git a/bindings/pyroot/pythonizations/test/rvec.py b/bindings/pyroot/pythonizations/test/rvec.py index f33f397194b91..cbbc5ad123230 100644 --- a/bindings/pyroot/pythonizations/test/rvec.py +++ b/bindings/pyroot/pythonizations/test/rvec.py @@ -35,6 +35,10 @@ def test_from_root_namespace(self): v1 = ROOT.VecOps.RVec['float']() v2 = ROOT.RVec['float']() + def test_construction_from_generator(self): + """An RVec can be constructed from a generator of its elements""" + v = ROOT.VecOps.RVec["int"](i * i for i in range(4)) + self.assertEqual(list(v), [0, 1, 4, 9]) if __name__ == '__main__': unittest.main() From 917d20d9621089bd87220c6d613795131d289197 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 00:51:27 +0530 Subject: [PATCH 63/71] [hist][PyROOT] Pythonize RHist and RHistEngine construction from Python sequences `RHist["int"]([axis1, axis2])` and `RHist["int"](nbins, (low, high))` relied on the old bindings converting Python sequences to the `std::vector` and `std::pair` parameters. With the Clang-driven overload resolution of cppjit, the list selects the `std::initializer_list` constructor (as in C++), for which cppjit can't build a temporary initializer list since RAxisVariant isn't default constructible, and the tuple doesn't convert to a std::pair (see the corresponding cppjit issue about lists vs. braced-init-lists). Add constructor pythonizations for RHist and RHistEngine that convert a list of axes to a std::vector and an interval tuple to a std::pair. Assisted-by: claude-code:claude-opus-5-5 --- .../pythonizations/python/CMakeLists.txt | 1 + .../python/ROOT/_pythonization/_rhist.py | 54 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rhist.py diff --git a/bindings/pyroot/pythonizations/python/CMakeLists.txt b/bindings/pyroot/pythonizations/python/CMakeLists.txt index 75546ab384382..4a15c71fe1096 100644 --- a/bindings/pyroot/pythonizations/python/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/python/CMakeLists.txt @@ -91,6 +91,7 @@ set(py_sources ROOT/_pythonization/_memory_utils.py ROOT/_pythonization/_pyz_utils.py ROOT/_pythonization/_rfile.py + ROOT/_pythonization/_rhist.py ROOT/_pythonization/_rntuple.py ROOT/_pythonization/_runtime_error.py ROOT/_pythonization/_rvec.py diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rhist.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rhist.py new file mode 100644 index 0000000000000..5c741d64211cf --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rhist.py @@ -0,0 +1,54 @@ +# Author: Vipul Cariappa 10/2026 + +################################################################################ +# Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. # +# All rights reserved. # +# # +# For the licensing terms see $ROOTSYS/LICENSE. # +# For the list of contributors see $ROOTSYS/README/CREDITS. # +################################################################################ +from . import pythonization + + +def _axes_vector(axes): + """Convert a Python sequence of axes to a std::vector.""" + import ROOT + + vec = ROOT.std.vector["ROOT::Experimental::RAxisVariant"]() + vec.reserve(len(axes)) + for axis in axes: + vec.push_back(ROOT.Experimental.RAxisVariant(axis)) + return vec + + +def _RHist_constructor(self, *args, **kwargs): + """ + Allow constructing an RHist or RHistEngine from a Python list of axes, + and from a number of bins and an interval given as a Python tuple. + + In C++, `RHist({axis1, axis2})` uses the constructor taking an + `std::initializer_list`, but a Python list can't be turned + into a temporary initializer list of RAxisVariant, which is not default + constructible. Pass the axes as the equivalent std::vector instead. + Likewise, a Python tuple is not a braced-init-list for the overload + resolution, so the interval is converted to a std::pair explicitly. + """ + import ROOT + + if len(args) == 1 and not kwargs and isinstance(args[0], (list, tuple)): + args = (_axes_vector(args[0]),) + elif len(args) == 2 and not kwargs and isinstance(args[1], tuple) and len(args[1]) == 2: + args = (args[0], ROOT.std.pair["double", "double"](*args[1])) + self._cpp_constructor(*args, **kwargs) + + +@pythonization("RHist<", ns="ROOT::Experimental", is_prefix=True) +def pythonize_rhist(klass, name): + klass._cpp_constructor = klass.__init__ + klass.__init__ = _RHist_constructor + + +@pythonization("RHistEngine<", ns="ROOT::Experimental", is_prefix=True) +def pythonize_rhistengine(klass, name): + klass._cpp_constructor = klass.__init__ + klass.__init__ = _RHist_constructor From db9e6e8d7255839b33ac2d247441f0a240bf666b Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 00:52:09 +0530 Subject: [PATCH 64/71] [math][tutorials] Pass nullptr for the initial parameters in combinedFit.py `fitter.FitFCN(functor, 0, size, True)` relied on the old bindings accepting the Python integer 0 as the `const double* params` null pointer. With the Clang-driven overload resolution of cppjit a Python integer is an `int` value, not a null pointer constant like the literal 0 in C++ (see the corresponding cppjit issue), so no FitFCN overload matched. Say what is meant and pass `ROOT.nullptr`. Assisted-by: claude-code:claude-opus-5-5 --- tutorials/math/fit/combinedFit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/math/fit/combinedFit.py b/tutorials/math/fit/combinedFit.py index 0703c96b97ff1..2e307e9a3e53d 100644 --- a/tutorials/math/fit/combinedFit.py +++ b/tutorials/math/fit/combinedFit.py @@ -110,7 +110,7 @@ def __call__(self, par): # fit FCN function # (specify optionally data size and flag to indicate that is a chi2 fit) -fitter.FitFCN(globalChi2Functor, 0, dataB.Size() + dataSB.Size(), True) +fitter.FitFCN(globalChi2Functor, ROOT.nullptr, dataB.Size() + dataSB.Size(), True) result = fitter.Result() result.Print(ROOT.std.cout) From d0126dedeba529e010e6abc604e9060dc2371dc2 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 01:03:12 +0530 Subject: [PATCH 65/71] [df][DistRDF] Merge the RMergeableValues, not the unique_ptrs holding them `merge_values` passed the `std::unique_ptr>` proxies to `MergeValues`. With the Clang-driven overload resolution of cppjit, an lvalue unique_ptr selects the `MergeValues(std::unique_ptr<...>, ...)` overload taking it by value (as it would in C++, where the call then fails on the deleted copy constructor), and cppjit moved the mergeable out of the Python proxy, so the merged value was lost ("no C++ object available"). Pass the objects the smart pointers point to, which selects the overload merging into the output in place, as intended. Assisted-by: claude-code:claude-opus-5-5 --- bindings/distrdf/python/DistRDF/Backends/Utils.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/bindings/distrdf/python/DistRDF/Backends/Utils.py b/bindings/distrdf/python/DistRDF/Backends/Utils.py index 6aa8d0a060cc2..2a6df2a0dc32c 100644 --- a/bindings/distrdf/python/DistRDF/Backends/Utils.py +++ b/bindings/distrdf/python/DistRDF/Backends/Utils.py @@ -257,7 +257,17 @@ def merge_values(mergeable_out, mergeable_in): Generally the arguments are `RMergeableValue` instances that can be directly passed to the C++ function responsible for merging them. """ - ROOT.Detail.RDF.MergeValues(mergeable_out, mergeable_in) + # The mergeables are usually held by std::unique_ptrs: pass the objects + # themselves, as passing the smart pointers would select the MergeValues + # overload that takes (and thus moves) them by value. + ROOT.Detail.RDF.MergeValues(_pointee(mergeable_out), _pointee(mergeable_in)) + + +def _pointee(obj): + """Return the object a smart pointer proxy points to, or the object itself.""" + smartptr = getattr(obj, "__smartptr__", None) + smartptr = smartptr() if smartptr is not None else None + return smartptr.get() if smartptr else obj @merge_values.register(AsNumpyResult) From ff6315fef2c206e4a75f5073adc33e464082d78d Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 01:15:30 +0530 Subject: [PATCH 66/71] [RF][PyROOT] Convert lists of RooFit objects in table(), get() and ModelConfig setters `data.table([cat])`, `hist.get([x, y])` and `modelConfig.SetParametersOfInterest([poi])` (and the other ModelConfig setters) relied on the old bindings converting a Python list into the `const RooArgSet&` parameter. These methods have other overloads of the same arity (taking a `const RooAbsCategory&`, an index, or a `const char*` list of names), so with the Clang-driven overload resolution of cppjit, which doesn't treat a list like a braced-init-list (see the corresponding cppjit issue), they don't resolve anymore. Pythonize them to convert lists and tuples of RooFit objects to a RooArgList and sets to a RooArgSet first. Assisted-by: claude-code:claude-opus-5-5 --- .../_pythonization/_roofit/_rooabsdata.py | 8 ++++- .../_pythonization/_roofit/_roodatahist.py | 8 ++++- .../ROOT/_pythonization/_roofit/_roostats.py | 35 +++++++++++++++++-- bindings/pyroot/pythonizations/test/roofit.py | 20 +++++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooabsdata.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooabsdata.py index 5289d36162d2e..563185f42ccf8 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooabsdata.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooabsdata.py @@ -12,7 +12,7 @@ ################################################################################ -from ._utils import _kwargs_to_roocmdargs, cpp_signature +from ._utils import _kwargs_to_roocmdargs, _sequences_to_roo_collections, cpp_signature class RooAbsData(object): @@ -30,6 +30,12 @@ class RooAbsData(object): __cpp_name__ = 'RooAbsData' + def table(self, *args, **kwargs): + r"""The RooAbsData::table function accepts a Python sequence of + categories for its `const RooArgSet&` parameter. + """ + return self._table(*_sequences_to_roo_collections(args), **kwargs) + @cpp_signature( "RooPlot *RooAbsData::plotOn(RooPlot* frame," " const RooCmdArg& arg1={}, const RooCmdArg& arg2={}," diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_roodatahist.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_roodatahist.py index c6f1140c0d7e5..3bc63f0559c3d 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_roodatahist.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_roodatahist.py @@ -11,7 +11,7 @@ ################################################################################ -from ._utils import _dict_to_flat_map, _kwargs_to_roocmdargs, cpp_signature +from ._utils import _dict_to_flat_map, _kwargs_to_roocmdargs, _sequences_to_roo_collections, cpp_signature class RooDataHist(object): @@ -47,6 +47,12 @@ def __init__(self, *args, **kwargs): args, kwargs = _kwargs_to_roocmdargs(*args, **kwargs) self._init(*args, **kwargs) + def get(self, *args, **kwargs): + r"""The RooDataHist::get function accepts a Python sequence of + coordinates for its `const RooArgSet&` parameter. + """ + return self._get(*_sequences_to_roo_collections(args), **kwargs) + @property def shape(self): shape = [] diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_roostats.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_roostats.py index 5824632f3b92d..f5c573520448d 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_roostats.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_roostats.py @@ -11,7 +11,7 @@ ################################################################################ from .. import pythonization -from ._utils import _kwargs_to_roocmdargs, cpp_signature +from ._utils import _kwargs_to_roocmdargs, _sequences_to_roo_collections, cpp_signature class SPlot(object): @@ -89,4 +89,35 @@ def pythonize_splot(klass): klass.__init__ = SPlot._SPlot_init klass._AddSWeight = klass.AddSWeight - klass.AddSWeight = SPlot._SPlot_AddSWeight \ No newline at end of file + klass.AddSWeight = SPlot._SPlot_AddSWeight + + +_modelconfig_set_methods = [ + "SetParameters", + "SetParametersOfInterest", + "SetNuisanceParameters", + "SetConstraintParameters", + "SetObservables", + "SetConditionalObservables", + "SetGlobalObservables", + "SetExternalConstraints", + "SetSnapshot", +] + + +def _modelconfig_set_method(cpp_method): + def set_method(self, *args, **kwargs): + r"""Convert a Python sequence of RooFit objects to the RooArgSet of the + C++ method, which the overload resolution doesn't do for a Python list. + """ + return cpp_method(self, *_sequences_to_roo_collections(args), **kwargs) + + set_method.__doc__ = cpp_method.__doc__ + return set_method + + +@pythonization("ModelConfig", ns="RooStats") +def pythonize_modelconfig(klass): + for name in _modelconfig_set_methods: + if hasattr(klass, name): + setattr(klass, name, _modelconfig_set_method(getattr(klass, name))) diff --git a/bindings/pyroot/pythonizations/test/roofit.py b/bindings/pyroot/pythonizations/test/roofit.py index 7a00e13f635a1..f1e8b0a5fda99 100644 --- a/bindings/pyroot/pythonizations/test/roofit.py +++ b/bindings/pyroot/pythonizations/test/roofit.py @@ -340,6 +340,26 @@ def test_workspace_import_python_collection(self): self.assertEqual(ws.pdf("gauss").GetName(), "gauss") self.assertEqual(ws.cat("cat").GetName(), "cat") + def test_python_collection_in_methods(self): + # Methods taking a `const RooArgSet&` that are overloaded with other + # signatures of the same arity. + x = ROOT.RooRealVar("x", "x", 0.0, 10.0) + cat = ROOT.RooCategory("cat", "cat") + cat.defineType("a") + cat.defineType("b") + data = ROOT.RooDataSet("data", "data", [x, cat]) + data.add([x, cat]) + self.assertIsInstance(data.table([cat]), ROOT.Roo1DTable) + + hist = ROOT.RooDataHist("hist", "hist", [x]) + self.assertEqual(hist.get([x]).size(), 1) + + mc = ROOT.RooStats.ModelConfig("mc") + ws = ROOT.RooWorkspace("ws_modelconfig", "ws_modelconfig") + mc.SetWorkspace(ws) + mc.SetParametersOfInterest([x]) + self.assertEqual(mc.GetParametersOfInterest().size(), 1) + def test_python_collection_in_pythonized_function(self): # RooAbsPdf::generate() has several overloads taking a RooArgSet and a # second argument, so the list needs to be converted up-front. From b042b9fddf7ee671ac774d665aef09b7a5b22641 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 02:26:26 +0530 Subject: [PATCH 67/71] [PyROOT] Select the const char* overloads of TText for Python strings TText has `const char*` and `const wchar_t*` overloads of its constructor and of DrawText(), DrawTextNDC(), SetText(), PaintText() and PaintTextNDC(). With the Clang-driven overload resolution of cppjit, a Python string is promoted to both pointer types and the call is reported as ambiguous, while in C++ a narrow string literal selects the `const char*` overload (see the corresponding cppjit issue). This broke e.g. `ROOT.TText(2, 100, "Signal")` in the rf106 tutorial. Pythonize these methods to select the `const char*` overload explicitly for a Python string. Assisted-by: claude-code:claude-opus-5-5 --- .../pythonizations/python/CMakeLists.txt | 1 + .../python/ROOT/_pythonization/_ttext.py | 48 +++++++++++++++++++ .../pyroot/pythonizations/test/misc_pyz.py | 17 +++++++ 3 files changed, 66 insertions(+) create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_ttext.py diff --git a/bindings/pyroot/pythonizations/python/CMakeLists.txt b/bindings/pyroot/pythonizations/python/CMakeLists.txt index 4a15c71fe1096..4e236695ea86f 100644 --- a/bindings/pyroot/pythonizations/python/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/python/CMakeLists.txt @@ -131,6 +131,7 @@ set(py_sources ROOT/_pythonization/_tseqcollection.py ROOT/_pythonization/_tstring.py ROOT/_pythonization/_tstyle.py + ROOT/_pythonization/_ttext.py ROOT/_pythonization/_ttree.py ROOT/_pythonization/_tvector3.py ROOT/_pythonization/_tvectort.py diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_ttext.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_ttext.py new file mode 100644 index 0000000000000..7f2fb66ee159a --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_ttext.py @@ -0,0 +1,48 @@ +# Author: Vipul Cariappa 10/2026 + +################################################################################ +# Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. # +# All rights reserved. # +# # +# For the licensing terms see $ROOTSYS/LICENSE. # +# For the list of contributors see $ROOTSYS/README/CREDITS. # +################################################################################ +from . import pythonization + +# Methods of TText that have overloads taking the text as `const char*` and as +# `const wchar_t*`, for which a Python string would be ambiguous: in C++, a +# (narrow) string literal selects the `const char*` overload. +_text_methods = ["DrawText", "DrawTextNDC", "SetText", "PaintText", "PaintTextNDC"] +_text_signature = "Double_t, Double_t, const char*" + + +def _select_char_overload(cpp_method, name): + def method(self, *args, **kwargs): + if len(args) == 3 and not kwargs and isinstance(args[2], str): + return getattr(self, "_" + name).__overload__(_text_signature)(*args) + return getattr(self, "_" + name)(*args, **kwargs) + + method.__doc__ = cpp_method.__doc__ + return method + + +def _TText_constructor(self, *args, **kwargs): + """ + Construct a TText from a Python string with the `const char*` + constructor rather than the `const wchar_t*` one, like a string literal + does in C++. + """ + if len(args) == 3 and not kwargs and isinstance(args[2], str): + return type(self)._cpp_constructor.__overload__(_text_signature)(self, *args) + return self._cpp_constructor(*args, **kwargs) + + +@pythonization("TText") +def pythonize_ttext(klass): + klass._cpp_constructor = klass.__init__ + klass.__init__ = _TText_constructor + for name in _text_methods: + if hasattr(klass, name): + cpp_method = getattr(klass, name) + setattr(klass, "_" + name, cpp_method) + setattr(klass, name, _select_char_overload(cpp_method, name)) diff --git a/bindings/pyroot/pythonizations/test/misc_pyz.py b/bindings/pyroot/pythonizations/test/misc_pyz.py index ea99554ad8403..ef01ce0babb44 100644 --- a/bindings/pyroot/pythonizations/test/misc_pyz.py +++ b/bindings/pyroot/pythonizations/test/misc_pyz.py @@ -162,6 +162,23 @@ def test_float_and_int_components(self): c_int = ROOT.TColor.GetColor(255, 128, 0) self.assertEqual(ROOT.TColor.GetColor("#ff8000"), c_int) +class TTextStringOverloads(unittest.TestCase): + """ + A Python string selects the `const char*` overloads of TText, which also + has `const wchar_t*` ones + """ + + def test_text(self): + t = ROOT.TText(0.5, 0.5, "Signal") + self.assertEqual(t.GetTitle(), "Signal") + t.SetText(0.1, 0.2, "Background") + self.assertEqual(t.GetTitle(), "Background") + self.assertEqual(t.GetX(), 0.1) + + def test_latex(self): + latex = ROOT.TLatex(0.5, 0.5, "#alpha") + self.assertEqual(latex.GetTitle(), "#alpha") + class TestTComplexOperators(unittest.TestCase): """ Test for the operators of TComplex: From 2cdf7bb6d07b2e223278c83b4f8b6e2f9fd86305 Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 02:27:12 +0530 Subject: [PATCH 68/71] [PyROOT][roottest] Select the make_any instantiation taking a T* explicitly `std.make_any["Derived1*", "Derived1*"](obj)` passes an object where the `Derived1*&&` parameter of that instantiation needs a pointer, which is ill-formed in C++ (`make_any(obj)`), and only worked with the lenient old bindings. With the Clang-driven overload resolution of cppjit a proxy is spelled as the object, and there is no Python expression that is spelled as a pointer to it (see the corresponding cppjit issue). Select the instantiation by its signature instead, which passes the object's address like `make_any(&obj)` in C++. Assisted-by: claude-code:claude-opus-5-5 --- roottest/python/cling/PyClassTest4.C | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/roottest/python/cling/PyClassTest4.C b/roottest/python/cling/PyClassTest4.C index 66e6e5cc1a004..278d65b837c61 100644 --- a/roottest/python/cling/PyClassTest4.C +++ b/roottest/python/cling/PyClassTest4.C @@ -6,11 +6,13 @@ void PyClassTest4() // python classes live in "MyPyClass.py" and "MyModule.py", which must have been // loaded in a separate line or macro, or this one won't compile - // test access to C++ classes derived from python classes + // test access to C++ classes derived from python classes; make_any(&val) + // in C++, which is spelled by selecting the instantiation taking a T*: a + // Python proxy of an object is not a pointer for the overload resolution std::any result; - TPython::Exec(R"(val = ROOT.Derived1( 42 ); _anyresult = ROOT.std.make_any["Derived1*", "Derived1*"](val))", &result); + TPython::Exec(R"(val = ROOT.Derived1( 42 ); _anyresult = ROOT.std.make_any.__overload__("Derived1*&&", "Derived1*, Derived1*")(val))", &result); std::cout << "Derived1 (42): " << std::any_cast(result)->fVal << std::endl; - TPython::Exec(R"(val = ROOT.Derived2( 5.0 ); _anyresult = ROOT.std.make_any["Derived2*", "Derived2*"](val))", &result); + TPython::Exec(R"(val = ROOT.Derived2( 5.0 ); _anyresult = ROOT.std.make_any.__overload__("Derived2*&&", "Derived2*, Derived2*")(val))", &result); std::cout << "Derived2 (5.0): " << std::any_cast(result)->fVal << std::endl; } From 56a298e3fe52b7b3fa1e4a24324b0496491128bf Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Sat, 17 Oct 2026 02:57:31 +0530 Subject: [PATCH 69/71] [df][PyROOT] Construct HistoND/HistoNSparseD models from tuples like for other histograms `df.HistoND(("name", "title", dim, nbins, xmin, xmax), cols)` (and the same for HistoNSparseD) relied on the old bindings converting the tuple into the `const THnDModel&` / `const THnSparseDModel&` parameter. The Clang-driven overload resolution of cppjit spells a tuple as an `std::initializer_list` of its first element's type rather than a braced-init-list (see the corresponding cppjit issue), so it no longer resolves. Add both methods to those whose tuple model argument is turned into a model object, like Histo1D/2D/3D and Profile1D/2D. Assisted-by: claude-code:claude-opus-5-5 --- .../python/ROOT/_pythonization/_rdataframe.py | 2 ++ bindings/pyroot/pythonizations/test/rdataframe_misc.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rdataframe.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rdataframe.py index ee50e05e7529f..f76d6c17a65e5 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rdataframe.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rdataframe.py @@ -525,6 +525,8 @@ def pythonize_rdataframe(klass): "Histo3D": RDF.TH3DModel, "Profile1D": RDF.TProfile1DModel, "Profile2D": RDF.TProfile2DModel, + "HistoND": RDF.THnDModel, + "HistoNSparseD": RDF.THnSparseDModel, } for method_name, model_class in methods_with_TModel.items(): diff --git a/bindings/pyroot/pythonizations/test/rdataframe_misc.py b/bindings/pyroot/pythonizations/test/rdataframe_misc.py index 900e5a384a52d..4b86300946312 100644 --- a/bindings/pyroot/pythonizations/test/rdataframe_misc.py +++ b/bindings/pyroot/pythonizations/test/rdataframe_misc.py @@ -106,6 +106,14 @@ def test_rungraphs_with_list(self): self.assertEqual(ROOT.RDF.RunGraphs([h, s]), 1) self.assertEqual(s.GetValue(), 10.0) + def test_histond_model_from_tuple(self): + """ + The model of HistoND can be given as a tuple of constructor arguments + """ + rdf = ROOT.RDataFrame(10).Define("x0", "1.").Define("x1", "2.") + h = rdf.HistoND(("name", "title", 2, [4, 4], [0.0, 0.0], [5.0, 5.0]), ("x0", "x1")) + self.assertEqual(h.GetEntries(), 10) + def _test_rdf_in_function(self, chain): rdf = ROOT.RDataFrame(chain) meanx = rdf.Mean("x") From d363401c96f26eceb880937fd890bc65941b90ea Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Wed, 21 Oct 2026 16:21:57 +0530 Subject: [PATCH 70/71] [ntuple][PyROOT] Pass the entry itself to RNTupleWriter.Fill and LoadEntry `CreateEntry()` returns a `std::unique_ptr`, and the pythonized `RNTupleWriter.Fill(entry)` and `RNTupleReader.LoadEntry(index, entry)` passed it on to the C++ methods taking `REntry&`. As in C++, a smart pointer doesn't bind to a reference to the object it points to, so with the new overload resolution `Fill` found no viable overload, and `LoadEntry` only worked through the argument-count fallback. Pass the entry the smart pointer points to (`__smartptr__().get()`). Assisted-by: claude-code:claude-opus-5-5 --- .../python/ROOT/_pythonization/_rntuple.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rntuple.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rntuple.py index abb435adbe1b0..bb6b31e7ec817 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rntuple.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rntuple.py @@ -205,6 +205,16 @@ def __exit__(self, *args): return False +def _entry_ref(entry): + # CreateEntry() returns a std::unique_ptr, which (as in C++) doesn't + # bind to the REntry& parameters of Fill() and LoadEntry(): pass the entry + # it points to + smartptr = getattr(entry, "__smartptr__", None) + if smartptr is not None and smartptr() is not None: + return smartptr().get() + return entry + + def _RNTupleReader_Open(maybe_model, *args): if hasattr(type(maybe_model), "__cpp_name__") and type(maybe_model).__cpp_name__ == "ROOT::RNTupleModel": # In Python, the user cannot create REntries directly from a model, so we can safely clone it and avoid destructively passing the user argument. @@ -217,7 +227,7 @@ def _RNTupleReader_Open(maybe_model, *args): def _RNTupleReader_LoadEntry(self, *args): if len(args) < 2: raise ValueError("default entries are not supported in Python, pass explicit entry") - return self._LoadEntry(*args) + return self._LoadEntry(args[0], _entry_ref(args[1]), *args[2:]) @pythonization("RNTupleReader", ns="ROOT") @@ -249,7 +259,7 @@ def _RNTupleWriter_Recreate(model_or_fields, *args): def _RNTupleWriter_Fill(self, *args): if len(args) < 1: raise ValueError("default entries are not supported in Python, pass explicit entry") - return self._Fill(*args) + return self._Fill(_entry_ref(args[0]), *args[1:]) def _RNTupleWriter_exit(self): From 297d7a4d7a7359377420285360db303e7550c79a Mon Sep 17 00:00:00 2001 From: Vipul Cariappa Date: Wed, 21 Oct 2026 19:44:19 +0530 Subject: [PATCH 71/71] [PyROOT] Append Python buffers to std::vector with += in a pythonization `vec += buf` with a Python buffer (array.array, numpy array) went through cppjit's fast `__iadd__`, which called `insert(end(), buf)`. No `vector::insert` overload takes a buffer; the old bindings converted it into an initializer_list, which cppjit deliberately doesn't do (it would be a hidden deep copy), so `+=` failed. Appending with `+=` is explicit though, so pythonize `std::vector.__iadd__` to copy the elements of a Python buffer, reusing the copy from the construction of a std::vector or RVec from a numpy array (now appending, with an element-wise fallback for vectors like std::vector whose `data()` has no address). Assisted-by: claude-code:claude-opus-5-5 --- .../python/ROOT/_pythonization/_rvec.py | 17 ++++++-- .../python/ROOT/_pythonization/_stl_vector.py | 43 ++++++++++++++++++- bindings/pyroot/pythonizations/test/stl.py | 4 ++ 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rvec.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rvec.py index 51aef5755db01..182e02616d67b 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rvec.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rvec.py @@ -137,8 +137,8 @@ def _is_numpy_array(obj): def _fill_from_numpy_array(self, arr): - """Copy the elements of the numpy array arr into the empty std::vector or - RVec self. + """Append copies of the elements of the numpy array arr to the std::vector + or RVec self. The overload resolution does not convert a numpy array into a container implicitly, as that would be a hidden deep copy. Constructing a container @@ -158,9 +158,18 @@ def _fill_from_numpy_array(self, arr): if kind in ("i", "u", "f"): dtype = np.dtype("{}{}".format(kind, ROOT._cppyy.sizeof(value_type))) src = np.ascontiguousarray(flat, dtype=dtype) - self.resize(src.size) + offset = self.size() + self.resize(offset + src.size) if src.size: - ctypes.memmove(ROOT._cppyy.ll.addressof(self.data()), src.ctypes.data, src.nbytes) + try: + dst = ROOT._cppyy.ll.addressof(self.data()) + offset * dtype.itemsize + except TypeError: + # no address for the data, e.g. std::vector::data() is + # converted to a Python str + for i, elem in enumerate(src.tolist()): + self[offset + i] = elem + else: + ctypes.memmove(dst, src.ctypes.data, src.nbytes) else: for elem in flat.tolist(): self.push_back(elem) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_stl_vector.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_stl_vector.py index 55a1ee25ccbdd..15dfaa193f545 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_stl_vector.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_stl_vector.py @@ -11,7 +11,7 @@ import sys from . import pythonization -from ._rvec import _add_numpy_array_constructor, _array_interface_dtype_map +from ._rvec import _add_numpy_array_constructor, _array_interface_dtype_map, _fill_from_numpy_array, _is_numpy_array def _data_vec_char(self): @@ -50,6 +50,42 @@ def _get_array_interface(self): } +def _is_python_buffer(obj): + """Check if obj is a Python object exposing a buffer (e.g. array.array or a + numpy array), other than strings and C++ objects.""" + import ROOT + + if isinstance(obj, (str, bytes, bytearray)) or isinstance(obj, ROOT._cppyy.types.Instance): + return False + try: + memoryview(obj).release() + except TypeError: + return False + return True + + +def _vector_iadd(self, other): + """vec += buffer: append copies of the elements of a Python buffer. + + The overload resolution of the bindings does not convert a buffer into a + container (or an initializer list) implicitly, as that would be a hidden + deep copy, so std::vector::insert() can't take it. Appending with += is + explicit, so the elements are copied here. + """ + if not _is_python_buffer(other): + return self._original_iadd(other) + if not _is_numpy_array(other): + try: + import numpy as np + except ImportError: + for elem in memoryview(other).tolist(): + self.push_back(elem) + return self + other = np.asarray(other) + _fill_from_numpy_array(self, other) + return self + + def _add_array_interface_property(klass): value_type = getattr(klass, "value_type", None) if value_type in _array_interface_dtype_map: @@ -67,6 +103,11 @@ def pythonize_stl_vector(klass, name): # Allow the construction from a numpy array (by copy) _add_numpy_array_constructor(klass) + # Allow appending the elements of a Python buffer (by copy) with += + if hasattr(klass, "__iadd__"): + klass._original_iadd = klass.__iadd__ + klass.__iadd__ = _vector_iadd + # Inject custom vector::data() value_type = getattr(klass, 'value_type', None) if value_type == 'char': diff --git a/bindings/pyroot/pythonizations/test/stl.py b/bindings/pyroot/pythonizations/test/stl.py index a2b587666fbc8..a8c9d4d5ad81f 100644 --- a/bindings/pyroot/pythonizations/test/stl.py +++ b/bindings/pyroot/pythonizations/test/stl.py @@ -189,6 +189,10 @@ def test_stl_vector_from_numpy(self): self.assertEqual(list(ROOT.VecOps.RVec["signed char"](np.array([1, 2], dtype=np.int8))), [1, 2]) self.assertEqual(len(ROOT.VecOps.RVec["std::string"](np.array(["a", "b"]))), 2) + # appending with += copies too + v += np.array([4, 5]) + self.assertEqual(list(v), [0.0, 1.0, 2.0, 3.0, 4.0, 5.0]) + class STL_set(unittest.TestCase): """ Tests for the pythonizations of std::set.