diff --git a/docs/config/autowrap.rst b/docs/config/autowrap.rst index b72394e..adae365 100644 --- a/docs/config/autowrap.rst +++ b/docs/config/autowrap.rst @@ -96,6 +96,38 @@ Semiwrap emits ``py::kw_only()`` before the first exposed argument that retains a default. An argument with ``param_override.no_default: true`` does not trigger the keyword-only marker. +Overload binding order +---------------------- + +When multiple overloads can accept the same Python arguments, pybind11 tries +matching overloads in registration order within each overload-resolution pass. +Use ``binding_order`` to register a preferred overload earlier: + +.. code-block:: yaml + + classes: + MyClass: + methods: + my_method: + overloads: + SomeType: + binding_order: -1 + OtherType: + +Lower integers bind first. The default is ``0``, so negative values move an +overload earlier and positive values move it later. Equal values preserve the +existing binding order; the order of entries in YAML does not affect it. + +The setting also works for constructors, static and exposed protected methods, +and free functions under ``functions``. It can be set on a function or method +as a default for its overloads; an overload's explicit value (including ``0``) +overrides that default. + +Ordering applies within a class (including public and exposed protected methods) +or among free functions generated from one header. It does not reorder classes, +headers, or handwritten ``inline_code`` bindings, and does not change pybind11's +preference for matches that require no argument conversion. + Name transforms --------------- diff --git a/src/semiwrap/autowrap/context.py b/src/semiwrap/autowrap/context.py index ee12f59..4589d0d 100644 --- a/src/semiwrap/autowrap/context.py +++ b/src/semiwrap/autowrap/context.py @@ -227,6 +227,9 @@ class FunctionContext: # User settings from autowrap_yml.FunctionData # + #: Lower integers are registered first; ties preserve existing order + binding_order: int + #: If True, don't wrap this, but provide a pure virtual implementation ignore_pure: bool diff --git a/src/semiwrap/autowrap/cxxparser.py b/src/semiwrap/autowrap/cxxparser.py index daa5ad2..a02cc1f 100644 --- a/src/semiwrap/autowrap/cxxparser.py +++ b/src/semiwrap/autowrap/cxxparser.py @@ -1575,6 +1575,7 @@ def _on_fn_or_method( # info # vararg=fn.vararg, # user settings + binding_order=data.binding_order if data.binding_order is not None else 0, ignore_pure=data.ignore_pure, ignore_py=data.ignore_py, cpp_code=data.cpp_code, diff --git a/src/semiwrap/autowrap/render_pybind11.py b/src/semiwrap/autowrap/render_pybind11.py index a1ab089..110c8dd 100644 --- a/src/semiwrap/autowrap/render_pybind11.py +++ b/src/semiwrap/autowrap/render_pybind11.py @@ -440,12 +440,14 @@ def cls_def(r: RenderBuffer, cls: ClassContext, varname: str): if cls.add_default_constructor: r.writeln(f"{varname}.def(py::init<>(), release_gil());") - for fn in cls.wrapped_public_methods: - genmethod(r, varname, cls.full_cpp_name, fn, None) - + methods: T.List[T.Tuple[FunctionContext, T.Optional[str]]] = [ + (fn, None) for fn in cls.wrapped_public_methods + ] if cls.trampoline is not None: - for fn in cls.wrapped_protected_methods: - genmethod(r, varname, cls.full_cpp_name, fn, cls.trampoline.var) + methods.extend((fn, cls.trampoline.var) for fn in cls.wrapped_protected_methods) + + for fn, trampoline in sorted(methods, key=lambda method: method[0].binding_order): + genmethod(r, varname, cls.full_cpp_name, fn, trampoline) for prop in cls.public_properties: _genprop(r, varname, cls.full_cpp_name, prop) diff --git a/src/semiwrap/autowrap/render_wrapped.py b/src/semiwrap/autowrap/render_wrapped.py index eab0faa..dac9cc8 100644 --- a/src/semiwrap/autowrap/render_wrapped.py +++ b/src/semiwrap/autowrap/render_wrapped.py @@ -338,7 +338,10 @@ def render_wrapped_cpp(hctx: HeaderContext) -> str: # Global methods if hctx.functions: r.writeln() - for index, fn in enumerate(hctx.functions, start=1): + for index, fn in sorted( + enumerate(hctx.functions, start=1), + key=lambda item: item[1].binding_order, + ): if not fn.ignore_py: r.writeln( f"{_absolute_qualname(_function_helper_qualname(hctx, fn, index))}" diff --git a/src/semiwrap/config/autowrap_yml.py b/src/semiwrap/config/autowrap_yml.py index c8dab36..42f6a24 100644 --- a/src/semiwrap/config/autowrap_yml.py +++ b/src/semiwrap/config/autowrap_yml.py @@ -152,6 +152,12 @@ class OverloadData: #: ``defaults.default_args_as_kw_only``. default_args_as_kw_only: Optional[bool] = None + #: Binding priority: lower integers are registered with pybind11 first. + #: Ties preserve the existing order. Inherits the function/method setting + #: when omitted, otherwise defaults to 0. Applies within a class or within + #: the free functions of a single header, not across headers or classes. + binding_order: Optional[int] = None + #: If True, prepends an underscore to the python name internal: bool = False diff --git a/tests/cpp/sw-test/semiwrap/ft/overloads.yml b/tests/cpp/sw-test/semiwrap/ft/overloads.yml index 50c634b..8903b45 100644 --- a/tests/cpp/sw-test/semiwrap/ft/overloads.yml +++ b/tests/cpp/sw-test/semiwrap/ft/overloads.yml @@ -1,9 +1,109 @@ functions: + bindingOrder: + overloads: + int: + long: + binding_order: -1 + bindingOrderUnchanged: + overloads: + long: + int: + bindingOrderLater: + overloads: + int: + binding_order: 3 + long: + bindingOrderTied: + overloads: + int, int: + long long, int: + binding_order: -2 + long: + binding_order: -2 + bindingOrderInherited: + binding_order: 2 + overloads: + int, int, int: + long, int, int: + long long, int: + binding_order: 0 + short: + binding_order: -1 fnOverload: overloads: int, int: int: classes: + BindingOrder: + attributes: + selected: + methods: + BindingOrder: + overloads: + int: + binding_order: 1 + long: + choose: + overloads: + int: + long: + binding_order: -1 + unchanged: + overloads: + int: + long: + chooseStatic: + overloads: + int: + long: + binding_order: -1 + later: + overloads: + int: + binding_order: 3 + long: + tied: + overloads: + int, int: + long long, int: + binding_order: -2 + long: + binding_order: -2 + inherited: + binding_order: 2 + overloads: + int, int, int: + long, int, int: + long long, int: + binding_order: 0 + short: + binding_order: -1 + mixedUnchanged: + rename: mixedUnchanged + overloads: + long: + int: + mixedPrioritized: + rename: mixedPrioritized + overloads: + long: + binding_order: -1 + int: + chooseProtected: + overloads: + int: + long: + binding_order: -1 + BindingOrderTemplate: + template_params: + - T + methods: + choose: + binding_order: 2 + overloads: + T: + long: + binding_order: 0 OverloadedObject: methods: overloaded: @@ -31,3 +131,8 @@ classes: overloaded_private: overloads: int: +templates: + BindingOrderTemplateInt: + qualname: BindingOrderTemplate + params: + - int diff --git a/tests/cpp/sw-test/src/swtest/ft/include/overloads.h b/tests/cpp/sw-test/src/swtest/ft/include/overloads.h index 836a10d..ba60746 100644 --- a/tests/cpp/sw-test/src/swtest/ft/include/overloads.h +++ b/tests/cpp/sw-test/src/swtest/ft/include/overloads.h @@ -1,11 +1,75 @@ #pragma once -int fnOverload(int i, int j) +// Template bindings include this header in multiple translation units. +inline int bindingOrder(int value) { return 1; } +inline int bindingOrder(long value) { return 2; } + +inline int bindingOrderUnchanged(int value) { return 1; } +inline int bindingOrderUnchanged(long value) { return 2; } + +inline int bindingOrderLater(int value) { return 1; } +inline int bindingOrderLater(long value) { return 2; } + +inline int bindingOrderTied(int value, int skip = 0) { return 1; } +inline int bindingOrderTied(long value) { return 2; } +inline int bindingOrderTied(long long value, int skip = 0) { return 3; } + +inline int bindingOrderInherited(int value, int subset = 0, int inherited = 0) { return 1; } +inline int bindingOrderInherited(long value, int subset = 0, int inherited = 0) { return 2; } +inline int bindingOrderInherited(long long value, int subset = 0) { return 3; } +inline int bindingOrderInherited(short value) { return 4; } + +struct BindingOrder { + BindingOrder(int value) : selected(1) {} + BindingOrder(long value) : selected(2) {} + virtual ~BindingOrder() = default; + + int choose(int value) { return 1; } + int choose(long value) { return 2; } + int unchanged(int value) { return 1; } + int unchanged(long value) { return 2; } + static int chooseStatic(int value) { return 1; } + static int chooseStatic(long value) { return 2; } + + int later(int value) { return 1; } + int later(long value) { return 2; } + + int tied(int value, int skip = 0) { return 1; } + int tied(long value) { return 2; } + int tied(long long value, int skip = 0) { return 3; } + + int inherited(int value, int subset = 0, int inherited = 0) { return 1; } + int inherited(long value, int subset = 0, int inherited = 0) { return 2; } + int inherited(long long value, int subset = 0) { return 3; } + int inherited(short value) { return 4; } + + int selected; + +protected: + int chooseProtected(int value) { return 1; } + int chooseProtected(long value) { return 2; } + + // Declared before public overloads to test the existing public-first order. + int mixedUnchanged(long value) { return 2; } + int mixedPrioritized(long value) { return 2; } + +public: + int mixedUnchanged(int value) { return 1; } + int mixedPrioritized(int value) { return 1; } +}; + +template +struct BindingOrderTemplate { + int choose(T value) { return 1; } + int choose(long value) { return 2; } +}; + +inline int fnOverload(int i, int j) { return j; } -int fnOverload(int i) +inline int fnOverload(int i) { return i; } diff --git a/tests/test_binding_order.py b/tests/test_binding_order.py new file mode 100644 index 0000000..afa2cfc --- /dev/null +++ b/tests/test_binding_order.py @@ -0,0 +1,58 @@ +import pytest + +from swtest.ft import _ft as ft + + +@pytest.fixture(params=["function", "method"]) +def overloads(request): + if request.param == "function": + return ( + ft.bindingOrderUnchanged, + ft.bindingOrderLater, + ft.bindingOrderTied, + ft.bindingOrderInherited, + ) + obj = ft.BindingOrder(42) + return obj.unchanged, obj.later, obj.tied, obj.inherited + + +def test_binding_order_defaults_preserve_declaration_order(overloads): + unchanged, _, _, _ = overloads + assert unchanged(42) == 1 + + +def test_positive_binding_order_moves_overload_later(overloads): + _, later, _, _ = overloads + assert later(42) == 2 + + +def test_equal_priorities_preserve_declaration_order(overloads): + _, _, tied, _ = overloads + # The second and third overloads have the same negative priority. + assert tied(42) == 2 + # Exclude the second overload: the third still precedes the first. + assert tied(42, skip=0) == 3 + + +def test_binding_order_inheritance_and_explicit_zero(overloads): + _, _, _, inherited = overloads + assert inherited(42) == 4 # Explicit -1 precedes explicit 0 and inherited 2. + # Exclude the fourth overload: explicit 0 overrides the inherited 2. + assert inherited(42, subset=0) == 3 + # Exclude both overrides: equal inherited priorities retain original order. + assert inherited(42, inherited=0) == 1 + + +def test_template_method_binding_order(): + assert ft.BindingOrderTemplateInt().choose(42) == 2 + + +def test_equal_priorities_preserve_public_before_protected_order(): + # The protected overload is declared first, but public bindings came first + # before binding_order was introduced. Both are renamed to the same Python + # name so this tests actual overload resolution across the access boundary. + assert ft.BindingOrder(42).mixedUnchanged(42) == 1 + + +def test_protected_overload_can_precede_public_overload(): + assert ft.BindingOrder(42).mixedPrioritized(42) == 2 diff --git a/tests/test_ft_overloads.py b/tests/test_ft_overloads.py index 20c870d..1074d22 100644 --- a/tests/test_ft_overloads.py +++ b/tests/test_ft_overloads.py @@ -1,4 +1,14 @@ -from swtest import ft +from swtest.ft import _ft as ft + + +def test_binding_order_resolves_ambiguous_overloads(): + assert ft.bindingOrder(42) == 2 + obj = ft.BindingOrder(42) + assert obj.selected == 2 + assert obj.choose(42) == 2 + assert obj.chooseStatic(42) == 2 + assert obj._chooseProtected(42) == 2 + assert obj.unchanged(42) == 1 def test_fn_overloads():