From a796d69f7fdb67693914dd9fc74ef17b66c41a34 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 10 Jul 2026 05:54:00 +0000 Subject: [PATCH 01/26] Document typealias diagnostics design --- ...2026-07-10-typealias-diagnostics-design.md | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-10-typealias-diagnostics-design.md diff --git a/docs/superpowers/specs/2026-07-10-typealias-diagnostics-design.md b/docs/superpowers/specs/2026-07-10-typealias-diagnostics-design.md new file mode 100644 index 0000000..5f0bbe2 --- /dev/null +++ b/docs/superpowers/specs/2026-07-10-typealias-diagnostics-design.md @@ -0,0 +1,53 @@ +# Compile-time diagnostics for missing `typealias` entries + +## Problem + +When semiwrap parses a header that uses locally visible aliases or using-declarations, it may emit generated C++ that refers to a short type name such as `CantResolve`. If that name is not visible in the generated binding scope, the user currently sees an opaque C++/pybind11 compile error. The error can be much worse when generated trampolines and base-class trampoline composition are involved. + +Semiwrap cannot definitively know during YAML/header processing whether a short C++ name will resolve in every generated C++ context. The diagnostic should therefore remain a compile-time diagnostic in generated C++. + +## Design + +Generate fail-fast C++ alias probes for type names that are likely to need a YAML `typealias` entry. A probe is a `using` declaration placed before the generated binding or trampoline code uses the type: + +```cpp +// semiwrap diagnostic: if this line fails because `CantResolve` is unknown, +// add a typealias entry for it to the semiwrap yaml file. +using semiwrap_typealias_probe_CantResolve = CantResolve; +``` + +If `CantResolve` is not visible, compilation fails at this probe instead of deep inside pybind11 or trampoline templates. The compiler may not show a custom `static_assert` message, but the failing generated alias name and nearby comment identify the likely fix. + +## Scope placement + +Probes must be emitted in the same generated scope where the unresolved-prone type will later be used. + +- For normal binding code, emit probes in the generated `.cpp` near existing user/auto typealiases and before `py::class_`, method, constructor, or function binding expressions that use the type. +- For classes with trampolines, emit class-scope probes in the generated trampoline `.hpp` before constructors, methods, and inherited trampoline composition use the type. +- Keep the generated probe names unique and deterministic so multiple probes cannot collide. + +## Candidate types + +Start with type spellings that semiwrap already emits in generated signatures, such as function return types and parameter types. Derive the probe target from the parsed type's base spelling, not from the full decorated spelling, so `const CantResolve&` produces a probe for `CantResolve`. + +Only add probes for names that are plausible unresolved aliases: + +- include unqualified or partially qualified names that appear in generated signatures; +- skip C++ built-in/fundamental types; +- skip names that are already fully qualified with a leading `::`; +- skip obvious standard-library names and other names semiwrap intentionally qualifies itself. + +This keeps noise low while targeting cases like the `using.h` testcase. + +The probe list is best-effort. It does not need to prove that a name is unresolved; it only needs to move failures for suspicious names to an earlier, clearer location. If a candidate type is already resolved, the probe compiles away as a harmless alias. + +## Rejected alternatives + +- Generation-time error or warning: semiwrap cannot reliably know whether C++ name lookup will succeed in generated contexts. +- `static_assert`-based type existence checks: C++ name lookup for a missing non-dependent type fails before `static_assert` or SFINAE can provide a custom diagnostic. +- Placeholder declarations for missing names: declaring dummy types changes name lookup, can shadow real types, and may make invalid code fail in misleading ways. +- Unconditional `#warning`/`#pragma message`: explicit but noisy and less portable. This can be considered later as an opt-in debug mode if needed. + +## Testing + +Add or adjust tests around the existing `using.h` testcase to verify the generated C++ contains typealias probes for short alias-like type names. A compile-failure fixture can then remove the YAML `typealias` and assert the compiler output points at the semiwrap probe name/comment area rather than pybind11/trampoline internals, if the test harness supports expected build failures. From f5ca75fc328b02a2d811e2c4c65b94a104c85426 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 10 Jul 2026 05:59:42 +0000 Subject: [PATCH 02/26] Add typealias diagnostics implementation plan --- .../plans/2026-07-10-typealias-diagnostics.md | 817 ++++++++++++++++++ 1 file changed, 817 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-10-typealias-diagnostics.md diff --git a/docs/superpowers/plans/2026-07-10-typealias-diagnostics.md b/docs/superpowers/plans/2026-07-10-typealias-diagnostics.md new file mode 100644 index 0000000..698036e --- /dev/null +++ b/docs/superpowers/plans/2026-07-10-typealias-diagnostics.md @@ -0,0 +1,817 @@ +# Typealias Diagnostics Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Generate fail-fast compile-time alias probes that make missing semiwrap YAML `typealias` entries fail at a clear generated line. + +**Architecture:** Add a small probe collection/rendering helper, store collected probe targets on header/class contexts, and emit probes before generated `.cpp` binding code and trampoline `.hpp` code uses those types. Collection is best-effort and based on parsed C++ types that semiwrap already processes for function signatures. + +**Tech Stack:** Python 3, cxxheaderparser type nodes, semiwrap autowrap dataclasses/renderers, pytest, existing C++ fixture project under `tests/cpp/sw-test`. + +## Global Constraints + +- Diagnostic must be compile-time only in generated C++; do not add generation-time missing-name errors. +- Do not use placeholder C++ declarations for missing types. +- Do not use unconditional `#warning` or `#pragma message` diagnostics. +- Probes must compile away as harmless `using` aliases when candidate types are already visible. +- Generated probe names must be unique, deterministic, and descriptive enough to mention `typealias` and YAML. +- Emit probes in generated `.cpp` binding scope and generated trampoline `.hpp` class scope where applicable. + +--- + +## File Structure + +- Create `src/semiwrap/autowrap/typealias_probe.py` + - Owns candidate extraction from cxxheaderparser type nodes. + - Owns deterministic/safe generated alias-name construction. + - Owns C++ rendering of probe comments and `using` lines. +- Modify `src/semiwrap/autowrap/context.py` + - Add `typealias_probes: List[str]` to `ClassContext` and `HeaderContext`. +- Modify `src/semiwrap/autowrap/cxxparser.py` + - Collect probes from return/parameter parsed types while building `FunctionContext`. + - Add global function probes to `HeaderContext.typealias_probes`. + - Add class method/constructor probes to `ClassContext.typealias_probes`. +- Modify `src/semiwrap/autowrap/render_wrapped.py` + - Emit header-level probes in the generated `.cpp` after user header-level `typealias` lines and before initializer struct/class declarations. +- Modify `src/semiwrap/autowrap/render_pybind11.py` + - Emit class-level probes near existing class user/auto using helpers before class declarations/definitions that use method signatures. +- Modify `src/semiwrap/autowrap/render_cls_trampoline_hpp.py` + - Emit class-level probes inside generated trampoline classes before constructors and virtual/protected methods. +- Create `tests/test_typealias_probe.py` + - Unit tests for helper extraction, de-duplication, sanitization, and rendering. +- Modify `tests/test_ft_misc.py` + - Add generated-output assertions for the existing `using.h` fixture after the C++ project has been built by the test harness. + +--- + +### Task 1: Add typealias probe helper and unit tests + +**Files:** +- Create: `src/semiwrap/autowrap/typealias_probe.py` +- Test: `tests/test_typealias_probe.py` + +**Interfaces:** +- Consumes: cxxheaderparser `DecoratedType`, `FunctionType`, `NameSpecifier`, `Type`, `Pointer`, `Reference`, `MoveReference`, `Array`, and `TemplateArgument` nodes. +- Produces: + - `collect_typealias_probes(dt: DecoratedType | FunctionType | None) -> list[str]` + - `add_typealias_probe(probes: list[str], target: str) -> None` + - `probe_alias_name(target: str) -> str` + - `render_typealias_probes(r: RenderBuffer, probes: Sequence[str], *, indent: str = "") -> None` + +- [ ] **Step 1: Write failing helper tests** + +Create `tests/test_typealias_probe.py` with: + +```python +from cxxheaderparser.simple import parse_typename + +from semiwrap.autowrap.buffer import RenderBuffer +from semiwrap.autowrap.typealias_probe import ( + add_typealias_probe, + collect_typealias_probes, + probe_alias_name, + render_typealias_probes, +) + + +def probes_for(type_text: str) -> list[str]: + return collect_typealias_probes(parse_typename(type_text)) + + +def test_collects_unqualified_alias_from_decorated_type(): + assert probes_for("const CantResolve&") == ["CantResolve"] + + +def test_collects_template_alias_and_inner_alias(): + assert probes_for("fancy_list") == [ + "fancy_list", + "CantResolve", + ] + + +def test_skips_builtin_std_and_global_root_targets_but_collects_template_args(): + assert probes_for("int") == [] + assert probes_for("std::vector") == ["CantResolve"] + assert probes_for("::AlreadyQualified") == [] + + +def test_add_typealias_probe_deduplicates_in_order(): + probes: list[str] = [] + add_typealias_probe(probes, "CantResolve") + add_typealias_probe(probes, "AlsoCantResolve") + add_typealias_probe(probes, "CantResolve") + assert probes == ["CantResolve", "AlsoCantResolve"] + + +def test_probe_alias_name_is_deterministic_and_descriptive(): + assert probe_alias_name("CantResolve") == ( + "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" + ) + assert probe_alias_name("fancy_list") == ( + "semiwrap_typealias_probe_fancy_list_CantResolve__add_typealias_to_yaml" + ) + + +def test_render_typealias_probes_emits_comment_and_using_lines(): + r = RenderBuffer() + render_typealias_probes(r, ["CantResolve"]) + out = r.getvalue() + assert "semiwrap diagnostic" in out + assert "add a typealias entry" in out + assert ( + "using semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml " + "[[maybe_unused]] = CantResolve;" + ) in out +``` + +- [ ] **Step 2: Run helper tests and verify they fail** + +Run: + +```bash +pytest tests/test_typealias_probe.py -q +``` + +Expected: FAIL during import with `ModuleNotFoundError: No module named 'semiwrap.autowrap.typealias_probe'`. + +- [ ] **Step 3: Implement helper module** + +Create `src/semiwrap/autowrap/typealias_probe.py` with: + +```python +import re +import typing as T + +from cxxheaderparser.types import ( + Array, + DecoratedType, + FunctionType, + FundamentalSpecifier, + MoveReference, + NameSpecifier, + Pointer, + Reference, + TemplateArgument, + Type, + Value, +) + +from .buffer import RenderBuffer + +_INT_TYPES = frozenset( + [ + "bool", + "char", + "char8_t", + "char16_t", + "char32_t", + "double", + "float", + "int", + "long", + "short", + "signed", + "unsigned", + "void", + "wchar_t", + "size_t", + "ssize_t", + "ptrdiff_t", + "intmax_t", + "uintmax_t", + ] + + [ + f"{prefix}{middle}{bits}_t" + for prefix in ("int", "uint") + for middle in ("", "_fast", "_least") + for bits in ("8", "16", "32", "64") + ] +) + +_SANITIZE_RE = re.compile(r"[^0-9A-Za-z]+") + + +def _is_builtin_type(t: Type) -> bool: + last = t.typename.segments[-1] + if isinstance(last, FundamentalSpecifier): + return True + if isinstance(last, NameSpecifier) and len(t.typename.segments) == 1: + return last.name in _INT_TYPES + return False + + +def _target_from_type(t: Type) -> str | None: + if _is_builtin_type(t): + return None + if not all(isinstance(seg, NameSpecifier) for seg in t.typename.segments): + return None + + target = t.typename.format() + if target.startswith("::"): + return None + if target.startswith(("std::", "py::", "pybind11::", "semiwrap::", "swgen::")): + return None + return target + + +def _collect_from_template_args(t: Type, out: list[str]) -> None: + for segment in t.typename.segments: + if not isinstance(segment, NameSpecifier) or segment.specialization is None: + continue + for arg in segment.specialization.args: + _collect_from_template_arg(arg, out) + + +def _collect_from_template_arg(arg: TemplateArgument, out: list[str]) -> None: + value = arg.arg + if isinstance(value, Value): + return + collect_typealias_probes_into(value, out) + + +def add_typealias_probe(probes: list[str], target: str) -> None: + if target and target not in probes: + probes.append(target) + + +def collect_typealias_probes_into( + dt: DecoratedType | FunctionType | None, out: list[str] +) -> None: + if dt is None: + return + if isinstance(dt, Type): + target = _target_from_type(dt) + if target is not None: + add_typealias_probe(out, target) + _collect_from_template_args(dt, out) + elif isinstance(dt, Pointer): + collect_typealias_probes_into(dt.ptr_to, out) + elif isinstance(dt, Reference): + collect_typealias_probes_into(dt.ref_to, out) + elif isinstance(dt, MoveReference): + collect_typealias_probes_into(dt.moveref_to, out) + elif isinstance(dt, Array): + collect_typealias_probes_into(dt.array_of, out) + elif isinstance(dt, FunctionType): + collect_typealias_probes_into(dt.return_type, out) + for param in dt.parameters: + collect_typealias_probes_into(param.type, out) + + +def collect_typealias_probes(dt: DecoratedType | FunctionType | None) -> list[str]: + probes: list[str] = [] + collect_typealias_probes_into(dt, probes) + return probes + + +def probe_alias_name(target: str) -> str: + safe = _SANITIZE_RE.sub("_", target).strip("_") + if not safe: + safe = "unknown" + return f"semiwrap_typealias_probe_{safe}__add_typealias_to_yaml" + + +def render_typealias_probes( + r: RenderBuffer, probes: T.Sequence[str], *, indent: str = "" +) -> None: + for target in probes: + alias = probe_alias_name(target) + r.writeln( + f"{indent}// semiwrap diagnostic: if this line fails because `{target}` " + "is unknown," + ) + r.writeln( + f"{indent}// add a typealias entry for `{target}` to the semiwrap yaml file." + ) + r.writeln(f"{indent}using {alias} [[maybe_unused]] = {target};") +``` + +- [ ] **Step 4: Run helper tests and verify they pass** + +Run: + +```bash +pytest tests/test_typealias_probe.py -q +``` + +Expected: PASS, all six tests pass. + +- [ ] **Step 5: Commit helper** + +```bash +git add src/semiwrap/autowrap/typealias_probe.py tests/test_typealias_probe.py +git commit -m "Add typealias probe helper" +``` + +--- + +### Task 2: Collect probes while parsing headers + +**Files:** +- Modify: `src/semiwrap/autowrap/context.py` +- Modify: `src/semiwrap/autowrap/cxxparser.py` +- Test: `tests/test_typealias_probe.py` + +**Interfaces:** +- Consumes from Task 1: + - `add_typealias_probe(probes: list[str], target: str) -> None` + - `collect_typealias_probes(dt: DecoratedType | FunctionType | None) -> list[str]` +- Produces: + - `ClassContext.typealias_probes: list[str]` + - `HeaderContext.typealias_probes: list[str]` + +- [ ] **Step 1: Add failing parser collection tests** + +Append to `tests/test_typealias_probe.py`: + +```python +import pathlib + +from cxxheaderparser.options import ParserOptions + +from semiwrap.autowrap.cxxparser import parse_header +from semiwrap.autowrap.generator_data import GeneratorData +from semiwrap.config.autowrap_yml import AutowrapConfigYaml + + +def parse_fixture_header(header_name: str, yaml_name: str): + root = pathlib.Path("tests/cpp/sw-test/src/swtest/ft/include") + yml = pathlib.Path("tests/cpp/sw-test/semiwrap/ft") / yaml_name + cfg = AutowrapConfigYaml.from_file(yml) + return parse_header( + header_name, + root / header_name, + root, + GeneratorData(cfg, yml), + ParserOptions(), + {}, + False, + ) + + +def test_parse_header_collects_global_function_typealias_probe(): + hctx = parse_fixture_header("using.h", "using.yml") + assert "AlsoCantResolve" in hctx.typealias_probes + + +def test_parse_header_collects_class_constructor_typealias_probe(): + hctx = parse_fixture_header("using.h", "using.yml") + cls = next(c for c in hctx.classes if c.cpp_name == "ProtectedUsing") + assert "CantResolve" in cls.typealias_probes +``` + +- [ ] **Step 2: Run parser collection tests and verify they fail** + +Run: + +```bash +pytest tests/test_typealias_probe.py::test_parse_header_collects_global_function_typealias_probe tests/test_typealias_probe.py::test_parse_header_collects_class_constructor_typealias_probe -q +``` + +Expected: FAIL with `AttributeError` for missing `typealias_probes` on context objects. + +- [ ] **Step 3: Add context fields** + +In `src/semiwrap/autowrap/context.py`, add this field to `ClassContext` near `auto_typealias`: + +```python + #: Best-effort C++ type spellings that should be probed before generated + #: code uses them. These produce clearer compile-time diagnostics when a + #: YAML typealias entry is missing. + typealias_probes: typing.List[str] = field(default_factory=list) +``` + +Add this field to `HeaderContext` near `user_typealias`: + +```python + #: Best-effort C++ type spellings that should be probed before generated + #: global binding code uses them. + typealias_probes: typing.List[str] = field(default_factory=list) +``` + +- [ ] **Step 4: Import helper and collect probes in parser** + +In `src/semiwrap/autowrap/cxxparser.py`, add this import near other autowrap imports: + +```python +from .typealias_probe import add_typealias_probe, collect_typealias_probes +``` + +In `AutowrapVisitor.on_function`, after `fctx.namespace = state.user_data` and before `self.hctx.functions.append(fctx)`, add: + +```python + for probe in collect_typealias_probes(fn.return_type): + add_typealias_probe(self.hctx.typealias_probes, probe) + for param in fn.parameters: + for probe in collect_typealias_probes(param.type): + add_typealias_probe(self.hctx.typealias_probes, probe) +``` + +In `AutowrapVisitor._on_class_method`, after the call to `self._on_fn_or_method(...)` and before class-specific method attributes are updated, add: + +```python + for probe in collect_typealias_probes(method.return_type): + add_typealias_probe(cctx.typealias_probes, probe) + for param in method.parameters: + for probe in collect_typealias_probes(param.type): + add_typealias_probe(cctx.typealias_probes, probe) +``` + +- [ ] **Step 5: Run parser collection tests and verify they pass** + +Run: + +```bash +pytest tests/test_typealias_probe.py::test_parse_header_collects_global_function_typealias_probe tests/test_typealias_probe.py::test_parse_header_collects_class_constructor_typealias_probe -q +``` + +Expected: PASS, both tests pass. + +- [ ] **Step 6: Run all probe tests** + +Run: + +```bash +pytest tests/test_typealias_probe.py -q +``` + +Expected: PASS, all tests in `tests/test_typealias_probe.py` pass. + +- [ ] **Step 7: Commit parser collection** + +```bash +git add src/semiwrap/autowrap/context.py src/semiwrap/autowrap/cxxparser.py tests/test_typealias_probe.py +git commit -m "Collect typealias probes while parsing" +``` + +--- + +### Task 3: Render probes in generated `.cpp` binding code + +**Files:** +- Modify: `src/semiwrap/autowrap/render_wrapped.py` +- Modify: `src/semiwrap/autowrap/render_pybind11.py` +- Test: `tests/test_typealias_probe.py` + +**Interfaces:** +- Consumes from Task 1: + - `render_typealias_probes(r: RenderBuffer, probes: Sequence[str], *, indent: str = "") -> None` +- Consumes from Task 2: + - `HeaderContext.typealias_probes` + - `ClassContext.typealias_probes` +- Produces generated `.cpp` output containing `semiwrap_typealias_probe_*__add_typealias_to_yaml` aliases. + +- [ ] **Step 1: Add failing render tests for `.cpp` output** + +Append to `tests/test_typealias_probe.py`: + +```python +from semiwrap.autowrap.render_wrapped import render_wrapped_cpp + + +def test_render_wrapped_cpp_emits_global_typealias_probe_before_initializer(): + hctx = parse_fixture_header("using.h", "using.yml") + out = render_wrapped_cpp(hctx) + probe = "semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" + assert probe in out + assert out.index(probe) < out.index("struct semiwrap_using_initializer") + assert "using semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" in out + assert "= AlsoCantResolve;" in out + + +def test_render_wrapped_cpp_emits_class_typealias_probe_inside_initializer(): + hctx = parse_fixture_header("using.h", "using.yml") + out = render_wrapped_cpp(hctx) + probe = "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" + assert probe in out + assert out.index("struct semiwrap_using_initializer") < out.index(probe) + assert out.index(probe) < out.index("py::class_ None` +- Consumes from Task 2: + - `ClassContext.typealias_probes` +- Produces generated trampoline `.hpp` output containing class-scope probe aliases before protected constructors and virtual/protected methods. + +- [ ] **Step 1: Add failing trampoline render test** + +Append to `tests/test_typealias_probe.py`: + +```python +from semiwrap.autowrap.render_cls_trampoline_hpp import render_cls_trampoline_hpp + + +def test_render_trampoline_hpp_emits_class_typealias_probe(): + hctx = parse_fixture_header("using.h", "using.yml") + cls = next(c for c in hctx.classes if c.cpp_name == "ProtectedUsing") + out = render_cls_trampoline_hpp(hctx, cls) + probe = "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" + assert probe in out + assert out.index(probe) < out.index("PyTrampoline_ProtectedUsing(CantResolve") +``` + +- [ ] **Step 2: Run trampoline render test and verify it fails** + +Run: + +```bash +pytest tests/test_typealias_probe.py::test_render_trampoline_hpp_emits_class_typealias_probe -q +``` + +Expected: FAIL because generated trampoline output does not contain `semiwrap_typealias_probe_`. + +- [ ] **Step 3: Render class-level probes in trampoline classes** + +In `src/semiwrap/autowrap/render_cls_trampoline_hpp.py`, add this import near existing imports: + +```python +from .typealias_probe import render_typealias_probes +``` + +Inside `_render_cls_trampoline`, in the existing `with r.indent():` block that emits child classes, enums, `cls.user_typealias`, and `cls.auto_typealias`, add this after the `cls.auto_typealias` loop and before the `if cls.constants:` block: + +```python + if cls.typealias_probes: + render_typealias_probes(r, cls.typealias_probes) +``` + +- [ ] **Step 4: Run trampoline render test and verify it passes** + +Run: + +```bash +pytest tests/test_typealias_probe.py::test_render_trampoline_hpp_emits_class_typealias_probe -q +``` + +Expected: PASS. + +- [ ] **Step 5: Run all probe tests** + +Run: + +```bash +pytest tests/test_typealias_probe.py -q +``` + +Expected: PASS, all tests in `tests/test_typealias_probe.py` pass. + +- [ ] **Step 6: Commit trampoline rendering** + +```bash +git add src/semiwrap/autowrap/render_cls_trampoline_hpp.py tests/test_typealias_probe.py +git commit -m "Render typealias probes in trampolines" +``` + +--- + +### Task 5: Add generated fixture assertions and run integration tests + +**Files:** +- Modify: `tests/test_ft_misc.py` + +**Interfaces:** +- Consumes generated files under `tests/cpp/sw-test/build/*/semiwrap/` created by the existing `tests/run_tests.py` install/build flow. +- Produces regression coverage that the real `using.h` fixture emits typealias probes in built generated C++. + +- [ ] **Step 1: Add failing fixture-output test** + +Append this test under the `# using.h / using2.h` section in `tests/test_ft_misc.py`: + +```python +def test_using_generated_typealias_probes_present(): + from pathlib import Path + + root = Path(__file__).parent / "cpp" / "sw-test" / "build" + using_cpp_files = sorted(root.glob("*/semiwrap/using.cpp")) + assert using_cpp_files, "sw-test build did not generate semiwrap/using.cpp" + using_cpp = using_cpp_files[-1].read_text() + + assert "semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" in using_cpp + assert "using semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" in using_cpp + assert "= AlsoCantResolve;" in using_cpp + assert "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" in using_cpp + assert "= CantResolve;" in using_cpp + + trampoline_files = sorted( + root.glob("*/semiwrap/trampolines/cr__inner__ProtectedUsing.hpp") + ) + assert trampoline_files, ( + "sw-test build did not generate trampoline for cr::inner::ProtectedUsing" + ) + trampoline_hpp = trampoline_files[-1].read_text() + assert "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" in trampoline_hpp + assert "add a typealias entry for `CantResolve`" in trampoline_hpp +``` + +- [ ] **Step 2: Run the fixture test and verify it passes after a rebuild** + +Run: + +```bash +python tests/run_tests.py tests/test_ft_misc.py::test_using_generated_typealias_probes_present -q +``` + +Expected: PASS. The command first rebuilds the C++ fixtures, then pytest finds the generated `using.cpp` and trampoline header with probe aliases. + +If this fails because the trampoline file name differs, run: + +```bash +find tests/cpp/sw-test/build -path '*/semiwrap/trampolines/*ProtectedUsing*.hpp' -print +``` + +Expected: one printed path. Replace `cr__inner__ProtectedUsing.hpp` in the test with the actual generated basename and rerun the `python tests/run_tests.py ...` command. + +- [ ] **Step 3: Run focused Python/unit tests** + +Run: + +```bash +pytest tests/test_typealias_probe.py -q +``` + +Expected: PASS. + +- [ ] **Step 4: Run focused existing runtime test** + +Run: + +```bash +pytest tests/test_ft_misc.py::test_using_fwddecl tests/test_ft_misc.py::test_using_generated_typealias_probes_present -q +``` + +Expected: PASS when the C++ fixture has already been built by Step 2. + +- [ ] **Step 5: Commit fixture assertions** + +```bash +git add tests/test_ft_misc.py +git commit -m "Test generated typealias probes in fixture build" +``` + +--- + +### Task 6: Full verification and cleanup + +**Files:** +- No new files. +- May modify any implementation/test file from earlier tasks only if verification exposes a concrete defect. + +**Interfaces:** +- Consumes all prior tasks. +- Produces a verified branch with passing focused tests and clean git status. + +- [ ] **Step 1: Run probe unit tests** + +Run: + +```bash +pytest tests/test_typealias_probe.py -q +``` + +Expected: PASS. + +- [ ] **Step 2: Run C++ fixture rebuild and focused tests** + +Run: + +```bash +python tests/run_tests.py tests/test_ft_misc.py::test_using_fwddecl tests/test_ft_misc.py::test_using_generated_typealias_probes_present -q +``` + +Expected: PASS. + +- [ ] **Step 3: Run the full test suite if time permits** + +Run: + +```bash +python tests/run_tests.py +``` + +Expected: PASS. If this command is too slow for the environment, record the timeout/runtime limitation and keep the focused passing commands from Steps 1-2 as verification evidence. + +- [ ] **Step 4: Inspect generated diagnostic output manually** + +Run: + +```bash +python - <<'PY' +from pathlib import Path +root = Path('tests/cpp/sw-test/build') +for p in sorted(root.glob('*/semiwrap/using.cpp'))[-1:]: + text = p.read_text() + for line in text.splitlines(): + if 'semiwrap_typealias_probe_' in line or 'semiwrap diagnostic' in line: + print(line) +PY +``` + +Expected output includes lines similar to: + +```text +// semiwrap diagnostic: if this line fails because `AlsoCantResolve` is unknown, +using semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml [[maybe_unused]] = AlsoCantResolve; +// semiwrap diagnostic: if this line fails because `CantResolve` is unknown, +using semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml [[maybe_unused]] = CantResolve; +``` + +- [ ] **Step 5: Check git status** + +Run: + +```bash +git status --short +``` + +Expected: no uncommitted tracked implementation/test changes. Ignored build artifacts under `tests/cpp/*/build` are acceptable and should not be committed. + +- [ ] **Step 6: Commit any verification fixes** + +Only if Step 1, 2, 3, or 4 required code/test changes, commit them: + +```bash +git add src/semiwrap/autowrap tests/test_typealias_probe.py tests/test_ft_misc.py +git commit -m "Fix typealias probe verification issues" +``` + +Expected: either a new commit is created for concrete fixes, or there is nothing to commit. From 50d8a931686fc471b460617b90d7da0ee545bbf6 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 10 Jul 2026 06:04:21 +0000 Subject: [PATCH 03/26] Require sorted typealias probes --- .../plans/2026-07-10-typealias-diagnostics.md | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/superpowers/plans/2026-07-10-typealias-diagnostics.md b/docs/superpowers/plans/2026-07-10-typealias-diagnostics.md index 698036e..9e08a99 100644 --- a/docs/superpowers/plans/2026-07-10-typealias-diagnostics.md +++ b/docs/superpowers/plans/2026-07-10-typealias-diagnostics.md @@ -15,6 +15,7 @@ - Do not use unconditional `#warning` or `#pragma message` diagnostics. - Probes must compile away as harmless `using` aliases when candidate types are already visible. - Generated probe names must be unique, deterministic, and descriptive enough to mention `typealias` and YAML. +- Generated probe `using` lines must be sorted by probe target within each generated C++ scope. - Emit probes in generated `.cpp` binding scope and generated trampoline `.hpp` class scope where applicable. --- @@ -24,7 +25,7 @@ - Create `src/semiwrap/autowrap/typealias_probe.py` - Owns candidate extraction from cxxheaderparser type nodes. - Owns deterministic/safe generated alias-name construction. - - Owns C++ rendering of probe comments and `using` lines. + - Owns sorted C++ rendering of probe comments and `using` lines. - Modify `src/semiwrap/autowrap/context.py` - Add `typealias_probes: List[str]` to `ClassContext` and `HeaderContext`. - Modify `src/semiwrap/autowrap/cxxparser.py` @@ -82,10 +83,10 @@ def test_collects_unqualified_alias_from_decorated_type(): assert probes_for("const CantResolve&") == ["CantResolve"] -def test_collects_template_alias_and_inner_alias(): +def test_collects_template_alias_and_inner_alias_sorted(): assert probes_for("fancy_list") == [ - "fancy_list", "CantResolve", + "fancy_list", ] @@ -95,12 +96,12 @@ def test_skips_builtin_std_and_global_root_targets_but_collects_template_args(): assert probes_for("::AlreadyQualified") == [] -def test_add_typealias_probe_deduplicates_in_order(): +def test_add_typealias_probe_deduplicates_and_sorts(): probes: list[str] = [] add_typealias_probe(probes, "CantResolve") add_typealias_probe(probes, "AlsoCantResolve") add_typealias_probe(probes, "CantResolve") - assert probes == ["CantResolve", "AlsoCantResolve"] + assert probes == ["AlsoCantResolve", "CantResolve"] def test_probe_alias_name_is_deterministic_and_descriptive(): @@ -112,12 +113,15 @@ def test_probe_alias_name_is_deterministic_and_descriptive(): ) -def test_render_typealias_probes_emits_comment_and_using_lines(): +def test_render_typealias_probes_emits_comment_and_sorted_using_lines(): r = RenderBuffer() - render_typealias_probes(r, ["CantResolve"]) + render_typealias_probes(r, ["CantResolve", "AlsoCantResolve"]) out = r.getvalue() assert "semiwrap diagnostic" in out assert "add a typealias entry" in out + assert out.index( + "using semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" + ) < out.index("using semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml") assert ( "using semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml " "[[maybe_unused]] = CantResolve;" @@ -232,6 +236,7 @@ def _collect_from_template_arg(arg: TemplateArgument, out: list[str]) -> None: def add_typealias_probe(probes: list[str], target: str) -> None: if target and target not in probes: probes.append(target) + probes.sort() def collect_typealias_probes_into( @@ -274,7 +279,7 @@ def probe_alias_name(target: str) -> str: def render_typealias_probes( r: RenderBuffer, probes: T.Sequence[str], *, indent: str = "" ) -> None: - for target in probes: + for target in sorted(probes): alias = probe_alias_name(target) r.writeln( f"{indent}// semiwrap diagnostic: if this line fails because `{target}` " From cc9536524aec0c6498b2cd21b1d5d3f5a0695f43 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 10 Jul 2026 06:18:38 +0000 Subject: [PATCH 04/26] Add typealias probe helper --- src/semiwrap/autowrap/typealias_probe.py | 149 +++++++++++++++++++++++ tests/test_typealias_probe.py | 76 ++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 src/semiwrap/autowrap/typealias_probe.py create mode 100644 tests/test_typealias_probe.py diff --git a/src/semiwrap/autowrap/typealias_probe.py b/src/semiwrap/autowrap/typealias_probe.py new file mode 100644 index 0000000..455f283 --- /dev/null +++ b/src/semiwrap/autowrap/typealias_probe.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import re +import typing as T + +from cxxheaderparser.types import ( + Array, + DecoratedType, + FunctionType, + FundamentalSpecifier, + MoveReference, + NameSpecifier, + Pointer, + Reference, + TemplateArgument, + Type, + Value, +) + +from .buffer import RenderBuffer + +_INT_TYPES = frozenset( + [ + "bool", + "char", + "char8_t", + "char16_t", + "char32_t", + "double", + "float", + "int", + "long", + "short", + "signed", + "unsigned", + "void", + "wchar_t", + "size_t", + "ssize_t", + "ptrdiff_t", + "intmax_t", + "uintmax_t", + ] + + [ + f"{prefix}{middle}{bits}_t" + for prefix in ("int", "uint") + for middle in ("", "_fast", "_least") + for bits in ("8", "16", "32", "64") + ] +) + +_SANITIZE_RE = re.compile(r"[^0-9A-Za-z]+") + + +def _is_builtin_type(t: Type) -> bool: + last = t.typename.segments[-1] + if isinstance(last, FundamentalSpecifier): + return True + if isinstance(last, NameSpecifier) and len(t.typename.segments) == 1: + return last.name in _INT_TYPES + return False + + +def _target_from_type(t: Type) -> str | None: + if _is_builtin_type(t): + return None + if not all(isinstance(seg, NameSpecifier) for seg in t.typename.segments): + return None + + target = t.typename.format() + if target.startswith("::"): + return None + if target.startswith(("std::", "py::", "pybind11::", "semiwrap::", "swgen::")): + return None + return target + + +def _collect_from_template_args(t: Type, out: list[str]) -> None: + for segment in t.typename.segments: + if not isinstance(segment, NameSpecifier) or segment.specialization is None: + continue + for arg in segment.specialization.args: + _collect_from_template_arg(arg, out) + + +def _collect_from_template_arg(arg: TemplateArgument, out: list[str]) -> None: + value = arg.arg + if isinstance(value, Value): + return + collect_typealias_probes_into(value, out) + + +def add_typealias_probe(probes: list[str], target: str) -> None: + if target and target not in probes: + probes.append(target) + probes.sort() + + +def collect_typealias_probes_into( + dt: DecoratedType | FunctionType | None, out: list[str] +) -> None: + if dt is None: + return + if isinstance(dt, Type): + target = _target_from_type(dt) + if target is not None: + add_typealias_probe(out, target) + _collect_from_template_args(dt, out) + elif isinstance(dt, Pointer): + collect_typealias_probes_into(dt.ptr_to, out) + elif isinstance(dt, Reference): + collect_typealias_probes_into(dt.ref_to, out) + elif isinstance(dt, MoveReference): + collect_typealias_probes_into(dt.moveref_to, out) + elif isinstance(dt, Array): + collect_typealias_probes_into(dt.array_of, out) + elif isinstance(dt, FunctionType): + collect_typealias_probes_into(dt.return_type, out) + for param in dt.parameters: + collect_typealias_probes_into(param.type, out) + + +def collect_typealias_probes(dt: DecoratedType | FunctionType | None) -> list[str]: + probes: list[str] = [] + collect_typealias_probes_into(dt, probes) + return probes + + +def probe_alias_name(target: str) -> str: + safe_target = target.replace("::", "_scope_") + safe = _SANITIZE_RE.sub("_", safe_target).strip("_") + if not safe: + safe = "unknown" + return f"semiwrap_typealias_probe_{safe}__add_typealias_to_yaml" + + +def render_typealias_probes( + r: RenderBuffer, probes: T.Sequence[str], *, indent: str = "" +) -> None: + for target in sorted(probes): + alias = probe_alias_name(target) + r.writeln( + f"{indent}// semiwrap diagnostic: if this line fails because `{target}` " + "is unknown," + ) + r.writeln( + f"{indent}// add a typealias entry for `{target}` to the semiwrap yaml file." + ) + r.writeln(f"{indent}using {alias} [[maybe_unused]] = {target};") diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py new file mode 100644 index 0000000..c04979b --- /dev/null +++ b/tests/test_typealias_probe.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from pathlib import Path + +from cxxheaderparser.simple import parse_typename + +from semiwrap.autowrap import typealias_probe +from semiwrap.autowrap.buffer import RenderBuffer +from semiwrap.autowrap.typealias_probe import ( + add_typealias_probe, + collect_typealias_probes, + probe_alias_name, + render_typealias_probes, +) + + +def probes_for(type_text: str) -> list[str]: + return collect_typealias_probes(parse_typename(type_text)) + + +def test_collects_unqualified_alias_from_decorated_type(): + assert probes_for("const CantResolve&") == ["CantResolve"] + + +def test_collects_template_alias_and_inner_alias_sorted(): + assert probes_for("fancy_list") == [ + "CantResolve", + "fancy_list", + ] + + +def test_skips_builtin_std_and_global_root_targets_but_collects_template_args(): + assert probes_for("int") == [] + assert probes_for("std::vector") == ["CantResolve"] + assert probes_for("::AlreadyQualified") == [] + + +def test_add_typealias_probe_deduplicates_and_sorts(): + probes: list[str] = [] + add_typealias_probe(probes, "CantResolve") + add_typealias_probe(probes, "AlsoCantResolve") + add_typealias_probe(probes, "CantResolve") + assert probes == ["AlsoCantResolve", "CantResolve"] + + +def test_probe_alias_name_is_deterministic_and_descriptive(): + assert probe_alias_name("CantResolve") == ( + "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" + ) + assert probe_alias_name("fancy_list") == ( + "semiwrap_typealias_probe_fancy_list_CantResolve__add_typealias_to_yaml" + ) + + +def test_render_typealias_probes_emits_comment_and_sorted_using_lines(): + r = RenderBuffer() + render_typealias_probes(r, ["CantResolve", "AlsoCantResolve"]) + out = r.getvalue() + assert "semiwrap diagnostic" in out + assert "add a typealias entry" in out + assert out.index( + "using semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" + ) < out.index("using semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml") + assert ( + "using semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml " + "[[maybe_unused]] = CantResolve;" + ) in out + + +def test_probe_alias_name_distinguishes_namespaces_from_templates(): + assert probe_alias_name("A::B") != probe_alias_name("A") + + +def test_helper_uses_deferred_annotations_for_python_38_runtime_compatibility(): + source = Path(typealias_probe.__file__).read_text() + assert source.startswith("from __future__ import annotations\n") From fc9d0d031ed70db63cc6ce71ffe565a2743a32ee Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 10 Jul 2026 06:27:36 +0000 Subject: [PATCH 05/26] Fix typealias probe alias collisions --- src/semiwrap/autowrap/typealias_probe.py | 39 ++++++++++++++++++++---- tests/test_typealias_probe.py | 6 +++- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/semiwrap/autowrap/typealias_probe.py b/src/semiwrap/autowrap/typealias_probe.py index 455f283..37fe4c1 100644 --- a/src/semiwrap/autowrap/typealias_probe.py +++ b/src/semiwrap/autowrap/typealias_probe.py @@ -1,6 +1,5 @@ from __future__ import annotations -import re import typing as T from cxxheaderparser.types import ( @@ -49,7 +48,17 @@ ] ) -_SANITIZE_RE = re.compile(r"[^0-9A-Za-z]+") +_ENCODE_TOKENS = { + "_": "_u_", + "<": "_lt_", + ">": "_gt_", + ",": "_comma_", + " ": "_space_", + "*": "_star_", + "&": "_amp_", + ".": "_dot_", + "-": "_dash_", +} def _is_builtin_type(t: Type) -> bool: @@ -126,11 +135,29 @@ def collect_typealias_probes(dt: DecoratedType | FunctionType | None) -> list[st return probes +def _encode_alias_target(target: str) -> str: + if not target: + return "_empty_" + + encoded: list[str] = [] + index = 0 + while index < len(target): + if target.startswith("::", index): + encoded.append("_scope_") + index += 2 + continue + + char = target[index] + if char.isascii() and char.isalnum(): + encoded.append(char) + else: + encoded.append(_ENCODE_TOKENS.get(char, f"_x{ord(char):x}_")) + index += 1 + return "".join(encoded) + + def probe_alias_name(target: str) -> str: - safe_target = target.replace("::", "_scope_") - safe = _SANITIZE_RE.sub("_", safe_target).strip("_") - if not safe: - safe = "unknown" + safe = _encode_alias_target(target) return f"semiwrap_typealias_probe_{safe}__add_typealias_to_yaml" diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py index c04979b..6560e00 100644 --- a/tests/test_typealias_probe.py +++ b/tests/test_typealias_probe.py @@ -48,7 +48,7 @@ def test_probe_alias_name_is_deterministic_and_descriptive(): "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" ) assert probe_alias_name("fancy_list") == ( - "semiwrap_typealias_probe_fancy_list_CantResolve__add_typealias_to_yaml" + "semiwrap_typealias_probe_fancy_u_list_lt_CantResolve_gt___add_typealias_to_yaml" ) @@ -71,6 +71,10 @@ def test_probe_alias_name_distinguishes_namespaces_from_templates(): assert probe_alias_name("A::B") != probe_alias_name("A") +def test_probe_alias_name_distinguishes_general_sanitizer_collisions(): + assert probe_alias_name("A") != probe_alias_name("A_B") + + def test_helper_uses_deferred_annotations_for_python_38_runtime_compatibility(): source = Path(typealias_probe.__file__).read_text() assert source.startswith("from __future__ import annotations\n") From 8ddc8f3d6bb922416fbc1c5e1b9773452301bc52 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 10 Jul 2026 06:33:20 +0000 Subject: [PATCH 06/26] Collect typealias probes while parsing --- src/semiwrap/autowrap/context.py | 9 ++++++++ src/semiwrap/autowrap/cxxparser.py | 12 ++++++++++ tests/test_typealias_probe.py | 35 ++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/src/semiwrap/autowrap/context.py b/src/semiwrap/autowrap/context.py index 769e199..5c03ddd 100644 --- a/src/semiwrap/autowrap/context.py +++ b/src/semiwrap/autowrap/context.py @@ -489,6 +489,11 @@ class ClassContext: #: Extra autodetected 'using' directives auto_typealias: typing.List[str] = field(default_factory=list) + #: Best-effort C++ type spellings that should be probed before generated + #: code uses them. These produce clearer compile-time diagnostics when a + #: YAML typealias entry is missing. + typealias_probes: typing.List[str] = field(default_factory=list) + #: vcheck are various static asserts that check things about the #: inline functions vcheck_fns: typing.List[FunctionContext] = field(default_factory=list) @@ -568,6 +573,10 @@ class HeaderContext: type_caster_includes: typing.List[str] = field(default_factory=list) user_typealias: typing.List[str] = field(default_factory=list) + #: Best-effort C++ type spellings that should be probed before generated + #: global binding code uses them. + typealias_probes: typing.List[str] = field(default_factory=list) + using_ns: typing.List[str] = field(default_factory=list) # All namespaces that occur in the file diff --git a/src/semiwrap/autowrap/cxxparser.py b/src/semiwrap/autowrap/cxxparser.py index 2af70ea..94726e1 100644 --- a/src/semiwrap/autowrap/cxxparser.py +++ b/src/semiwrap/autowrap/cxxparser.py @@ -91,6 +91,7 @@ TemplateInstanceContext, TrampolineData, ) +from .typealias_probe import add_typealias_probe, collect_typealias_probes from ..name_transform import NameTransforms, resolve_name_transforms from ..util import relpath_walk_up @@ -446,6 +447,11 @@ def on_function(self, state: AWNonClassBlockState, fn: Function) -> None: fn, data, fn_name, scope_var, False, overload_tracker ) fctx.namespace = state.user_data + for probe in collect_typealias_probes(fn.return_type): + add_typealias_probe(self.hctx.typealias_probes, probe) + for param in fn.parameters: + for probe in collect_typealias_probes(param.type): + add_typealias_probe(self.hctx.typealias_probes, probe) self.hctx.functions.append(fctx) def on_method_impl(self, state: AWNonClassBlockState, method: Method) -> None: @@ -1130,6 +1136,12 @@ def _on_class_method( overload_tracker, ) + for probe in collect_typealias_probes(method.return_type): + add_typealias_probe(cctx.typealias_probes, probe) + for param in method.parameters: + for probe in collect_typealias_probes(param.type): + add_typealias_probe(cctx.typealias_probes, probe) + # Update class-specific method attributes fctx.is_constructor = is_constructor if is_constructor and method_data.rename and not method_data.cpp_code: diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py index 6560e00..c6c70c5 100644 --- a/tests/test_typealias_probe.py +++ b/tests/test_typealias_probe.py @@ -78,3 +78,38 @@ def test_probe_alias_name_distinguishes_general_sanitizer_collisions(): def test_helper_uses_deferred_annotations_for_python_38_runtime_compatibility(): source = Path(typealias_probe.__file__).read_text() assert source.startswith("from __future__ import annotations\n") + + +import pathlib + +from cxxheaderparser.options import ParserOptions + +from semiwrap.autowrap.cxxparser import parse_header +from semiwrap.autowrap.generator_data import GeneratorData +from semiwrap.config.autowrap_yml import AutowrapConfigYaml + + +def parse_fixture_header(header_name: str, yaml_name: str): + root = pathlib.Path("tests/cpp/sw-test/src/swtest/ft/include") + yml = pathlib.Path("tests/cpp/sw-test/semiwrap/ft") / yaml_name + cfg = AutowrapConfigYaml.from_file(yml) + return parse_header( + header_name, + root / header_name, + root, + GeneratorData(cfg, yml), + ParserOptions(), + {}, + False, + ) + + +def test_parse_header_collects_global_function_typealias_probe(): + hctx = parse_fixture_header("using.h", "using.yml") + assert "AlsoCantResolve" in hctx.typealias_probes + + +def test_parse_header_collects_class_constructor_typealias_probe(): + hctx = parse_fixture_header("using.h", "using.yml") + cls = next(c for c in hctx.classes if c.cpp_name == "ProtectedUsing") + assert "CantResolve" in cls.typealias_probes From 8a8c284bcca8d918d19a5054c930ac96aa7c5f67 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 10 Jul 2026 06:43:19 +0000 Subject: [PATCH 07/26] Render typealias probes in wrapper cpp --- src/semiwrap/autowrap/render_pybind11.py | 3 +++ src/semiwrap/autowrap/render_wrapped.py | 5 +++++ tests/test_typealias_probe.py | 22 +++++++++++++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/semiwrap/autowrap/render_pybind11.py b/src/semiwrap/autowrap/render_pybind11.py index 0f0275f..c88377f 100644 --- a/src/semiwrap/autowrap/render_pybind11.py +++ b/src/semiwrap/autowrap/render_pybind11.py @@ -10,6 +10,7 @@ GeneratedLambda, PropContext, ) +from .typealias_probe import render_typealias_probes def mkdoc(pre: str, doc: Documentation, post: str) -> str: @@ -303,6 +304,8 @@ def enum_def(r: RenderBuffer, varname: str, enum: EnumContext): def cls_user_using(r: RenderBuffer, cls: ClassContext): for typealias in cls.user_typealias: r.writeln(f"{typealias};") + if cls.typealias_probes: + render_typealias_probes(r, cls.typealias_probes) def cls_auto_using(r: RenderBuffer, cls: ClassContext): diff --git a/src/semiwrap/autowrap/render_wrapped.py b/src/semiwrap/autowrap/render_wrapped.py index 4eb5cb8..ba00ba1 100644 --- a/src/semiwrap/autowrap/render_wrapped.py +++ b/src/semiwrap/autowrap/render_wrapped.py @@ -3,6 +3,7 @@ from . import render_pybind11 as rpybind11 from .render_cls_prologue import render_class_prologue +from .typealias_probe import render_typealias_probes def render_wrapped_cpp(hctx: HeaderContext) -> str: @@ -28,6 +29,10 @@ def render_wrapped_cpp(hctx: HeaderContext) -> str: for typealias in hctx.user_typealias: r.writeln(f"{typealias};") + if hctx.typealias_probes: + r.writeln() + render_typealias_probes(r, hctx.typealias_probes) + # # Ordering of the initialization function # diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py index c6c70c5..0992400 100644 --- a/tests/test_typealias_probe.py +++ b/tests/test_typealias_probe.py @@ -12,6 +12,7 @@ probe_alias_name, render_typealias_probes, ) +from semiwrap.autowrap.render_wrapped import render_wrapped_cpp def probes_for(type_text: str) -> list[str]: @@ -94,7 +95,7 @@ def parse_fixture_header(header_name: str, yaml_name: str): yml = pathlib.Path("tests/cpp/sw-test/semiwrap/ft") / yaml_name cfg = AutowrapConfigYaml.from_file(yml) return parse_header( - header_name, + pathlib.Path(header_name).stem, root / header_name, root, GeneratorData(cfg, yml), @@ -113,3 +114,22 @@ def test_parse_header_collects_class_constructor_typealias_probe(): hctx = parse_fixture_header("using.h", "using.yml") cls = next(c for c in hctx.classes if c.cpp_name == "ProtectedUsing") assert "CantResolve" in cls.typealias_probes + + +def test_render_wrapped_cpp_emits_global_typealias_probe_before_initializer(): + hctx = parse_fixture_header("using.h", "using.yml") + out = render_wrapped_cpp(hctx) + probe = "semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" + assert probe in out + assert out.index(probe) < out.index("struct semiwrap_using_initializer") + assert "using semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" in out + assert "= AlsoCantResolve;" in out + + +def test_render_wrapped_cpp_emits_class_typealias_probe_inside_initializer(): + hctx = parse_fixture_header("using.h", "using.yml") + out = render_wrapped_cpp(hctx) + probe = "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" + assert probe in out + assert out.index("struct semiwrap_using_initializer") < out.index(probe) + assert out.index(probe) < out.index("py::class_ Date: Fri, 10 Jul 2026 06:49:47 +0000 Subject: [PATCH 08/26] Deduplicate typealias probes per cpp scope --- src/semiwrap/autowrap/render_pybind11.py | 17 +++++++++++++++-- src/semiwrap/autowrap/render_wrapped.py | 10 ++++++---- tests/test_typealias_probe.py | 10 ++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/semiwrap/autowrap/render_pybind11.py b/src/semiwrap/autowrap/render_pybind11.py index c88377f..d6a9e0f 100644 --- a/src/semiwrap/autowrap/render_pybind11.py +++ b/src/semiwrap/autowrap/render_pybind11.py @@ -304,8 +304,21 @@ def enum_def(r: RenderBuffer, varname: str, enum: EnumContext): def cls_user_using(r: RenderBuffer, cls: ClassContext): for typealias in cls.user_typealias: r.writeln(f"{typealias};") - if cls.typealias_probes: - render_typealias_probes(r, cls.typealias_probes) + + +def _collect_class_typealias_probes(cls: ClassContext, probes: T.Set[str]) -> None: + probes.update(cls.typealias_probes) + for ccls in cls.child_classes: + _collect_class_typealias_probes(ccls, probes) + + +def cls_typealias_probes(r: RenderBuffer, classes: T.Iterable[ClassContext]) -> None: + probes: T.Set[str] = set() + for cls in classes: + if not cls.template: + _collect_class_typealias_probes(cls, probes) + if probes: + render_typealias_probes(r, sorted(probes)) def cls_auto_using(r: RenderBuffer, cls: ClassContext): diff --git a/src/semiwrap/autowrap/render_wrapped.py b/src/semiwrap/autowrap/render_wrapped.py index ba00ba1..58265c4 100644 --- a/src/semiwrap/autowrap/render_wrapped.py +++ b/src/semiwrap/autowrap/render_wrapped.py @@ -29,10 +29,6 @@ def render_wrapped_cpp(hctx: HeaderContext) -> str: for typealias in hctx.user_typealias: r.writeln(f"{typealias};") - if hctx.typealias_probes: - r.writeln() - render_typealias_probes(r, hctx.typealias_probes) - # # Ordering of the initialization function # @@ -57,6 +53,10 @@ def render_wrapped_cpp(hctx: HeaderContext) -> str: for ns in hctx.namespaces: r.writeln(f"using namespace {ns};") + if hctx.typealias_probes: + r.writeln() + render_typealias_probes(r, hctx.typealias_probes) + r.writeln(f"\nstruct semiwrap_{hctx.hname}_initializer {{\n") with r.indent(): @@ -65,6 +65,8 @@ def render_wrapped_cpp(hctx: HeaderContext) -> str: rpybind11.cls_user_using(r, cls) rpybind11.cls_consts(r, cls) + rpybind11.cls_typealias_probes(r, hctx.classes) + if hctx.subpackages: r.writeln() for vname in hctx.subpackages.values(): diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py index 0992400..5a54f6f 100644 --- a/tests/test_typealias_probe.py +++ b/tests/test_typealias_probe.py @@ -121,6 +121,7 @@ def test_render_wrapped_cpp_emits_global_typealias_probe_before_initializer(): out = render_wrapped_cpp(hctx) probe = "semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" assert probe in out + assert out.index("using namespace u;") < out.index(probe) assert out.index(probe) < out.index("struct semiwrap_using_initializer") assert "using semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" in out assert "= AlsoCantResolve;" in out @@ -133,3 +134,12 @@ def test_render_wrapped_cpp_emits_class_typealias_probe_inside_initializer(): assert probe in out assert out.index("struct semiwrap_using_initializer") < out.index(probe) assert out.index(probe) < out.index("py::class_ Date: Fri, 10 Jul 2026 06:53:43 +0000 Subject: [PATCH 09/26] Skip template child typealias probes --- src/semiwrap/autowrap/render_pybind11.py | 3 ++- tests/test_typealias_probe.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/semiwrap/autowrap/render_pybind11.py b/src/semiwrap/autowrap/render_pybind11.py index d6a9e0f..1dbb8a1 100644 --- a/src/semiwrap/autowrap/render_pybind11.py +++ b/src/semiwrap/autowrap/render_pybind11.py @@ -309,7 +309,8 @@ def cls_user_using(r: RenderBuffer, cls: ClassContext): def _collect_class_typealias_probes(cls: ClassContext, probes: T.Set[str]) -> None: probes.update(cls.typealias_probes) for ccls in cls.child_classes: - _collect_class_typealias_probes(ccls, probes) + if not ccls.template: + _collect_class_typealias_probes(ccls, probes) def cls_typealias_probes(r: RenderBuffer, classes: T.Iterable[ClassContext]) -> None: diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py index 5a54f6f..19835af 100644 --- a/tests/test_typealias_probe.py +++ b/tests/test_typealias_probe.py @@ -87,6 +87,7 @@ def test_helper_uses_deferred_annotations_for_python_38_runtime_compatibility(): from semiwrap.autowrap.cxxparser import parse_header from semiwrap.autowrap.generator_data import GeneratorData +from semiwrap.autowrap.context import ClassTemplateData from semiwrap.config.autowrap_yml import AutowrapConfigYaml @@ -143,3 +144,17 @@ def test_render_wrapped_cpp_deduplicates_class_typealias_probes_in_initializer_s out = render_wrapped_cpp(hctx) probe = "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" assert out.count(f"using {probe}") == 1 + + +def test_render_wrapped_cpp_skips_templated_child_typealias_probes(): + hctx = parse_fixture_header("using.h", "using.yml") + parent = next(c for c in hctx.classes if c.cpp_name == "ProtectedUsing") + child_template = next(c for c in hctx.classes if c.cpp_name == "FwdDecl") + child_template.template = ClassTemplateData("", "", "") + child_template.typealias_probes.append("ChildTemplateProbe") + parent.child_classes.append(child_template) + hctx.classes = [parent] + + out = render_wrapped_cpp(hctx) + + assert "semiwrap_typealias_probe_ChildTemplateProbe__add_typealias_to_yaml" not in out From 0d6fcf2eac30e996b4259db264252c977394f5d1 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 10 Jul 2026 07:04:21 +0000 Subject: [PATCH 10/26] Render typealias probes in trampolines --- src/semiwrap/autowrap/render_cls_trampoline_hpp.py | 4 ++++ tests/test_typealias_probe.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/semiwrap/autowrap/render_cls_trampoline_hpp.py b/src/semiwrap/autowrap/render_cls_trampoline_hpp.py index cfb0c6e..807245f 100644 --- a/src/semiwrap/autowrap/render_cls_trampoline_hpp.py +++ b/src/semiwrap/autowrap/render_cls_trampoline_hpp.py @@ -7,6 +7,7 @@ TrampolineData, ) from .mangle import trampoline_signature +from .typealias_probe import render_typealias_probes from . import render_pybind11 as rpybind11 @@ -222,6 +223,9 @@ def _render_cls_trampoline( for typealias in cls.auto_typealias: r.writeln(f"{typealias};") + if cls.typealias_probes: + render_typealias_probes(r, cls.typealias_probes) + if cls.constants: r.writeln() for name, constant in cls.constants: diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py index 19835af..66f14c1 100644 --- a/tests/test_typealias_probe.py +++ b/tests/test_typealias_probe.py @@ -13,6 +13,7 @@ render_typealias_probes, ) from semiwrap.autowrap.render_wrapped import render_wrapped_cpp +from semiwrap.autowrap.render_cls_trampoline_hpp import render_cls_trampoline_hpp def probes_for(type_text: str) -> list[str]: @@ -158,3 +159,12 @@ def test_render_wrapped_cpp_skips_templated_child_typealias_probes(): out = render_wrapped_cpp(hctx) assert "semiwrap_typealias_probe_ChildTemplateProbe__add_typealias_to_yaml" not in out + + +def test_render_trampoline_hpp_emits_class_typealias_probe(): + hctx = parse_fixture_header("using.h", "using.yml") + cls = next(c for c in hctx.classes if c.cpp_name == "ProtectedUsing") + out = render_cls_trampoline_hpp(hctx, cls) + probe = "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" + assert probe in out + assert out.index(probe) < out.index("PyTrampoline_ProtectedUsing(CantResolve") From b3e3abb7722e1adf236a0f69aa831fa6c3a334a0 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 10 Jul 2026 07:13:07 +0000 Subject: [PATCH 11/26] Fix typealias probe fixture false positives --- src/semiwrap/autowrap/cxxparser.py | 50 +++++++++++++++++++++++------- tests/test_typealias_probe.py | 12 +++++++ 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/semiwrap/autowrap/cxxparser.py b/src/semiwrap/autowrap/cxxparser.py index 94726e1..e8017b1 100644 --- a/src/semiwrap/autowrap/cxxparser.py +++ b/src/semiwrap/autowrap/cxxparser.py @@ -369,6 +369,21 @@ def __init__( ) self.types = set() self.user_types = set() + self.header_typealias_names: typing.Set[str] = set() + self._extract_typealias( + self.user_cfg.typealias, self.hctx.user_typealias, self.header_typealias_names + ) + + def _add_matching_typealias_probes( + self, + probes: typing.List[str], + dtype: typing.Optional[typing.Union[DecoratedType, FunctionType]], + typealias_names: typing.Set[str], + ) -> None: + for probe in collect_typealias_probes(dtype): + probe_name = probe.split("::")[-1].split("<", 1)[0].strip() + if probe_name in typealias_names: + add_typealias_probe(probes, probe) # # Visitor interface @@ -447,11 +462,17 @@ def on_function(self, state: AWNonClassBlockState, fn: Function) -> None: fn, data, fn_name, scope_var, False, overload_tracker ) fctx.namespace = state.user_data - for probe in collect_typealias_probes(fn.return_type): - add_typealias_probe(self.hctx.typealias_probes, probe) + self._add_matching_typealias_probes( + self.hctx.typealias_probes, + fn.return_type, + self.header_typealias_names, + ) for param in fn.parameters: - for probe in collect_typealias_probes(param.type): - add_typealias_probe(self.hctx.typealias_probes, probe) + self._add_matching_typealias_probes( + self.hctx.typealias_probes, + param.type, + self.header_typealias_names, + ) self.hctx.functions.append(fctx) def on_method_impl(self, state: AWNonClassBlockState, method: Method) -> None: @@ -1136,11 +1157,17 @@ def _on_class_method( overload_tracker, ) - for probe in collect_typealias_probes(method.return_type): - add_typealias_probe(cctx.typealias_probes, probe) + self._add_matching_typealias_probes( + cctx.typealias_probes, + method.return_type, + cdata.typealias_names, + ) for param in method.parameters: - for probe in collect_typealias_probes(param.type): - add_typealias_probe(cctx.typealias_probes, probe) + self._add_matching_typealias_probes( + cctx.typealias_probes, + param.type, + cdata.typealias_names, + ) # Update class-specific method attributes fctx.is_constructor = is_constructor @@ -2004,6 +2031,10 @@ def _extract_typealias( for typealias in in_ta: if typealias.startswith("template"): out_ta.append(typealias) + using_pos = typealias.find(" using ") + if using_pos != -1: + ta_name = typealias[using_pos + 7 :].split("=", 1)[0].strip() + ta_names.add(ta_name.split("<", 1)[0].strip()) else: teq = typealias.find("=") if teq != -1: @@ -2204,9 +2235,6 @@ def parse_header( if isinstance(param, str): visitor._add_user_type_caster(param) - # User typealias additions - visitor._extract_typealias(user_cfg.typealias, hctx.user_typealias, set()) - # Type caster visitor._set_type_caster_includes() diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py index 66f14c1..2d7a3b6 100644 --- a/tests/test_typealias_probe.py +++ b/tests/test_typealias_probe.py @@ -118,6 +118,18 @@ def test_parse_header_collects_class_constructor_typealias_probe(): assert "CantResolve" in cls.typealias_probes +def test_parse_header_skips_autogenerated_inner_alias_typealias_probes(): + hctx = parse_fixture_header("retval.h", "retval.yml") + cls = next(c for c in hctx.classes if c.cpp_name == "RetvalClass") + assert cls.typealias_probes == [] + + +def test_parse_header_skips_embedded_using_alias_typealias_probes(): + hctx = parse_fixture_header("using2.h", "using2.yml") + cls = next(c for c in hctx.classes if c.cpp_name == "Using1") + assert cls.typealias_probes == [] + + def test_render_wrapped_cpp_emits_global_typealias_probe_before_initializer(): hctx = parse_fixture_header("using.h", "using.yml") out = render_wrapped_cpp(hctx) From f6cc933aaf227df0b01a2fcf11990c8262122d51 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 10 Jul 2026 07:13:11 +0000 Subject: [PATCH 12/26] Test generated typealias probes in fixture build --- tests/test_ft_misc.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_ft_misc.py b/tests/test_ft_misc.py index 9f60fd4..ebcce97 100644 --- a/tests/test_ft_misc.py +++ b/tests/test_ft_misc.py @@ -196,6 +196,31 @@ def test_using_fwddecl(): assert u.getX(f) == 43 +def test_using_generated_typealias_probes_present(): + from pathlib import Path + + root = Path(__file__).parent / "cpp" / "sw-test" / "build" + using_cpp_files = sorted(root.glob("*/semiwrap/using.cpp")) + assert using_cpp_files, "sw-test build did not generate semiwrap/using.cpp" + using_cpp = using_cpp_files[-1].read_text() + + assert "semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" in using_cpp + assert "using semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" in using_cpp + assert "= AlsoCantResolve;" in using_cpp + assert "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" in using_cpp + assert "= CantResolve;" in using_cpp + + trampoline_files = sorted( + root.glob("*/semiwrap/trampolines/cr__inner__ProtectedUsing.hpp") + ) + assert trampoline_files, ( + "sw-test build did not generate trampoline for cr::inner::ProtectedUsing" + ) + trampoline_hpp = trampoline_files[-1].read_text() + assert "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" in trampoline_hpp + assert "add a typealias entry for `CantResolve`" in trampoline_hpp + + # # virtual_xform.h # From 99270a7f30651fc4c67283cf22ea669bae2c3e52 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 10 Jul 2026 07:27:43 +0000 Subject: [PATCH 13/26] Address typealias probe review findings --- src/semiwrap/autowrap/cxxparser.py | 76 +++++++++++++++++++++--------- tests/test_typealias_probe.py | 49 +++++++++++++++++++ 2 files changed, 104 insertions(+), 21 deletions(-) diff --git a/src/semiwrap/autowrap/cxxparser.py b/src/semiwrap/autowrap/cxxparser.py index e8017b1..1228e8c 100644 --- a/src/semiwrap/autowrap/cxxparser.py +++ b/src/semiwrap/autowrap/cxxparser.py @@ -51,6 +51,7 @@ PQName, PQNameSegment, Reference, + TemplateDecl, TemplateInst, TemplateTypeParam, Type, @@ -298,6 +299,7 @@ class ClassStateData(typing.NamedTuple): data: ClassData typealias_names: typing.Set[str] + local_typealias_names: typing.Set[str] # have to defer processing these defer_protected_methods: typing.List[Method] @@ -369,22 +371,40 @@ def __init__( ) self.types = set() self.user_types = set() - self.header_typealias_names: typing.Set[str] = set() - self._extract_typealias( - self.user_cfg.typealias, self.hctx.user_typealias, self.header_typealias_names - ) + self._extract_typealias(self.user_cfg.typealias, self.hctx.user_typealias, set()) - def _add_matching_typealias_probes( + def _add_typealias_probes( self, probes: typing.List[str], dtype: typing.Optional[typing.Union[DecoratedType, FunctionType]], - typealias_names: typing.Set[str], + suppressed_names: typing.Set[str], ) -> None: for probe in collect_typealias_probes(dtype): - probe_name = probe.split("::")[-1].split("<", 1)[0].strip() - if probe_name in typealias_names: + if not any( + re.search( + rf"(? typing.Set[str]: + if template is None: + return set() + if isinstance(template, list): + template = template[-1] + return { + param.name + for param in template.params + if isinstance(param, TemplateTypeParam) and param.name + } + # # Visitor interface # @@ -462,16 +482,17 @@ def on_function(self, state: AWNonClassBlockState, fn: Function) -> None: fn, data, fn_name, scope_var, False, overload_tracker ) fctx.namespace = state.user_data - self._add_matching_typealias_probes( + suppressed_names = self._template_type_param_names(fn.template) + self._add_typealias_probes( self.hctx.typealias_probes, fn.return_type, - self.header_typealias_names, + suppressed_names, ) for param in fn.parameters: - self._add_matching_typealias_probes( + self._add_typealias_probes( self.hctx.typealias_probes, param.type, - self.header_typealias_names, + suppressed_names, ) self.hctx.functions.append(fctx) @@ -503,6 +524,7 @@ def on_using_alias(self, state: AWState, using: UsingAlias) -> None: ctx.auto_typealias.append( f"using {using.alias} [[maybe_unused]] = typename {ctx.full_cpp_name}::{using.alias}" ) + state.user_data.local_typealias_names.add(using.alias) def on_using_declaration(self, state: AWState, using: UsingDecl) -> None: self._add_type_caster_pqname(using.typename) @@ -629,6 +651,8 @@ def on_enum(self, state: AWState, enum: EnumDecl) -> None: inline_code=enum_data.inline_code, ) ) + if ename and isinstance(user_data, ClassStateData): + user_data.local_typealias_names.add(ename) # # Class/union/struct @@ -814,15 +838,22 @@ def on_class_start(self, state: AWClassBlockState) -> typing.Optional[bool]: # Add to parent class or global class list if parent_ctx: parent_ctx.child_classes.append(ctx) + if isinstance(state.parent.user_data, ClassStateData): + state.parent.user_data.local_typealias_names.add(cls_name) else: self.hctx.classes.append(ctx) + local_typealias_names = {cls_name} + for param in class_data.template_params or []: + local_typealias_names.add(param.split()[-1]) + # Store for other events to use state.user_data = ClassStateData( ctx=ctx, cls_key=cls_key, data=class_data, typealias_names=typealias_names, + local_typealias_names=local_typealias_names, # Method data defer_protected_methods=[], defer_private_nonvirtual_methods=[], @@ -1157,17 +1188,20 @@ def _on_class_method( overload_tracker, ) - self._add_matching_typealias_probes( - cctx.typealias_probes, - method.return_type, - cdata.typealias_names, - ) - for param in method.parameters: - self._add_matching_typealias_probes( + if is_constructor or state.access != "public" or is_virtual or fctx.is_overloaded: + suppressed_names = set(cdata.local_typealias_names) + suppressed_names.update(self._template_type_param_names(method.template)) + self._add_typealias_probes( cctx.typealias_probes, - param.type, - cdata.typealias_names, + method.return_type, + suppressed_names, ) + for param in method.parameters: + self._add_typealias_probes( + cctx.typealias_probes, + param.type, + suppressed_names, + ) # Update class-specific method attributes fctx.is_constructor = is_constructor diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py index 2d7a3b6..57020e0 100644 --- a/tests/test_typealias_probe.py +++ b/tests/test_typealias_probe.py @@ -107,6 +107,20 @@ def parse_fixture_header(header_name: str, yaml_name: str): ) +def parse_fixture_header_with_yaml(header_name: str, yml: pathlib.Path): + root = pathlib.Path("tests/cpp/sw-test/src/swtest/ft/include") + cfg = AutowrapConfigYaml.from_file(yml) + return parse_header( + pathlib.Path(header_name).stem, + root / header_name, + root, + GeneratorData(cfg, yml), + ParserOptions(), + {}, + False, + ) + + def test_parse_header_collects_global_function_typealias_probe(): hctx = parse_fixture_header("using.h", "using.yml") assert "AlsoCantResolve" in hctx.typealias_probes @@ -130,6 +144,41 @@ def test_parse_header_skips_embedded_using_alias_typealias_probes(): assert cls.typealias_probes == [] +def test_parse_header_skips_class_template_parameter_typealias_probes(): + hctx = parse_fixture_header("templates/tvchild.h", "tvchild.yml") + cls = next(c for c in hctx.classes if c.cpp_name == "TVChild") + assert cls.typealias_probes == [] + + +def test_parse_header_collects_missing_yaml_typealias_probes(tmp_path): + yml = tmp_path / "using_missing_typealias.yml" + yml.write_text( + """ +classes: + cr::inner::ProtectedUsing: + methods: + ProtectedUsing: + overloads: + "": + CantResolve: + u::FwdDecl: + attributes: + x: +functions: + fn_using: + overloads: + AlsoCantResolve: + std::string: +""" + ) + + hctx = parse_fixture_header_with_yaml("using.h", yml) + cls = next(c for c in hctx.classes if c.cpp_name == "ProtectedUsing") + + assert "AlsoCantResolve" in hctx.typealias_probes + assert "CantResolve" in cls.typealias_probes + + def test_render_wrapped_cpp_emits_global_typealias_probe_before_initializer(): hctx = parse_fixture_header("using.h", "using.yml") out = render_wrapped_cpp(hctx) From 961e3890f1ddd93ee1505b312bc4c8d9acf41257 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 10 Jul 2026 07:37:54 +0000 Subject: [PATCH 14/26] Fix typealias probe template suppression --- src/semiwrap/autowrap/cxxparser.py | 12 ++--- tests/test_typealias_probe.py | 70 +++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/src/semiwrap/autowrap/cxxparser.py b/src/semiwrap/autowrap/cxxparser.py index 1228e8c..96dc3a9 100644 --- a/src/semiwrap/autowrap/cxxparser.py +++ b/src/semiwrap/autowrap/cxxparser.py @@ -53,6 +53,7 @@ Reference, TemplateDecl, TemplateInst, + TemplateNonTypeParam, TemplateTypeParam, Type, Typedef, @@ -380,13 +381,7 @@ def _add_typealias_probes( suppressed_names: typing.Set[str], ) -> None: for probe in collect_typealias_probes(dtype): - if not any( - re.search( - rf"(? +struct DependentAliasHolder { + DependentAliasHolder(MissingAlias value); +}; +""", + """ +classes: + DependentAliasHolder: + template_params: + - typename N + methods: + DependentAliasHolder: + overloads: + MissingAlias: +""", + ) + + cls = next(c for c in hctx.classes if c.cpp_name == "DependentAliasHolder") + assert cls.typealias_probes == ["MissingAlias"] + + +def test_parse_header_suppresses_non_type_function_template_parameter_probe(tmp_path): + hctx = parse_tmp_header( + tmp_path, + "non_type_template_param", + """ +#pragma once + +template +void takes_value_template(TVParam value); +""", + """ +functions: + takes_value_template: + cpp_code: "[](auto) {}" +""", + ) + + assert "N" not in hctx.typealias_probes + assert "TVParam" in hctx.typealias_probes + + def test_render_wrapped_cpp_emits_global_typealias_probe_before_initializer(): hctx = parse_fixture_header("using.h", "using.yml") out = render_wrapped_cpp(hctx) From 71fa185e4b3e63f6a10573a60faa1f3be8518da7 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 10 Jul 2026 07:46:01 +0000 Subject: [PATCH 15/26] Fix typealias probe verification issues --- tests/run_tests.py | 6 ++++-- tests/test_typealias_probe.py | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/run_tests.py b/tests/run_tests.py index aaa3a7c..cd99537 100755 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -7,11 +7,13 @@ if __name__ == "__main__": root = abspath(dirname(__file__)) + project_root = dirname(root) os.chdir(root) # Install cpp project wcpp = join(root, "cpp", "run_install.py") - subprocess.check_call([sys.executable, wcpp] + sys.argv[1:]) + subprocess.check_call([sys.executable, wcpp]) # Run pytest - subprocess.check_call([sys.executable, "-m", "pytest"]) + os.chdir(project_root) + subprocess.check_call([sys.executable, "-m", "pytest"] + sys.argv[1:]) diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py index d55dd51..18e4274 100644 --- a/tests/test_typealias_probe.py +++ b/tests/test_typealias_probe.py @@ -84,6 +84,8 @@ def test_helper_uses_deferred_annotations_for_python_38_runtime_compatibility(): import pathlib +PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[1] + from cxxheaderparser.options import ParserOptions from semiwrap.autowrap.cxxparser import parse_header @@ -93,8 +95,8 @@ def test_helper_uses_deferred_annotations_for_python_38_runtime_compatibility(): def parse_fixture_header(header_name: str, yaml_name: str): - root = pathlib.Path("tests/cpp/sw-test/src/swtest/ft/include") - yml = pathlib.Path("tests/cpp/sw-test/semiwrap/ft") / yaml_name + root = PROJECT_ROOT / "tests/cpp/sw-test/src/swtest/ft/include" + yml = PROJECT_ROOT / "tests/cpp/sw-test/semiwrap/ft" / yaml_name cfg = AutowrapConfigYaml.from_file(yml) return parse_header( pathlib.Path(header_name).stem, @@ -108,7 +110,7 @@ def parse_fixture_header(header_name: str, yaml_name: str): def parse_fixture_header_with_yaml(header_name: str, yml: pathlib.Path): - root = pathlib.Path("tests/cpp/sw-test/src/swtest/ft/include") + root = PROJECT_ROOT / "tests/cpp/sw-test/src/swtest/ft/include" cfg = AutowrapConfigYaml.from_file(yml) return parse_header( pathlib.Path(header_name).stem, From 78488cfc9f787eec5b21738d99e55f4b4eac7141 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 10 Jul 2026 08:10:22 +0000 Subject: [PATCH 16/26] Fix typealias probe final review findings --- src/semiwrap/autowrap/cxxparser.py | 50 +++++++++++++++-- src/semiwrap/autowrap/typealias_probe.py | 2 +- tests/test_typealias_probe.py | 69 +++++++++++++++++++++++- 3 files changed, 115 insertions(+), 6 deletions(-) diff --git a/src/semiwrap/autowrap/cxxparser.py b/src/semiwrap/autowrap/cxxparser.py index 96dc3a9..bf8732b 100644 --- a/src/semiwrap/autowrap/cxxparser.py +++ b/src/semiwrap/autowrap/cxxparser.py @@ -379,10 +379,37 @@ def _add_typealias_probes( probes: typing.List[str], dtype: typing.Optional[typing.Union[DecoratedType, FunctionType]], suppressed_names: typing.Set[str], + dependent_suppressed_names: typing.Optional[typing.Set[str]] = None, + suppressed_template_aliases: typing.Optional[typing.Set[str]] = None, ) -> None: + dependent_suppressed_names = dependent_suppressed_names or set() + suppressed_template_aliases = suppressed_template_aliases or set() for probe in collect_typealias_probes(dtype): - if probe not in suppressed_names: - add_typealias_probe(probes, probe) + if probe in suppressed_names: + continue + if self._probe_references_suppressed_name( + probe, dependent_suppressed_names + ): + continue + if self._probe_uses_suppressed_template_alias( + probe, suppressed_template_aliases + ): + continue + add_typealias_probe(probes, probe) + + def _probe_references_suppressed_name( + self, probe: str, suppressed_names: typing.Set[str] + ) -> bool: + for name in suppressed_names: + pattern = rf"(? bool: + return any(probe.startswith(f"{name}<") for name in suppressed_aliases) def _template_type_param_names( self, @@ -483,12 +510,14 @@ def on_function(self, state: AWNonClassBlockState, fn: Function) -> None: self.hctx.typealias_probes, fn.return_type, suppressed_names, + suppressed_names, ) for param in fn.parameters: self._add_typealias_probes( self.hctx.typealias_probes, param.type, suppressed_names, + suppressed_names, ) self.hctx.functions.append(fctx) @@ -1184,19 +1213,32 @@ def _on_class_method( overload_tracker, ) - if is_constructor or state.access != "public" or is_virtual or fctx.is_overloaded: + if ( + is_constructor + or state.access != "public" + or is_virtual + or fctx.is_overloaded + or fctx.genlambda + ): suppressed_names = set(cdata.local_typealias_names) - suppressed_names.update(self._template_type_param_names(method.template)) + method_template_names = self._template_type_param_names(method.template) + suppressed_names.update(method_template_names) + suppressed_template_aliases = set(cdata.local_typealias_names) + suppressed_template_aliases.update(cdata.typealias_names) self._add_typealias_probes( cctx.typealias_probes, method.return_type, suppressed_names, + method_template_names, + suppressed_template_aliases, ) for param in method.parameters: self._add_typealias_probes( cctx.typealias_probes, param.type, suppressed_names, + method_template_names, + suppressed_template_aliases, ) # Update class-specific method attributes diff --git a/src/semiwrap/autowrap/typealias_probe.py b/src/semiwrap/autowrap/typealias_probe.py index 37fe4c1..943b65a 100644 --- a/src/semiwrap/autowrap/typealias_probe.py +++ b/src/semiwrap/autowrap/typealias_probe.py @@ -164,7 +164,7 @@ def probe_alias_name(target: str) -> str: def render_typealias_probes( r: RenderBuffer, probes: T.Sequence[str], *, indent: str = "" ) -> None: - for target in sorted(probes): + for target in sorted(set(probes)): alias = probe_alias_name(target) r.writeln( f"{indent}// semiwrap diagnostic: if this line fails because `{target}` " diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py index 18e4274..a5a79fb 100644 --- a/tests/test_typealias_probe.py +++ b/tests/test_typealias_probe.py @@ -69,6 +69,15 @@ def test_render_typealias_probes_emits_comment_and_sorted_using_lines(): ) in out +def test_render_typealias_probes_deduplicates_duplicate_input(): + r = RenderBuffer() + render_typealias_probes(r, ["CantResolve", "CantResolve"]) + out = r.getvalue() + assert out.count( + "using semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" + ) == 1 + + def test_probe_alias_name_distinguishes_namespaces_from_templates(): assert probe_alias_name("A::B") != probe_alias_name("A") @@ -163,6 +172,12 @@ def test_parse_header_skips_embedded_using_alias_typealias_probes(): assert cls.typealias_probes == [] +def test_parse_header_skips_local_user_alias_template_typealias_probes(): + hctx = parse_fixture_header("using2.h", "using2.yml") + cls = next(c for c in hctx.classes if c.cpp_name == "Using3") + assert "fancy_list" not in cls.typealias_probes + + def test_parse_header_suppresses_class_template_parameter_probe(): hctx = parse_fixture_header("templates/tvchild.h", "tvchild.yml") cls = next(c for c in hctx.classes if c.cpp_name == "TVChild") @@ -244,7 +259,59 @@ def test_parse_header_suppresses_non_type_function_template_parameter_probe(tmp_ ) assert "N" not in hctx.typealias_probes - assert "TVParam" in hctx.typealias_probes + assert "TVParam" not in hctx.typealias_probes + + +def test_parse_header_suppresses_method_template_dependent_alias_probe(tmp_path): + hctx = parse_tmp_header( + tmp_path, + "method_template_param", + """ +#pragma once + +struct MethodTemplateProbe { + virtual ~MethodTemplateProbe() = default; + +protected: + template + void hidden(Alias value) {} +}; +""", + """ +classes: + MethodTemplateProbe: + methods: + hidden: + cpp_code: "[](auto&, auto) {}" +""", + ) + + cls = next(c for c in hctx.classes if c.cpp_name == "MethodTemplateProbe") + assert "T" not in cls.typealias_probes + assert "Alias" not in cls.typealias_probes + + +def test_parse_header_collects_public_generated_lambda_method_probe(tmp_path): + hctx = parse_tmp_header( + tmp_path, + "generated_lambda_probe", + """ +#pragma once + +struct GeneratedLambdaProbe { + void needs_lambda(MissingLambdaAlias value, int* out) {} +}; +""", + """ +classes: + GeneratedLambdaProbe: + methods: + needs_lambda: +""", + ) + + cls = next(c for c in hctx.classes if c.cpp_name == "GeneratedLambdaProbe") + assert "MissingLambdaAlias" in cls.typealias_probes def test_render_wrapped_cpp_emits_global_typealias_probe_before_initializer(): From 88ec5140ba26879baf0f640864106869882043f8 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sat, 11 Jul 2026 05:39:37 +0000 Subject: [PATCH 17/26] Fix typealias probe false positives --- src/semiwrap/autowrap/cxxparser.py | 58 ++++++++++++------ tests/test_typealias_probe.py | 94 ++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 17 deletions(-) diff --git a/src/semiwrap/autowrap/cxxparser.py b/src/semiwrap/autowrap/cxxparser.py index bf8732b..16b7879 100644 --- a/src/semiwrap/autowrap/cxxparser.py +++ b/src/semiwrap/autowrap/cxxparser.py @@ -409,7 +409,16 @@ def _probe_references_suppressed_name( def _probe_uses_suppressed_template_alias( self, probe: str, suppressed_aliases: typing.Set[str] ) -> bool: - return any(probe.startswith(f"{name}<") for name in suppressed_aliases) + return self._probe_references_suppressed_name(probe, suppressed_aliases) + + def _template_typealias_names(self, typealiases: typing.List[str]) -> typing.Set[str]: + names: typing.Set[str] = set() + for typealias in typealiases: + if typealias.startswith("template"): + m = re.search(r"\busing\s+([A-Za-z_]\w*)\b", typealias) + if m: + names.add(m.group(1)) + return names def _template_type_param_names( self, @@ -505,20 +514,6 @@ def on_function(self, state: AWNonClassBlockState, fn: Function) -> None: fn, data, fn_name, scope_var, False, overload_tracker ) fctx.namespace = state.user_data - suppressed_names = self._template_type_param_names(fn.template) - self._add_typealias_probes( - self.hctx.typealias_probes, - fn.return_type, - suppressed_names, - suppressed_names, - ) - for param in fn.parameters: - self._add_typealias_probes( - self.hctx.typealias_probes, - param.type, - suppressed_names, - suppressed_names, - ) self.hctx.functions.append(fctx) def on_method_impl(self, state: AWNonClassBlockState, method: Method) -> None: @@ -1213,18 +1208,27 @@ def _on_class_method( overload_tracker, ) + needs_trampoline_signature_probe = ( + is_virtual and not state.class_decl.final and not cdata.data.force_no_trampoline + ) if ( is_constructor or state.access != "public" - or is_virtual + or needs_trampoline_signature_probe or fctx.is_overloaded or fctx.genlambda ): suppressed_names = set(cdata.local_typealias_names) method_template_names = self._template_type_param_names(method.template) suppressed_names.update(method_template_names) + class_template_names = { + param.split()[-1] for param in cdata.data.template_params or [] + } suppressed_template_aliases = set(cdata.local_typealias_names) - suppressed_template_aliases.update(cdata.typealias_names) + suppressed_template_aliases.difference_update(class_template_names) + suppressed_template_aliases.update( + self._template_typealias_names(cdata.data.typealias) + ) self._add_typealias_probes( cctx.typealias_probes, method.return_type, @@ -2307,6 +2311,26 @@ def parse_header( if isinstance(param, str): visitor._add_user_type_caster(param) + # Global function typealias probes. Do this after parsing so overload + # trackers have seen every overload. + for fctx in hctx.functions: + if fctx.is_overloaded or fctx.genlambda: + fn = fctx._fn + suppressed_names = visitor._template_type_param_names(fn.template) + visitor._add_typealias_probes( + hctx.typealias_probes, + fn.return_type, + suppressed_names, + suppressed_names, + ) + for param in fn.parameters: + visitor._add_typealias_probes( + hctx.typealias_probes, + param.type, + suppressed_names, + suppressed_names, + ) + # Type caster visitor._set_type_caster_includes() diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py index a5a79fb..1bdf3be 100644 --- a/tests/test_typealias_probe.py +++ b/tests/test_typealias_probe.py @@ -154,6 +154,32 @@ def test_parse_header_collects_global_function_typealias_probe(): assert "AlsoCantResolve" in hctx.typealias_probes +def test_parse_header_skips_non_overloaded_global_direct_function_probe(tmp_path): + hctx = parse_tmp_header( + tmp_path, + "global_direct_function", + """ +#pragma once + +namespace parent { +struct ControlWord {}; +namespace child { +void set_program_state(const ControlWord& word); +} +} +""", + """ +classes: + parent::ControlWord: + ignore: true +functions: + set_program_state: +""", + ) + + assert "ControlWord" not in hctx.typealias_probes + + def test_parse_header_collects_class_constructor_typealias_probe(): hctx = parse_fixture_header("using.h", "using.yml") cls = next(c for c in hctx.classes if c.cpp_name == "ProtectedUsing") @@ -178,6 +204,43 @@ def test_parse_header_skips_local_user_alias_template_typealias_probes(): assert "fancy_list" not in cls.typealias_probes +def test_parse_header_skips_targets_with_local_alias_template_arguments(tmp_path): + hctx = parse_tmp_header( + tmp_path, + "nested_local_alias_arg", + """ +#pragma once + +namespace units { +template struct unit_t {}; +} + +struct NestedLocalAliasArg { + using Velocity = int; + using kv_unit = int; + + NestedLocalAliasArg(units::unit_t gain) {} + void calculate(units::unit_t velocity) {} +}; +""", + """ +classes: + units::unit_t: + ignore: true + NestedLocalAliasArg: + methods: + NestedLocalAliasArg: + calculate: +""", + ) + + cls = next(c for c in hctx.classes if c.cpp_name == "NestedLocalAliasArg") + assert "Velocity" not in cls.typealias_probes + assert "kv_unit" not in cls.typealias_probes + assert "units::unit_t" not in cls.typealias_probes + assert "units::unit_t" not in cls.typealias_probes + + def test_parse_header_suppresses_class_template_parameter_probe(): hctx = parse_fixture_header("templates/tvchild.h", "tvchild.yml") cls = next(c for c in hctx.classes if c.cpp_name == "TVChild") @@ -291,6 +354,37 @@ def test_parse_header_suppresses_method_template_dependent_alias_probe(tmp_path) assert "Alias" not in cls.typealias_probes +def test_parse_header_skips_force_no_trampoline_virtual_method_probe(tmp_path): + hctx = parse_tmp_header( + tmp_path, + "force_no_trampoline_virtual", + """ +#pragma once + +struct ForceNoTrampolineBase { + struct ControlVector {}; + virtual const ControlVector& get() const = 0; +}; + +struct ForceNoTrampolineDerived : ForceNoTrampolineBase { + const ControlVector& get() const override; +}; +""", + """ +classes: + ForceNoTrampolineBase: + ignore: true + ForceNoTrampolineDerived: + force_no_trampoline: true + methods: + get: +""", + ) + + cls = next(c for c in hctx.classes if c.cpp_name == "ForceNoTrampolineDerived") + assert "ControlVector" not in cls.typealias_probes + + def test_parse_header_collects_public_generated_lambda_method_probe(tmp_path): hctx = parse_tmp_header( tmp_path, From 5093fde9d8e8abf7415aa419d0bd573789e18212 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sat, 11 Jul 2026 06:02:19 +0000 Subject: [PATCH 18/26] Improve typealias diagnostic yaml path --- src/semiwrap/autowrap/render_pybind11.py | 10 ++++++++-- src/semiwrap/autowrap/render_wrapped.py | 4 ++-- src/semiwrap/autowrap/typealias_probe.py | 21 +++++++++++++++++---- tests/test_typealias_probe.py | 13 +++++++++++++ 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/semiwrap/autowrap/render_pybind11.py b/src/semiwrap/autowrap/render_pybind11.py index 1dbb8a1..bb38eaf 100644 --- a/src/semiwrap/autowrap/render_pybind11.py +++ b/src/semiwrap/autowrap/render_pybind11.py @@ -1,4 +1,5 @@ import inspect +import pathlib import typing as T from .buffer import RenderBuffer @@ -313,13 +314,18 @@ def _collect_class_typealias_probes(cls: ClassContext, probes: T.Set[str]) -> No _collect_class_typealias_probes(ccls, probes) -def cls_typealias_probes(r: RenderBuffer, classes: T.Iterable[ClassContext]) -> None: +def cls_typealias_probes( + r: RenderBuffer, + classes: T.Iterable[ClassContext], + *, + yaml_path: str | pathlib.Path | None = None, +) -> None: probes: T.Set[str] = set() for cls in classes: if not cls.template: _collect_class_typealias_probes(cls, probes) if probes: - render_typealias_probes(r, sorted(probes)) + render_typealias_probes(r, sorted(probes), yaml_path=yaml_path) def cls_auto_using(r: RenderBuffer, cls: ClassContext): diff --git a/src/semiwrap/autowrap/render_wrapped.py b/src/semiwrap/autowrap/render_wrapped.py index 58265c4..f4375b4 100644 --- a/src/semiwrap/autowrap/render_wrapped.py +++ b/src/semiwrap/autowrap/render_wrapped.py @@ -55,7 +55,7 @@ def render_wrapped_cpp(hctx: HeaderContext) -> str: if hctx.typealias_probes: r.writeln() - render_typealias_probes(r, hctx.typealias_probes) + render_typealias_probes(r, hctx.typealias_probes, yaml_path=hctx.orig_yaml) r.writeln(f"\nstruct semiwrap_{hctx.hname}_initializer {{\n") @@ -65,7 +65,7 @@ def render_wrapped_cpp(hctx: HeaderContext) -> str: rpybind11.cls_user_using(r, cls) rpybind11.cls_consts(r, cls) - rpybind11.cls_typealias_probes(r, hctx.classes) + rpybind11.cls_typealias_probes(r, hctx.classes, yaml_path=hctx.orig_yaml) if hctx.subpackages: r.writeln() diff --git a/src/semiwrap/autowrap/typealias_probe.py b/src/semiwrap/autowrap/typealias_probe.py index 943b65a..778e8cc 100644 --- a/src/semiwrap/autowrap/typealias_probe.py +++ b/src/semiwrap/autowrap/typealias_probe.py @@ -1,5 +1,6 @@ from __future__ import annotations +import pathlib import typing as T from cxxheaderparser.types import ( @@ -162,15 +163,27 @@ def probe_alias_name(target: str) -> str: def render_typealias_probes( - r: RenderBuffer, probes: T.Sequence[str], *, indent: str = "" + r: RenderBuffer, + probes: T.Sequence[str], + *, + indent: str = "", + yaml_path: str | pathlib.Path | None = None, ) -> None: + if yaml_path is not None: + yaml_target = pathlib.Path(yaml_path).resolve() + else: + yaml_target = None + for target in sorted(set(probes)): alias = probe_alias_name(target) r.writeln( f"{indent}// semiwrap diagnostic: if this line fails because `{target}` " "is unknown," ) - r.writeln( - f"{indent}// add a typealias entry for `{target}` to the semiwrap yaml file." - ) + if yaml_target is None: + r.writeln( + f"{indent}// add a typealias entry for `{target}` to the semiwrap yaml file." + ) + else: + r.writeln(f"{indent}// add a typealias entry for `{target}` to {yaml_target}.") r.writeln(f"{indent}using {alias} [[maybe_unused]] = {target};") diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py index 1bdf3be..b21deb6 100644 --- a/tests/test_typealias_probe.py +++ b/tests/test_typealias_probe.py @@ -69,6 +69,15 @@ def test_render_typealias_probes_emits_comment_and_sorted_using_lines(): ) in out +def test_render_typealias_probes_mentions_specific_yaml_when_provided(): + r = RenderBuffer() + yml = Path("/tmp/project/semiwrap/using.yml") + render_typealias_probes(r, ["CantResolve"], yaml_path=yml) + out = r.getvalue() + assert "add a typealias entry for `CantResolve` to /tmp/project/semiwrap/using.yml." in out + assert "to the semiwrap yaml file" not in out + + def test_render_typealias_probes_deduplicates_duplicate_input(): r = RenderBuffer() render_typealias_probes(r, ["CantResolve", "CantResolve"]) @@ -417,6 +426,7 @@ def test_render_wrapped_cpp_emits_global_typealias_probe_before_initializer(): assert out.index(probe) < out.index("struct semiwrap_using_initializer") assert "using semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" in out assert "= AlsoCantResolve;" in out + assert f"add a typealias entry for `AlsoCantResolve` to {hctx.orig_yaml}." in out def test_render_wrapped_cpp_emits_class_typealias_probe_inside_initializer(): @@ -426,6 +436,7 @@ def test_render_wrapped_cpp_emits_class_typealias_probe_inside_initializer(): assert probe in out assert out.index("struct semiwrap_using_initializer") < out.index(probe) assert out.index(probe) < out.index("py::class_ Date: Sat, 11 Jul 2026 06:16:20 +0000 Subject: [PATCH 19/26] Fix Python 3.8 type hint compatibility --- src/semiwrap/autowrap/render_pybind11.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/semiwrap/autowrap/render_pybind11.py b/src/semiwrap/autowrap/render_pybind11.py index bb38eaf..c57ca54 100644 --- a/src/semiwrap/autowrap/render_pybind11.py +++ b/src/semiwrap/autowrap/render_pybind11.py @@ -318,7 +318,7 @@ def cls_typealias_probes( r: RenderBuffer, classes: T.Iterable[ClassContext], *, - yaml_path: str | pathlib.Path | None = None, + yaml_path: T.Optional[T.Union[str, pathlib.Path]] = None, ) -> None: probes: T.Set[str] = set() for cls in classes: From eecef424696fba7d0d6dbfe6b21e244f614bf796 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sat, 11 Jul 2026 06:16:39 +0000 Subject: [PATCH 20/26] Formatting --- src/semiwrap/autowrap/cxxparser.py | 12 +++++++++--- src/semiwrap/autowrap/typealias_probe.py | 4 +++- tests/test_ft_misc.py | 19 +++++++++++++------ tests/test_typealias_probe.py | 20 ++++++++++++++------ 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/semiwrap/autowrap/cxxparser.py b/src/semiwrap/autowrap/cxxparser.py index 16b7879..a152ffe 100644 --- a/src/semiwrap/autowrap/cxxparser.py +++ b/src/semiwrap/autowrap/cxxparser.py @@ -372,7 +372,9 @@ def __init__( ) self.types = set() self.user_types = set() - self._extract_typealias(self.user_cfg.typealias, self.hctx.user_typealias, set()) + self._extract_typealias( + self.user_cfg.typealias, self.hctx.user_typealias, set() + ) def _add_typealias_probes( self, @@ -411,7 +413,9 @@ def _probe_uses_suppressed_template_alias( ) -> bool: return self._probe_references_suppressed_name(probe, suppressed_aliases) - def _template_typealias_names(self, typealiases: typing.List[str]) -> typing.Set[str]: + def _template_typealias_names( + self, typealiases: typing.List[str] + ) -> typing.Set[str]: names: typing.Set[str] = set() for typealias in typealiases: if typealias.startswith("template"): @@ -1209,7 +1213,9 @@ def _on_class_method( ) needs_trampoline_signature_probe = ( - is_virtual and not state.class_decl.final and not cdata.data.force_no_trampoline + is_virtual + and not state.class_decl.final + and not cdata.data.force_no_trampoline ) if ( is_constructor diff --git a/src/semiwrap/autowrap/typealias_probe.py b/src/semiwrap/autowrap/typealias_probe.py index 778e8cc..18fb45f 100644 --- a/src/semiwrap/autowrap/typealias_probe.py +++ b/src/semiwrap/autowrap/typealias_probe.py @@ -185,5 +185,7 @@ def render_typealias_probes( f"{indent}// add a typealias entry for `{target}` to the semiwrap yaml file." ) else: - r.writeln(f"{indent}// add a typealias entry for `{target}` to {yaml_target}.") + r.writeln( + f"{indent}// add a typealias entry for `{target}` to {yaml_target}." + ) r.writeln(f"{indent}using {alias} [[maybe_unused]] = {target};") diff --git a/tests/test_ft_misc.py b/tests/test_ft_misc.py index ebcce97..6a8c0da 100644 --- a/tests/test_ft_misc.py +++ b/tests/test_ft_misc.py @@ -204,8 +204,13 @@ def test_using_generated_typealias_probes_present(): assert using_cpp_files, "sw-test build did not generate semiwrap/using.cpp" using_cpp = using_cpp_files[-1].read_text() - assert "semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" in using_cpp - assert "using semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" in using_cpp + assert ( + "semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" in using_cpp + ) + assert ( + "using semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" + in using_cpp + ) assert "= AlsoCantResolve;" in using_cpp assert "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" in using_cpp assert "= CantResolve;" in using_cpp @@ -213,11 +218,13 @@ def test_using_generated_typealias_probes_present(): trampoline_files = sorted( root.glob("*/semiwrap/trampolines/cr__inner__ProtectedUsing.hpp") ) - assert trampoline_files, ( - "sw-test build did not generate trampoline for cr::inner::ProtectedUsing" - ) + assert ( + trampoline_files + ), "sw-test build did not generate trampoline for cr::inner::ProtectedUsing" trampoline_hpp = trampoline_files[-1].read_text() - assert "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" in trampoline_hpp + assert ( + "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" in trampoline_hpp + ) assert "add a typealias entry for `CantResolve`" in trampoline_hpp diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py index b21deb6..c0dbf8d 100644 --- a/tests/test_typealias_probe.py +++ b/tests/test_typealias_probe.py @@ -74,7 +74,10 @@ def test_render_typealias_probes_mentions_specific_yaml_when_provided(): yml = Path("/tmp/project/semiwrap/using.yml") render_typealias_probes(r, ["CantResolve"], yaml_path=yml) out = r.getvalue() - assert "add a typealias entry for `CantResolve` to /tmp/project/semiwrap/using.yml." in out + assert ( + "add a typealias entry for `CantResolve` to /tmp/project/semiwrap/using.yml." + in out + ) assert "to the semiwrap yaml file" not in out @@ -82,9 +85,10 @@ def test_render_typealias_probes_deduplicates_duplicate_input(): r = RenderBuffer() render_typealias_probes(r, ["CantResolve", "CantResolve"]) out = r.getvalue() - assert out.count( - "using semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" - ) == 1 + assert ( + out.count("using semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml") + == 1 + ) def test_probe_alias_name_distinguishes_namespaces_from_templates(): @@ -424,7 +428,9 @@ def test_render_wrapped_cpp_emits_global_typealias_probe_before_initializer(): assert probe in out assert out.index("using namespace u;") < out.index(probe) assert out.index(probe) < out.index("struct semiwrap_using_initializer") - assert "using semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" in out + assert ( + "using semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" in out + ) assert "= AlsoCantResolve;" in out assert f"add a typealias entry for `AlsoCantResolve` to {hctx.orig_yaml}." in out @@ -459,7 +465,9 @@ def test_render_wrapped_cpp_skips_templated_child_typealias_probes(): out = render_wrapped_cpp(hctx) - assert "semiwrap_typealias_probe_ChildTemplateProbe__add_typealias_to_yaml" not in out + assert ( + "semiwrap_typealias_probe_ChildTemplateProbe__add_typealias_to_yaml" not in out + ) def test_render_trampoline_hpp_emits_class_typealias_probe(): From 172fc0b2344d12564d4a4e03672f28e2863dd0dc Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 17 Jul 2026 04:13:12 +0000 Subject: [PATCH 21/26] Fix typealias probe yaml path diagnostics --- src/semiwrap/autowrap/typealias_probe.py | 2 +- tests/test_typealias_probe.py | 62 ++++++++++++------------ 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/src/semiwrap/autowrap/typealias_probe.py b/src/semiwrap/autowrap/typealias_probe.py index 18fb45f..cfe3baa 100644 --- a/src/semiwrap/autowrap/typealias_probe.py +++ b/src/semiwrap/autowrap/typealias_probe.py @@ -170,7 +170,7 @@ def render_typealias_probes( yaml_path: str | pathlib.Path | None = None, ) -> None: if yaml_path is not None: - yaml_target = pathlib.Path(yaml_path).resolve() + yaml_target = pathlib.Path(yaml_path) else: yaml_target = None diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py index c0dbf8d..d733758 100644 --- a/tests/test_typealias_probe.py +++ b/tests/test_typealias_probe.py @@ -2,18 +2,27 @@ from pathlib import Path +from cxxheaderparser.options import ParserOptions from cxxheaderparser.simple import parse_typename from semiwrap.autowrap import typealias_probe from semiwrap.autowrap.buffer import RenderBuffer +from semiwrap.autowrap.context import ClassTemplateData +from semiwrap.autowrap.cxxparser import parse_header +from semiwrap.autowrap.generator_data import GeneratorData +from semiwrap.autowrap.render_cls_trampoline_hpp import render_cls_trampoline_hpp +from semiwrap.autowrap.render_wrapped import render_wrapped_cpp from semiwrap.autowrap.typealias_probe import ( add_typealias_probe, collect_typealias_probes, probe_alias_name, render_typealias_probes, ) -from semiwrap.autowrap.render_wrapped import render_wrapped_cpp -from semiwrap.autowrap.render_cls_trampoline_hpp import render_cls_trampoline_hpp +from semiwrap.config.autowrap_yml import AutowrapConfigYaml + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +FIXTURE_INCLUDE_ROOT = PROJECT_ROOT / "tests/cpp/sw-test/src/swtest/ft/include" +FIXTURE_YAML_ROOT = PROJECT_ROOT / "tests/cpp/sw-test/semiwrap/ft" def probes_for(type_text: str) -> list[str]: @@ -69,15 +78,18 @@ def test_render_typealias_probes_emits_comment_and_sorted_using_lines(): ) in out -def test_render_typealias_probes_mentions_specific_yaml_when_provided(): +def test_render_typealias_probes_mentions_provided_yaml_path(tmp_path: Path): + real_dir = tmp_path / "real" + link_dir = tmp_path / "link" + real_dir.mkdir() + link_dir.symlink_to(real_dir, target_is_directory=True) + r = RenderBuffer() - yml = Path("/tmp/project/semiwrap/using.yml") + yml = link_dir / "using.yml" render_typealias_probes(r, ["CantResolve"], yaml_path=yml) + out = r.getvalue() - assert ( - "add a typealias entry for `CantResolve` to /tmp/project/semiwrap/using.yml." - in out - ) + assert f"add a typealias entry for `CantResolve` to {yml}." in out assert "to the semiwrap yaml file" not in out @@ -104,26 +116,13 @@ def test_helper_uses_deferred_annotations_for_python_38_runtime_compatibility(): assert source.startswith("from __future__ import annotations\n") -import pathlib - -PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[1] - -from cxxheaderparser.options import ParserOptions - -from semiwrap.autowrap.cxxparser import parse_header -from semiwrap.autowrap.generator_data import GeneratorData -from semiwrap.autowrap.context import ClassTemplateData -from semiwrap.config.autowrap_yml import AutowrapConfigYaml - - def parse_fixture_header(header_name: str, yaml_name: str): - root = PROJECT_ROOT / "tests/cpp/sw-test/src/swtest/ft/include" - yml = PROJECT_ROOT / "tests/cpp/sw-test/semiwrap/ft" / yaml_name + yml = FIXTURE_YAML_ROOT / yaml_name cfg = AutowrapConfigYaml.from_file(yml) return parse_header( - pathlib.Path(header_name).stem, - root / header_name, - root, + Path(header_name).stem, + FIXTURE_INCLUDE_ROOT / header_name, + FIXTURE_INCLUDE_ROOT, GeneratorData(cfg, yml), ParserOptions(), {}, @@ -131,13 +130,12 @@ def parse_fixture_header(header_name: str, yaml_name: str): ) -def parse_fixture_header_with_yaml(header_name: str, yml: pathlib.Path): - root = PROJECT_ROOT / "tests/cpp/sw-test/src/swtest/ft/include" +def parse_fixture_header_with_yaml(header_name: str, yml: Path): cfg = AutowrapConfigYaml.from_file(yml) return parse_header( - pathlib.Path(header_name).stem, - root / header_name, - root, + Path(header_name).stem, + FIXTURE_INCLUDE_ROOT / header_name, + FIXTURE_INCLUDE_ROOT, GeneratorData(cfg, yml), ParserOptions(), {}, @@ -145,11 +143,11 @@ def parse_fixture_header_with_yaml(header_name: str, yml: pathlib.Path): ) -def parse_tmp_header(tmp_path: pathlib.Path, name: str, header: str, yaml: str): +def parse_tmp_header(tmp_path: Path, name: str, header: str, yaml_text: str): header_path = tmp_path / f"{name}.h" yaml_path = tmp_path / f"{name}.yml" header_path.write_text(header) - yaml_path.write_text(yaml) + yaml_path.write_text(yaml_text) cfg = AutowrapConfigYaml.from_file(yaml_path) return parse_header( name, From 610fbf4ee67138a1a4351e6ba7274f532d5ebc42 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 17 Jul 2026 00:41:39 -0400 Subject: [PATCH 22/26] Remove useless test --- tests/test_ft_misc.py | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/tests/test_ft_misc.py b/tests/test_ft_misc.py index 6a8c0da..9f60fd4 100644 --- a/tests/test_ft_misc.py +++ b/tests/test_ft_misc.py @@ -196,38 +196,6 @@ def test_using_fwddecl(): assert u.getX(f) == 43 -def test_using_generated_typealias_probes_present(): - from pathlib import Path - - root = Path(__file__).parent / "cpp" / "sw-test" / "build" - using_cpp_files = sorted(root.glob("*/semiwrap/using.cpp")) - assert using_cpp_files, "sw-test build did not generate semiwrap/using.cpp" - using_cpp = using_cpp_files[-1].read_text() - - assert ( - "semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" in using_cpp - ) - assert ( - "using semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" - in using_cpp - ) - assert "= AlsoCantResolve;" in using_cpp - assert "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" in using_cpp - assert "= CantResolve;" in using_cpp - - trampoline_files = sorted( - root.glob("*/semiwrap/trampolines/cr__inner__ProtectedUsing.hpp") - ) - assert ( - trampoline_files - ), "sw-test build did not generate trampoline for cr::inner::ProtectedUsing" - trampoline_hpp = trampoline_files[-1].read_text() - assert ( - "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" in trampoline_hpp - ) - assert "add a typealias entry for `CantResolve`" in trampoline_hpp - - # # virtual_xform.h # From 2584748d05beaee6b5fb7e45e7178b754cbfe543 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 17 Jul 2026 05:34:54 +0000 Subject: [PATCH 23/26] Fix typealias diagnostics for non-emitted types --- src/semiwrap/autowrap/context.py | 8 +- src/semiwrap/autowrap/cxxparser.py | 134 ++++++++++++------ .../autowrap/render_cls_trampoline_hpp.py | 2 + src/semiwrap/autowrap/render_pybind11.py | 2 +- tests/cpp/sw-test/semiwrap/ft/using.yml | 4 + .../cpp/sw-test/src/swtest/ft/include/using.h | 10 ++ .../src/swtest/ft/include/using_companion.h | 1 + tests/test_typealias_probe.py | 93 +++++++++++- 8 files changed, 205 insertions(+), 49 deletions(-) diff --git a/src/semiwrap/autowrap/context.py b/src/semiwrap/autowrap/context.py index 5c03ddd..cb699bc 100644 --- a/src/semiwrap/autowrap/context.py +++ b/src/semiwrap/autowrap/context.py @@ -490,10 +490,14 @@ class ClassContext: auto_typealias: typing.List[str] = field(default_factory=list) #: Best-effort C++ type spellings that should be probed before generated - #: code uses them. These produce clearer compile-time diagnostics when a - #: YAML typealias entry is missing. + #: class/trampoline code uses them. These produce clearer compile-time + #: diagnostics when a YAML typealias entry is missing. typealias_probes: typing.List[str] = field(default_factory=list) + #: Best-effort C++ type spellings that should be probed before generated + #: wrapped initializer code uses them. + wrapped_typealias_probes: typing.List[str] = field(default_factory=list) + #: vcheck are various static asserts that check things about the #: inline functions vcheck_fns: typing.List[FunctionContext] = field(default_factory=list) diff --git a/src/semiwrap/autowrap/cxxparser.py b/src/semiwrap/autowrap/cxxparser.py index a152ffe..de9b373 100644 --- a/src/semiwrap/autowrap/cxxparser.py +++ b/src/semiwrap/autowrap/cxxparser.py @@ -1212,18 +1212,52 @@ def _on_class_method( overload_tracker, ) - needs_trampoline_signature_probe = ( + needs_trampoline_method_probe = ( is_virtual and not state.class_decl.final and not cdata.data.force_no_trampoline + and not fctx.has_buffers ) - if ( - is_constructor - or state.access != "public" - or needs_trampoline_signature_probe - or fctx.is_overloaded - or fctx.genlambda - ): + needs_trampoline_constructor_probe = ( + is_constructor and state.access == "protected" + ) + # If the method has cpp_code defined, it must either match the function + # signature of the method, or virtual_xform must be defined with an + # appropriate conversion. If neither of these are true, it will lead + # to difficult to diagnose errors at runtime. We add a static assert + # to try and catch these errors at compile time + need_vcheck = ( + is_virtual + and method_data.cpp_code + and not method_data.virtual_xform + and not method_data.trampoline_cpp_code + and not state.class_decl.final + and not cdata.data.force_no_trampoline + ) + wrapped_uses_signature = not fctx.ignore_py and ( + (fctx.is_overloaded and not fctx.genlambda and not fctx.cpp_code) + or ( + state.access == "protected" + and not is_constructor + and not fctx.genlambda + and not fctx.cpp_code + ) + ) + wrapped_uses_params = ( + need_vcheck + or wrapped_uses_signature + or ( + not fctx.ignore_py + and ((is_constructor and not fctx.cpp_code) or bool(fctx.genlambda)) + ) + ) + needs_any_typealias_probe = ( + needs_trampoline_method_probe + or needs_trampoline_constructor_probe + or wrapped_uses_signature + or wrapped_uses_params + ) + if needs_any_typealias_probe: suppressed_names = set(cdata.local_typealias_names) method_template_names = self._template_type_param_names(method.template) suppressed_names.update(method_template_names) @@ -1235,21 +1269,44 @@ def _on_class_method( suppressed_template_aliases.update( self._template_typealias_names(cdata.data.typealias) ) - self._add_typealias_probes( + + def add_probes( + probes: typing.List[str], + *, + include_return_type: bool, + include_parameter_types: bool, + ) -> None: + if include_return_type: + self._add_typealias_probes( + probes, + method.return_type, + suppressed_names, + method_template_names, + suppressed_template_aliases, + ) + if include_parameter_types: + for param in method.parameters: + self._add_typealias_probes( + probes, + param.type, + suppressed_names, + method_template_names, + suppressed_template_aliases, + ) + + add_probes( cctx.typealias_probes, - method.return_type, - suppressed_names, - method_template_names, - suppressed_template_aliases, + include_return_type=needs_trampoline_method_probe, + include_parameter_types=( + needs_trampoline_method_probe or needs_trampoline_constructor_probe + ), + ) + + add_probes( + cctx.wrapped_typealias_probes, + include_return_type=(wrapped_uses_signature or need_vcheck), + include_parameter_types=wrapped_uses_params, ) - for param in method.parameters: - self._add_typealias_probes( - cctx.typealias_probes, - param.type, - suppressed_names, - method_template_names, - suppressed_template_aliases, - ) # Update class-specific method attributes fctx.is_constructor = is_constructor @@ -1316,19 +1373,6 @@ def _on_class_method( elif not is_virtual: cdata.non_virtual_protected_methods.append(fctx) - # If the method has cpp_code defined, it must either match the function - # signature of the method, or virtual_xform must be defined with an - # appropriate conversion. If neither of these are true, it will lead - # to difficult to diagnose errors at runtime. We add a static assert - # to try and catch these errors at compile time - need_vcheck = ( - is_virtual - and method_data.cpp_code - and not method_data.virtual_xform - and not method_data.trampoline_cpp_code - and not state.class_decl.final - and not cdata.data.force_no_trampoline - ) if need_vcheck: cctx.vcheck_fns.append(fctx) self.hctx.has_vcheck = True @@ -2320,22 +2364,28 @@ def parse_header( # Global function typealias probes. Do this after parsing so overload # trackers have seen every overload. for fctx in hctx.functions: + if fctx.ignore_py: + continue if fctx.is_overloaded or fctx.genlambda: fn = fctx._fn suppressed_names = visitor._template_type_param_names(fn.template) - visitor._add_typealias_probes( - hctx.typealias_probes, - fn.return_type, - suppressed_names, - suppressed_names, - ) - for param in fn.parameters: + if fctx.is_overloaded and not fctx.genlambda and not fctx.cpp_code: visitor._add_typealias_probes( hctx.typealias_probes, - param.type, + fn.return_type, suppressed_names, suppressed_names, ) + if fctx.genlambda or ( + fctx.is_overloaded and not fctx.genlambda and not fctx.cpp_code + ): + for param in fn.parameters: + visitor._add_typealias_probes( + hctx.typealias_probes, + param.type, + suppressed_names, + suppressed_names, + ) # Type caster visitor._set_type_caster_includes() diff --git a/src/semiwrap/autowrap/render_cls_trampoline_hpp.py b/src/semiwrap/autowrap/render_cls_trampoline_hpp.py index 807245f..63ab04e 100644 --- a/src/semiwrap/autowrap/render_cls_trampoline_hpp.py +++ b/src/semiwrap/autowrap/render_cls_trampoline_hpp.py @@ -392,6 +392,8 @@ def _render_cls_template_impl( with r.indent(): rpybind11.cls_user_using(r, cls) rpybind11.cls_auto_using(r, cls) + if cls.wrapped_typealias_probes: + render_typealias_probes(r, cls.wrapped_typealias_probes) rpybind11.cls_consts(r, cls) rpybind11.cls_decl(r, cls) diff --git a/src/semiwrap/autowrap/render_pybind11.py b/src/semiwrap/autowrap/render_pybind11.py index c57ca54..2067e4d 100644 --- a/src/semiwrap/autowrap/render_pybind11.py +++ b/src/semiwrap/autowrap/render_pybind11.py @@ -308,7 +308,7 @@ def cls_user_using(r: RenderBuffer, cls: ClassContext): def _collect_class_typealias_probes(cls: ClassContext, probes: T.Set[str]) -> None: - probes.update(cls.typealias_probes) + probes.update(cls.wrapped_typealias_probes) for ccls in cls.child_classes: if not ccls.template: _collect_class_typealias_probes(ccls, probes) diff --git a/tests/cpp/sw-test/semiwrap/ft/using.yml b/tests/cpp/sw-test/semiwrap/ft/using.yml index e473655..1932c6f 100644 --- a/tests/cpp/sw-test/semiwrap/ft/using.yml +++ b/tests/cpp/sw-test/semiwrap/ft/using.yml @@ -10,6 +10,9 @@ classes: overloads: "": CantResolve: + cr::inner::VirtualReturnProbe: + methods: + setPosition: u::FwdDecl: attributes: x: @@ -18,3 +21,4 @@ functions: overloads: AlsoCantResolve: std::string: + fn_lambda_return_probe: diff --git a/tests/cpp/sw-test/src/swtest/ft/include/using.h b/tests/cpp/sw-test/src/swtest/ft/include/using.h index 96ee5be..de9736b 100644 --- a/tests/cpp/sw-test/src/swtest/ft/include/using.h +++ b/tests/cpp/sw-test/src/swtest/ft/include/using.h @@ -18,8 +18,18 @@ class ProtectedUsing { ProtectedUsing(CantResolve t) {} }; +class VirtualReturnProbe { +public: + virtual ~VirtualReturnProbe() = default; + virtual REVLibError setPosition(double position) { return 1; } +}; + inline void fn_using(AlsoCantResolve t) {} inline void fn_using(std::string t) {} +inline REVLibError fn_lambda_return_probe(double position, int* status) { + *status = static_cast(position); + return 1; +} } diff --git a/tests/cpp/sw-test/src/swtest/ft/include/using_companion.h b/tests/cpp/sw-test/src/swtest/ft/include/using_companion.h index f7b016d..dc20dc9 100644 --- a/tests/cpp/sw-test/src/swtest/ft/include/using_companion.h +++ b/tests/cpp/sw-test/src/swtest/ft/include/using_companion.h @@ -4,6 +4,7 @@ namespace cr { using CantResolve = int; using AlsoCantResolve = int; + using REVLibError = int; } namespace cr2 { diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py index d733758..0ad5109 100644 --- a/tests/test_typealias_probe.py +++ b/tests/test_typealias_probe.py @@ -197,6 +197,12 @@ def test_parse_header_collects_class_constructor_typealias_probe(): assert "CantResolve" in cls.typealias_probes +def test_parse_header_skips_fixture_global_generated_lambda_return_probe(): + hctx = parse_fixture_header("using.h", "using.yml") + + assert "REVLibError" not in hctx.typealias_probes + + def test_parse_header_skips_autogenerated_inner_alias_typealias_probes(): hctx = parse_fixture_header("retval.h", "retval.yml") cls = next(c for c in hctx.classes if c.cpp_name == "RetvalClass") @@ -269,6 +275,8 @@ def test_parse_header_collects_missing_yaml_typealias_probes(tmp_path): overloads: "": CantResolve: + cr::inner::VirtualReturnProbe: + ignore: true u::FwdDecl: attributes: x: @@ -312,7 +320,70 @@ def test_parse_header_preserves_dependent_missing_alias_probe(tmp_path): ) cls = next(c for c in hctx.classes if c.cpp_name == "DependentAliasHolder") - assert cls.typealias_probes == ["MissingAlias"] + assert cls.typealias_probes == [] + assert cls.wrapped_typealias_probes == ["MissingAlias"] + + +def test_render_template_binder_emits_dependent_wrapped_typealias_probe(tmp_path): + hctx = parse_tmp_header( + tmp_path, + "dependent_alias", + """ +#pragma once + +template +struct DependentAliasHolder { + DependentAliasHolder(MissingAlias value); +}; +""", + """ +classes: + DependentAliasHolder: + template_params: + - typename N + methods: + DependentAliasHolder: + overloads: + MissingAlias: +""", + ) + + cls = next(c for c in hctx.classes if c.cpp_name == "DependentAliasHolder") + out = render_cls_trampoline_hpp(hctx, cls) + probe = "semiwrap_typealias_probe_MissingAlias_lt_N_gt___add_typealias_to_yaml" + + assert probe in out + assert out.index(probe) < out.index("py::init>") + + +def test_parse_header_skips_cpp_code_constructor_original_param_probe(tmp_path): + hctx = parse_tmp_header( + tmp_path, + "cpp_code_constructor_probe", + """ +#pragma once + +struct MissingCtorAlias {}; +struct CppCodeCtorProbe { + CppCodeCtorProbe(MissingCtorAlias value) {} + CppCodeCtorProbe() = default; +}; +""", + """ +classes: + MissingCtorAlias: + ignore: true + CppCodeCtorProbe: + methods: + CppCodeCtorProbe: + cpp_code: "[]() { return new CppCodeCtorProbe(); }" +""", + ) + + cls = next(c for c in hctx.classes if c.cpp_name == "CppCodeCtorProbe") + assert "MissingCtorAlias" not in cls.typealias_probes + assert "MissingCtorAlias" not in cls.wrapped_typealias_probes + assert "semiwrap_typealias_probe_MissingCtorAlias" not in render_wrapped_cpp(hctx) def test_parse_header_suppresses_non_type_function_template_parameter_probe(tmp_path): @@ -416,7 +487,8 @@ def test_parse_header_collects_public_generated_lambda_method_probe(tmp_path): ) cls = next(c for c in hctx.classes if c.cpp_name == "GeneratedLambdaProbe") - assert "MissingLambdaAlias" in cls.typealias_probes + assert "MissingLambdaAlias" not in cls.typealias_probes + assert "MissingLambdaAlias" in cls.wrapped_typealias_probes def test_render_wrapped_cpp_emits_global_typealias_probe_before_initializer(): @@ -443,10 +515,23 @@ def test_render_wrapped_cpp_emits_class_typealias_probe_inside_initializer(): assert f"add a typealias entry for `CantResolve` to {hctx.orig_yaml}." in out +def test_render_wrapped_cpp_skips_fixture_virtual_return_probe_used_only_by_trampoline(): + hctx = parse_fixture_header("using.h", "using.yml") + cls = next(c for c in hctx.classes if c.cpp_name == "VirtualReturnProbe") + probe = "semiwrap_typealias_probe_REVLibError__add_typealias_to_yaml" + + wrapped = render_wrapped_cpp(hctx) + assert probe not in wrapped + + trampoline = render_cls_trampoline_hpp(hctx, cls) + assert probe in trampoline + assert "REVLibError setPosition(double position) override" in trampoline + + def test_render_wrapped_cpp_deduplicates_class_typealias_probes_in_initializer_scope(): hctx = parse_fixture_header("using.h", "using.yml") fwd_decl = next(c for c in hctx.classes if c.cpp_name == "FwdDecl") - fwd_decl.typealias_probes.append("CantResolve") + fwd_decl.wrapped_typealias_probes.append("CantResolve") out = render_wrapped_cpp(hctx) probe = "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" assert out.count(f"using {probe}") == 1 @@ -457,7 +542,7 @@ def test_render_wrapped_cpp_skips_templated_child_typealias_probes(): parent = next(c for c in hctx.classes if c.cpp_name == "ProtectedUsing") child_template = next(c for c in hctx.classes if c.cpp_name == "FwdDecl") child_template.template = ClassTemplateData("", "", "") - child_template.typealias_probes.append("ChildTemplateProbe") + child_template.wrapped_typealias_probes.append("ChildTemplateProbe") parent.child_classes.append(child_template) hctx.classes = [parent] From 3cbbbe1ea1971bf14410a32738cfbb29940e41df Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 17 Jul 2026 05:43:01 +0000 Subject: [PATCH 24/26] Suppress probes for configured typealiases --- src/semiwrap/autowrap/cxxparser.py | 6 +- tests/cpp/sw-test/semiwrap/ft/using.yml | 5 ++ .../cpp/sw-test/src/swtest/ft/include/using.h | 6 ++ .../src/swtest/ft/include/using_companion.h | 1 + tests/test_typealias_probe.py | 71 +++++++++++++------ 5 files changed, 68 insertions(+), 21 deletions(-) diff --git a/src/semiwrap/autowrap/cxxparser.py b/src/semiwrap/autowrap/cxxparser.py index de9b373..ef6f8e4 100644 --- a/src/semiwrap/autowrap/cxxparser.py +++ b/src/semiwrap/autowrap/cxxparser.py @@ -372,8 +372,9 @@ def __init__( ) self.types = set() self.user_types = set() + self.user_typealias_names: typing.Set[str] = set() self._extract_typealias( - self.user_cfg.typealias, self.hctx.user_typealias, set() + self.user_cfg.typealias, self.hctx.user_typealias, self.user_typealias_names ) def _add_typealias_probes( @@ -1259,12 +1260,14 @@ def _on_class_method( ) if needs_any_typealias_probe: suppressed_names = set(cdata.local_typealias_names) + suppressed_names.update(cdata.typealias_names) method_template_names = self._template_type_param_names(method.template) suppressed_names.update(method_template_names) class_template_names = { param.split()[-1] for param in cdata.data.template_params or [] } suppressed_template_aliases = set(cdata.local_typealias_names) + suppressed_template_aliases.update(cdata.typealias_names) suppressed_template_aliases.difference_update(class_template_names) suppressed_template_aliases.update( self._template_typealias_names(cdata.data.typealias) @@ -2369,6 +2372,7 @@ def parse_header( if fctx.is_overloaded or fctx.genlambda: fn = fctx._fn suppressed_names = visitor._template_type_param_names(fn.template) + suppressed_names.update(visitor.user_typealias_names) if fctx.is_overloaded and not fctx.genlambda and not fctx.cpp_code: visitor._add_typealias_probes( hctx.typealias_probes, diff --git a/tests/cpp/sw-test/semiwrap/ft/using.yml b/tests/cpp/sw-test/semiwrap/ft/using.yml index 1932c6f..f0458a7 100644 --- a/tests/cpp/sw-test/semiwrap/ft/using.yml +++ b/tests/cpp/sw-test/semiwrap/ft/using.yml @@ -13,6 +13,11 @@ classes: cr::inner::VirtualReturnProbe: methods: setPosition: + cr::inner::ConfiguredAliasReturnProbe: + typealias: + - cr::ConfiguredAliasReturn + methods: + getError: u::FwdDecl: attributes: x: diff --git a/tests/cpp/sw-test/src/swtest/ft/include/using.h b/tests/cpp/sw-test/src/swtest/ft/include/using.h index de9736b..48a3c55 100644 --- a/tests/cpp/sw-test/src/swtest/ft/include/using.h +++ b/tests/cpp/sw-test/src/swtest/ft/include/using.h @@ -24,6 +24,12 @@ class VirtualReturnProbe { virtual REVLibError setPosition(double position) { return 1; } }; +class ConfiguredAliasReturnProbe { +public: + virtual ~ConfiguredAliasReturnProbe() = default; + virtual ConfiguredAliasReturn getError() { return 1; } +}; + inline void fn_using(AlsoCantResolve t) {} inline void fn_using(std::string t) {} inline REVLibError fn_lambda_return_probe(double position, int* status) { diff --git a/tests/cpp/sw-test/src/swtest/ft/include/using_companion.h b/tests/cpp/sw-test/src/swtest/ft/include/using_companion.h index dc20dc9..1917068 100644 --- a/tests/cpp/sw-test/src/swtest/ft/include/using_companion.h +++ b/tests/cpp/sw-test/src/swtest/ft/include/using_companion.h @@ -5,6 +5,7 @@ namespace cr { using CantResolve = int; using AlsoCantResolve = int; using REVLibError = int; + using ConfiguredAliasReturn = int; } namespace cr2 { diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py index 0ad5109..aa1da63 100644 --- a/tests/test_typealias_probe.py +++ b/tests/test_typealias_probe.py @@ -160,9 +160,9 @@ def parse_tmp_header(tmp_path: Path, name: str, header: str, yaml_text: str): ) -def test_parse_header_collects_global_function_typealias_probe(): +def test_parse_header_skips_configured_global_function_typealias_probe(): hctx = parse_fixture_header("using.h", "using.yml") - assert "AlsoCantResolve" in hctx.typealias_probes + assert "AlsoCantResolve" not in hctx.typealias_probes def test_parse_header_skips_non_overloaded_global_direct_function_probe(tmp_path): @@ -191,10 +191,10 @@ def test_parse_header_skips_non_overloaded_global_direct_function_probe(tmp_path assert "ControlWord" not in hctx.typealias_probes -def test_parse_header_collects_class_constructor_typealias_probe(): +def test_parse_header_skips_configured_class_constructor_typealias_probe(): hctx = parse_fixture_header("using.h", "using.yml") cls = next(c for c in hctx.classes if c.cpp_name == "ProtectedUsing") - assert "CantResolve" in cls.typealias_probes + assert "CantResolve" not in cls.typealias_probes def test_parse_header_skips_fixture_global_generated_lambda_return_probe(): @@ -277,6 +277,8 @@ def test_parse_header_collects_missing_yaml_typealias_probes(tmp_path): CantResolve: cr::inner::VirtualReturnProbe: ignore: true + cr::inner::ConfiguredAliasReturnProbe: + ignore: true u::FwdDecl: attributes: x: @@ -491,28 +493,19 @@ def test_parse_header_collects_public_generated_lambda_method_probe(tmp_path): assert "MissingLambdaAlias" in cls.wrapped_typealias_probes -def test_render_wrapped_cpp_emits_global_typealias_probe_before_initializer(): +def test_render_wrapped_cpp_skips_configured_global_typealias_probe(): hctx = parse_fixture_header("using.h", "using.yml") out = render_wrapped_cpp(hctx) probe = "semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" - assert probe in out - assert out.index("using namespace u;") < out.index(probe) - assert out.index(probe) < out.index("struct semiwrap_using_initializer") - assert ( - "using semiwrap_typealias_probe_AlsoCantResolve__add_typealias_to_yaml" in out - ) - assert "= AlsoCantResolve;" in out - assert f"add a typealias entry for `AlsoCantResolve` to {hctx.orig_yaml}." in out + assert "using AlsoCantResolve = cr::AlsoCantResolve;" in out + assert probe not in out -def test_render_wrapped_cpp_emits_class_typealias_probe_inside_initializer(): +def test_render_wrapped_cpp_skips_configured_class_typealias_probe(): hctx = parse_fixture_header("using.h", "using.yml") out = render_wrapped_cpp(hctx) probe = "semiwrap_typealias_probe_CantResolve__add_typealias_to_yaml" - assert probe in out - assert out.index("struct semiwrap_using_initializer") < out.index(probe) - assert out.index(probe) < out.index("py::class_ Date: Fri, 17 Jul 2026 01:46:51 -0400 Subject: [PATCH 25/26] Remove indent --- src/semiwrap/autowrap/typealias_probe.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/semiwrap/autowrap/typealias_probe.py b/src/semiwrap/autowrap/typealias_probe.py index cfe3baa..d1b8963 100644 --- a/src/semiwrap/autowrap/typealias_probe.py +++ b/src/semiwrap/autowrap/typealias_probe.py @@ -166,7 +166,6 @@ def render_typealias_probes( r: RenderBuffer, probes: T.Sequence[str], *, - indent: str = "", yaml_path: str | pathlib.Path | None = None, ) -> None: if yaml_path is not None: @@ -177,15 +176,15 @@ def render_typealias_probes( for target in sorted(set(probes)): alias = probe_alias_name(target) r.writeln( - f"{indent}// semiwrap diagnostic: if this line fails because `{target}` " + f"// semiwrap diagnostic: if this line fails because `{target}` " "is unknown," ) if yaml_target is None: r.writeln( - f"{indent}// add a typealias entry for `{target}` to the semiwrap yaml file." + f"// add a typealias entry for `{target}` to the semiwrap yaml file." ) else: r.writeln( - f"{indent}// add a typealias entry for `{target}` to {yaml_target}." + f"// add a typealias entry for `{target}` to {yaml_target}." ) - r.writeln(f"{indent}using {alias} [[maybe_unused]] = {target};") + r.writeln(f"using {alias} [[maybe_unused]] = {target};") From e59c068cbd1625c21dbb1166863f978ff734e3c2 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 17 Jul 2026 01:50:07 -0400 Subject: [PATCH 26/26] Fix formatting --- src/semiwrap/autowrap/render_wrapped.py | 1 - src/semiwrap/autowrap/typealias_probe.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/semiwrap/autowrap/render_wrapped.py b/src/semiwrap/autowrap/render_wrapped.py index f4375b4..93dbd16 100644 --- a/src/semiwrap/autowrap/render_wrapped.py +++ b/src/semiwrap/autowrap/render_wrapped.py @@ -54,7 +54,6 @@ def render_wrapped_cpp(hctx: HeaderContext) -> str: r.writeln(f"using namespace {ns};") if hctx.typealias_probes: - r.writeln() render_typealias_probes(r, hctx.typealias_probes, yaml_path=hctx.orig_yaml) r.writeln(f"\nstruct semiwrap_{hctx.hname}_initializer {{\n") diff --git a/src/semiwrap/autowrap/typealias_probe.py b/src/semiwrap/autowrap/typealias_probe.py index d1b8963..e8e3925 100644 --- a/src/semiwrap/autowrap/typealias_probe.py +++ b/src/semiwrap/autowrap/typealias_probe.py @@ -175,6 +175,7 @@ def render_typealias_probes( for target in sorted(set(probes)): alias = probe_alias_name(target) + r.writeln() r.writeln( f"// semiwrap diagnostic: if this line fails because `{target}` " "is unknown,"