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..9e08a99 --- /dev/null +++ b/docs/superpowers/plans/2026-07-10-typealias-diagnostics.md @@ -0,0 +1,822 @@ +# 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. +- 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. + +--- + +## File Structure + +- Create `src/semiwrap/autowrap/typealias_probe.py` + - Owns candidate extraction from cxxheaderparser type nodes. + - Owns deterministic/safe generated alias-name construction. + - 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` + - 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_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 +``` + +- [ ] **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) + 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 = _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 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};") +``` + +- [ ] **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. 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. 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..a152ffe 100644 --- a/src/semiwrap/autowrap/cxxparser.py +++ b/src/semiwrap/autowrap/cxxparser.py @@ -51,7 +51,9 @@ PQName, PQNameSegment, Reference, + TemplateDecl, TemplateInst, + TemplateNonTypeParam, TemplateTypeParam, Type, Typedef, @@ -91,6 +93,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 @@ -297,6 +300,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] @@ -368,6 +372,74 @@ def __init__( ) self.types = set() self.user_types = set() + self._extract_typealias( + self.user_cfg.typealias, self.hctx.user_typealias, set() + ) + + def _add_typealias_probes( + self, + 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 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 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, + template: typing.Optional[ + typing.Union[TemplateDecl, typing.List[TemplateDecl]] + ], + ) -> 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, TemplateNonTypeParam)) + and param.name + } # # Visitor interface @@ -476,6 +548,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) @@ -602,6 +675,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 @@ -787,15 +862,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=[], @@ -1130,6 +1212,45 @@ 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 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.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, + 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 fctx.is_constructor = is_constructor if is_constructor and method_data.rename and not method_data.cpp_code: @@ -1992,6 +2113,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: @@ -2192,8 +2317,25 @@ 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()) + # 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/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/src/semiwrap/autowrap/render_pybind11.py b/src/semiwrap/autowrap/render_pybind11.py index 0f0275f..c57ca54 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 @@ -10,6 +11,7 @@ GeneratedLambda, PropContext, ) +from .typealias_probe import render_typealias_probes def mkdoc(pre: str, doc: Documentation, post: str) -> str: @@ -305,6 +307,27 @@ def cls_user_using(r: RenderBuffer, cls: ClassContext): r.writeln(f"{typealias};") +def _collect_class_typealias_probes(cls: ClassContext, probes: T.Set[str]) -> None: + probes.update(cls.typealias_probes) + for ccls in cls.child_classes: + if not ccls.template: + _collect_class_typealias_probes(ccls, probes) + + +def cls_typealias_probes( + r: RenderBuffer, + classes: T.Iterable[ClassContext], + *, + yaml_path: T.Optional[T.Union[str, pathlib.Path]] = 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), yaml_path=yaml_path) + + def cls_auto_using(r: RenderBuffer, cls: ClassContext): for ccls in cls.child_classes: if not ccls.template: diff --git a/src/semiwrap/autowrap/render_wrapped.py b/src/semiwrap/autowrap/render_wrapped.py index 4eb5cb8..f4375b4 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: @@ -52,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, yaml_path=hctx.orig_yaml) + r.writeln(f"\nstruct semiwrap_{hctx.hname}_initializer {{\n") with r.indent(): @@ -60,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, yaml_path=hctx.orig_yaml) + if hctx.subpackages: r.writeln() for vname in hctx.subpackages.values(): diff --git a/src/semiwrap/autowrap/typealias_probe.py b/src/semiwrap/autowrap/typealias_probe.py new file mode 100644 index 0000000..cfe3baa --- /dev/null +++ b/src/semiwrap/autowrap/typealias_probe.py @@ -0,0 +1,191 @@ +from __future__ import annotations + +import pathlib +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") + ] +) + +_ENCODE_TOKENS = { + "_": "_u_", + "<": "_lt_", + ">": "_gt_", + ",": "_comma_", + " ": "_space_", + "*": "_star_", + "&": "_amp_", + ".": "_dot_", + "-": "_dash_", +} + + +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 _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 = _encode_alias_target(target) + return f"semiwrap_typealias_probe_{safe}__add_typealias_to_yaml" + + +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: + yaml_target = pathlib.Path(yaml_path) + 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," + ) + 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/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_ft_misc.py b/tests/test_ft_misc.py index 9f60fd4..6a8c0da 100644 --- a/tests/test_ft_misc.py +++ b/tests/test_ft_misc.py @@ -196,6 +196,38 @@ 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 # diff --git a/tests/test_typealias_probe.py b/tests/test_typealias_probe.py new file mode 100644 index 0000000..d733758 --- /dev/null +++ b/tests/test_typealias_probe.py @@ -0,0 +1,479 @@ +from __future__ import annotations + +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.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]: + 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_u_list_lt_CantResolve_gt___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_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 = link_dir / "using.yml" + render_typealias_probes(r, ["CantResolve"], yaml_path=yml) + + out = r.getvalue() + assert f"add a typealias entry for `CantResolve` to {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"]) + 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") + + +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") + + +def parse_fixture_header(header_name: str, yaml_name: str): + yml = FIXTURE_YAML_ROOT / yaml_name + cfg = AutowrapConfigYaml.from_file(yml) + return parse_header( + Path(header_name).stem, + FIXTURE_INCLUDE_ROOT / header_name, + FIXTURE_INCLUDE_ROOT, + GeneratorData(cfg, yml), + ParserOptions(), + {}, + False, + ) + + +def parse_fixture_header_with_yaml(header_name: str, yml: Path): + cfg = AutowrapConfigYaml.from_file(yml) + return parse_header( + Path(header_name).stem, + FIXTURE_INCLUDE_ROOT / header_name, + FIXTURE_INCLUDE_ROOT, + GeneratorData(cfg, yml), + ParserOptions(), + {}, + False, + ) + + +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_text) + cfg = AutowrapConfigYaml.from_file(yaml_path) + return parse_header( + name, + header_path, + tmp_path, + GeneratorData(cfg, yaml_path), + 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_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") + 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_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_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") + assert "N" not in 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_parse_header_preserves_dependent_missing_alias_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") + 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" 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_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, + "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(): + 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 + + +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_