Skip to content

[WIP] [cppjit] New overload resolution - #23715

Draft
Vipul-Cariappa wants to merge 71 commits into
root-project:masterfrom
Vipul-Cariappa:dev/overload-resolution
Draft

Vipul-Cariappa wants to merge 71 commits into
root-project:masterfrom
Vipul-Cariappa:dev/overload-resolution

Conversation

@Vipul-Cariappa

Copy link
Copy Markdown
Contributor

The new overload resolution matches more closely to the overload resolution in C++. This should resolve multiple open ROOT issues.

Vipul-Cariappa and others added 17 commits October 14, 2026 16:36
…nOperator` (root-project#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)
…s 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)
(cherry picked from commit 76c198c)
…ist (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)
…Alias() (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:
wlav/CPyCppyy#27

(cherry picked from commit edb749b)
…er-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<long> with its std::less<long> and
std::allocator<long> 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 <set>\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)
…ler-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#19017 (commit
700fee6), 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 552d291. 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<double>` 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)
(cherry picked from commit a57f54a)
…valent

`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)
(cherry picked from commit e24c182)
* 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 <regex> 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 <regex> 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)
(cherry picked from commit 425cada)
…test

BestOverloadFunctionMatch now takes an out-parameter for ambiguous
candidates. Update the ROOT-only FunctionReflection_TemplatedOperatorArrow
test to the new signature.
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Test Results

    21 files      21 suites   3d 6h 26m 22s ⏱️
 3 871 tests  3 837 ✅ 0 💤  34 ❌
66 107 runs  65 631 ✅ 6 💤 470 ❌

For more details on these failures, see this check.

Results for commit 297d7a4.

♻️ This comment has been updated with latest results.

`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
`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
`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
…intee

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<A>&, 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<RDataFrame>`
returned by `ROOT.RDF.FromNumpy()`, and `RFile.Put()`.

Assisted-by: claude-code:claude-opus-5-5
…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
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
`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<T>` parameter of a candidate.

Assisted-by: claude-code:claude-opus-5-5
`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
When the overload resolution selects a function taking `T&&` for a proxy
of a class U that converts to T (e.g. `std::vector<Variant>::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
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
…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
…structors

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 <class Func> 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
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<std::string>` constructor directly, so the actual
C++ exception surfaces, like it already did for an explicit
`std::vector<std::string>`.

Assisted-by: claude-code:claude-opus-5-5
The old bindings implicitly converted any iterable into a
`std::vector<std::string>`, 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.RDF.RunGraphs([h1, h2])` relied on the old bindings converting a
Python list into the `std::vector<RResultHandle>` 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
`MakeCSVDataFrame(..., colTypes={"col": "T"})` assigned the Python dict
to the `std::unordered_map<std::string, char>` 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
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
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 <class T> 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
`tree.Branch("folder", 0)` fails with the Clang-driven overload resolution
of cppjit: the argument promotions make it consider the
`template <class T> 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
…eir pointer

The Branch() pythonization hands an object proxy to the
`BranchPtrToPtr<T>(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
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
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
…ject

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
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
`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
`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
…thonizations

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<std::string>`
explicitly.

Assisted-by: claude-code:claude-opus-5-5
…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
`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
`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
…on sequences

`RHist["int"]([axis1, axis2])` and `RHist["int"](nbins, (low, high))`
relied on the old bindings converting Python sequences to the
`std::vector<RAxisVariant>` and `std::pair<double, double>` parameters.
With the Clang-driven overload resolution of cppjit, the list selects the
`std::initializer_list<RAxisVariant>` 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<RAxisVariant> and an interval tuple to a
std::pair.

Assisted-by: claude-code:claude-opus-5-5
…Fit.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
… them

`merge_values` passed the `std::unique_ptr<RMergeableValue<T>>` 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
…delConfig 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
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
…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<Derived1*, Derived1*>(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<Derived1*>(&obj)` in C++.

Assisted-by: claude-code:claude-opus-5-5
…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
…Entry

`CreateEntry()` returns a `std::unique_ptr<REntry>`, 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
`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<char> whose
`data()` has no address).

Assisted-by: claude-code:claude-opus-5-5
@guitargeek

Copy link
Copy Markdown
Contributor

Thanks a lot @Vipul-Cariappa for opening the PR! That's really a great help to see where we stand. Given that you needed tons of new Pythonizations to make ROOT interfaces behave as expected, I think getting this into ROOT 6.42 is unfortunately very risky. The ROOT Python interface powered by cppyy (now cppjit) is used by tons of user and experiment code for their own C++ libraries, and just as ROOT has to paper over the behavior changes in cppjit with Python-side Pythonizations, so would our users. We can't expect this from them at this point, and I'd argue even less so in a release that already risks disrupting user code with the migration from cppyy and cppjit. My conclusion from this excellent R & D PR is therefore that the new overload resolution is something we should target as a 6.44 highlight, and not as a last-minute inclusion to 6.42 before branching, and then releasing in November. That would nicely factor the internal move to cppjit from user-facing behavior changes that follow up, which would also make our life easier when fixing regressions.

@vgvassilev

Copy link
Copy Markdown
Member

Thanks a lot @Vipul-Cariappa for opening the PR! That's really a great help to see where we stand. Given that you needed tons of new Pythonizations to make ROOT interfaces behave as expected, I think getting this into ROOT 6.42 is unfortunately very risky. The ROOT Python interface powered by cppyy (now cppjit) is used by tons of user and experiment code for their own C++ libraries, and just as ROOT has to paper over the behavior changes in cppjit with Python-side Pythonizations, so would our users. We can't expect this from them at this point, and I'd argue even less so in a release that already risks disrupting user code with the migration from cppyy and cppjit. My conclusion from this excellent R & D PR is therefore that the new overload resolution is something we should target as a 6.44 highlight, and not as a last-minute inclusion to 6.42 before branching, and then releasing in November. That would nicely factor the internal move to cppjit from user-facing behavior changes that follow up, which would also make our life easier when fixing regressions.

I agree -- the code changes are more than I was hoping for. @Vipul-Cariappa, can we make sure we get the this PR to a green state while we are at it?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants