Skip to content

[WIP] New Overload Resolution - #150

Draft
Vipul-Cariappa wants to merge 27 commits into
compiler-research:mainfrom
Vipul-Cariappa:dev/overload-resolution
Draft

Vipul-Cariappa wants to merge 27 commits into
compiler-research:mainfrom
Vipul-Cariappa:dev/overload-resolution

Conversation

@Vipul-Cariappa

Copy link
Copy Markdown
Collaborator

No description provided.

Vipul-Cariappa and others added 20 commits October 21, 2026 19:01
`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.
* 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
`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
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
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
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
`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<char>`, `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
`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<CustomStruct>"]`
in ROOT's REntry pythonization, called before the dictionary of that
class was loaded, instantiated `REntry::GetPtr<UnknownType>`. 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
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
`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
@Vipul-Cariappa
Vipul-Cariappa force-pushed the dev/overload-resolution branch from 425cada to 05090fc Compare October 21, 2026 13:46

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.

2 participants