diff --git a/README.md b/README.md index 16776b15..0109cc93 100644 --- a/README.md +++ b/README.md @@ -267,7 +267,9 @@ r = Rectangle(4, 5) auto_includes: True # resolves PottsMesh.hpp from the signature ``` - cppwg then scans each such class's wrapped method/constructor signatures, and + cppwg then scans each such class's wrapped method/constructor signatures, plus + the template arguments of its instantiations (e.g. a `Foo` needs + `Bar.hpp` even if `Bar` never appears in a method/constructor signature), and for every **project type** it finds (a class defined under the module `source_locations`) adds that class's header to the wrapper. Only project types are resolved — library types (`std::`, boost, PETSc, VTK, …) are left alone — diff --git a/cppwg/info/package_info.py b/cppwg/info/package_info.py index 62aefb04..2b828e1a 100644 --- a/cppwg/info/package_info.py +++ b/cppwg/info/package_info.py @@ -819,11 +819,12 @@ def resolve_auto_includes(self) -> None: For each wrapped class that opts into ``auto_includes``, inspect the types its wrapped methods/constructors actually expose (via - _iter_wrapped_arg_return_types, so exclusions are honoured), resolve any - that name a project class to that class's header, and record the headers - on ``class_info.auto_include_headers`` for the writer to emit. The class's - own header is dropped (the writer always includes it), as is anything that - does not resolve to a project header (library types are left alone). + _iter_wrapped_arg_return_types, so exclusions are honoured) plus the + project types named as its template arguments (e.g. ``Foo``), + resolve any that name a project class to that class's header, and record + the headers on ``class_info.auto_include_headers`` for the writer to emit. + The class's own header is dropped (the writer always includes it), as is + anything that does not resolve to a project header (library types are left alone). A class using ``common_include_file`` is skipped: the common header already includes every project header, so per-type includes are moot. @@ -854,6 +855,18 @@ def resolve_auto_includes(self) -> None: if header: headers.add(header) + # Project types named as template arguments of this class's + # instantiations (e.g. Foo) are dependencies the generated + # code needs even if they never appear in a wrapped signature, so + # resolve their headers here too. Numeric args (dimensions like 2 + # or 3) and library types resolve to nothing. + for arg_list in class_info.template_arg_lists: + for arg in arg_list: + for name in _IDENTIFIER_RE.findall(str(arg)): + header = type_header_map.get(name) + if header: + headers.add(header) + # The writer always includes the class's own header; drop it here. own_header = class_info.source_file if not own_header and class_info.decls: diff --git a/examples/cells/dynamic/config.yaml b/examples/cells/dynamic/config.yaml index e0e0692e..93496afd 100644 --- a/examples/cells/dynamic/config.yaml +++ b/examples/cells/dynamic/config.yaml @@ -53,6 +53,16 @@ modules: # cell - name: Cell + # CellFactory is header-only and uses CELL_TYPE only + # inside its method bodies, never in the wrapped interface. cppwg sees + # CELL_TYPE only as a template argument, so the package-level + # auto_includes resolves Cell.hpp from the instantiation arguments below. + # Without it the generated wrapper would not compile. + - name: CellFactory + template_substitutions: + - signature: + replacement: [[Cell, 2], [Cell, 3]] + # mesh - name: AbstractMesh diff --git a/examples/cells/dynamic/wrappers/all/CellFactory.cppwg.cpp b/examples/cells/dynamic/wrappers/all/CellFactory.cppwg.cpp new file mode 100644 index 00000000..07628db9 --- /dev/null +++ b/examples/cells/dynamic/wrappers/all/CellFactory.cppwg.cpp @@ -0,0 +1,42 @@ +// This file is auto-generated by cppwg; manual changes will be overwritten. + +#include +#include +#include "Cell.hpp" +#include +#include "CellFactory.hpp" + +#include "CellFactory.cppwg.hpp" + +namespace py = pybind11; +PYBIND11_DECLARE_HOLDER_TYPE(T, std::shared_ptr); +typedef CellFactory CellFactory_Cell_2; +typedef CellFactory CellFactory_Cell_3; + + +void register_CellFactory_Cell_2_class(py::module &m) +{ + py::class_>(m, "CellFactory_Cell_2") + .def(py::init<>()) + .def("GetDimension", + (unsigned int(CellFactory_Cell_2::*)() const) &CellFactory_Cell_2::GetDimension, + " ") + .def("CreateCell", + (unsigned int(CellFactory_Cell_2::*)()) &CellFactory_Cell_2::CreateCell, + " ") + ; +} + + +void register_CellFactory_Cell_3_class(py::module &m) +{ + py::class_>(m, "CellFactory_Cell_3") + .def(py::init<>()) + .def("GetDimension", + (unsigned int(CellFactory_Cell_3::*)() const) &CellFactory_Cell_3::GetDimension, + " ") + .def("CreateCell", + (unsigned int(CellFactory_Cell_3::*)()) &CellFactory_Cell_3::CreateCell, + " ") + ; +} diff --git a/examples/cells/dynamic/wrappers/all/CellFactory.cppwg.hpp b/examples/cells/dynamic/wrappers/all/CellFactory.cppwg.hpp new file mode 100644 index 00000000..0a350ec2 --- /dev/null +++ b/examples/cells/dynamic/wrappers/all/CellFactory.cppwg.hpp @@ -0,0 +1,10 @@ +// This file is auto-generated by cppwg; manual changes will be overwritten. + +#ifndef CellFactory_hpp__cppwg_wrapper +#define CellFactory_hpp__cppwg_wrapper + +#include + +void register_CellFactory_Cell_2_class(pybind11::module &m); +void register_CellFactory_Cell_3_class(pybind11::module &m); +#endif // CellFactory_hpp__cppwg_wrapper diff --git a/examples/cells/dynamic/wrappers/all/_pycells_all.main.cppwg.cpp b/examples/cells/dynamic/wrappers/all/_pycells_all.main.cppwg.cpp index b325c7a9..94027f0d 100644 --- a/examples/cells/dynamic/wrappers/all/_pycells_all.main.cppwg.cpp +++ b/examples/cells/dynamic/wrappers/all/_pycells_all.main.cppwg.cpp @@ -9,6 +9,7 @@ #pragma GCC visibility pop #endif #include "Cell.cppwg.hpp" +#include "CellFactory.cppwg.hpp" #include "Corner.cppwg.hpp" #include "Facet.cppwg.hpp" #include "MacroMesh.cppwg.hpp" @@ -32,6 +33,8 @@ PYBIND11_MODULE(_pycells_all, m) }); register_Cell_class(m); + register_CellFactory_Cell_2_class(m); + register_CellFactory_Cell_3_class(m); register_Corner_2_class(m); register_Facet_2_class(m); register_MacroMesh_2_2_class(m); diff --git a/examples/cells/dynamic/wrappers/wrapper_header_collection.cppwg.hpp b/examples/cells/dynamic/wrappers/wrapper_header_collection.cppwg.hpp index 3bc4b48f..3bd3b53d 100644 --- a/examples/cells/dynamic/wrappers/wrapper_header_collection.cppwg.hpp +++ b/examples/cells/dynamic/wrappers/wrapper_header_collection.cppwg.hpp @@ -6,6 +6,7 @@ // Includes #include "AbstractMesh.hpp" #include "Cell.hpp" +#include "CellFactory.hpp" #include "Corner.hpp" #include "Facet.hpp" #include "MacroMesh.hpp" @@ -19,6 +20,8 @@ // Instantiate Template Classes template class AbstractMesh<2, 2>; template class AbstractMesh<3, 3>; +template class CellFactory; +template class CellFactory; template class Corner<2>; template class Facet<2>; template class MacroMesh<2, 2>; @@ -37,6 +40,8 @@ namespace cppwg { typedef AbstractMesh<2, 2> AbstractMesh_2_2; typedef AbstractMesh<3, 3> AbstractMesh_3_3; + typedef CellFactory CellFactory_Cell_2; + typedef CellFactory CellFactory_Cell_3; typedef Corner<2> Corner_2; typedef Facet<2> Facet_2; typedef MacroMesh<2, 2> MacroMesh_2_2; diff --git a/examples/cells/src/cpp/cell/CellFactory.hpp b/examples/cells/src/cpp/cell/CellFactory.hpp new file mode 100644 index 00000000..a57e914f --- /dev/null +++ b/examples/cells/src/cpp/cell/CellFactory.hpp @@ -0,0 +1,48 @@ +#ifndef CELLFACTORY_HPP_ +#define CELLFACTORY_HPP_ + +/** + * A minimal, header-only factory templated on a cell type. CELL_TYPE is used + * only in the inline method bodies and never appears in the wrapped interface, + * so cppwg sees it only as a template argument of the CellFactory + * instantiations - not in any signature. With auto_includes enabled, cppwg + * resolves CELL_TYPE's header (Cell.hpp) from those template arguments, so the + * generated wrapper - which odr-uses CreateCell and therefore needs the complete + * CELL_TYPE - compiles. + */ +template +class CellFactory +{ +public: + /** + * Default Constructor + */ + CellFactory() : mNumCells(0u) + { + } + + /** + * Return the spatial dimension this factory builds in. + */ + unsigned GetDimension() const + { + return DIM; + } + + /** + * Build one cell of CELL_TYPE and return the running total. Constructing a + * CELL_TYPE here is what makes the wrapper need CELL_TYPE's header. + */ + unsigned CreateCell() + { + CELL_TYPE cell; + static_cast(cell); + return ++mNumCells; + } + +private: + /** Number of cells created so far. */ + unsigned mNumCells; +}; + +#endif // CELLFACTORY_HPP_ diff --git a/examples/cells/src/py/pycells/__init__.py b/examples/cells/src/py/pycells/__init__.py index b59054ca..2fc972a7 100644 --- a/examples/cells/src/py/pycells/__init__.py +++ b/examples/cells/src/py/pycells/__init__.py @@ -2,6 +2,8 @@ from ._pycells_all import ( Cell, + CellFactory_Cell_2, + CellFactory_Cell_3, Corner_2, Facet_2, MacroMesh_2_2, @@ -18,6 +20,16 @@ ) from ._syntax import TemplateClassDict +# CellFactory names CELL_TYPE only as a template argument, so +# cppwg's auto_includes resolved Cell.hpp from the instantiation arguments (not +# a signature) - see CellFactory.hpp and examples/cells/dynamic/config.yaml. +CellFactory = TemplateClassDict( + { + ("Cell", "2"): CellFactory_Cell_2, + ("Cell", "3"): CellFactory_Cell_3, + } +) + # Only Facet<2> is wrapped (curated). Facet<1> is deliberately left unwrapped; # wrapping it would reference the never-instantiated Facet<0> and fail to import # (see Facet.hpp and examples/cells/dynamic/config.yaml). @@ -73,6 +85,7 @@ __all__ = [ "Cell", + "CellFactory", "Corner", "Facet", "MacroMesh", diff --git a/examples/cells/tests/test_cells.py b/examples/cells/tests/test_cells.py index 74b41cbb..cb3cb6c6 100644 --- a/examples/cells/tests/test_cells.py +++ b/examples/cells/tests/test_cells.py @@ -3,7 +3,16 @@ import petsc4py import vtk -from pycells import Corner, Facet, MacroMesh, Node, PetscUtils, Scene +from pycells import ( + Cell, + CellFactory, + Corner, + Facet, + MacroMesh, + Node, + PetscUtils, + Scene, +) class TestCells(unittest.TestCase): @@ -25,6 +34,18 @@ def testNonCuratedBoundaryElement(self): with self.assertRaises(KeyError): _ = Corner[1] + def testAutoIncludeTemplateArg(self): + # CellFactory names CELL_TYPE (Cell) only as a template + # argument - never in a wrapped signature - mirroring pychaste's + # CellsGenerator. auto_includes resolved Cell.hpp + # from the instantiation arguments so the wrapper compiled at all; this + # confirms the instantiations import and work. + factory = CellFactory[Cell, 2]() + self.assertEqual(factory.GetDimension(), 2) + self.assertEqual(factory.CreateCell(), 1) + self.assertEqual(factory.CreateCell(), 2) + self.assertEqual(CellFactory[Cell, 3]().GetDimension(), 3) + def testMacroInstantiationFallback(self): # MacroMesh's explicit template instantiations are declared via a macro # (see MacroMesh.cpp), which cppwg's source-text scan cannot see. This diff --git a/tests/test_package_info.py b/tests/test_package_info.py index 927e7002..e15513f9 100644 --- a/tests/test_package_info.py +++ b/tests/test_package_info.py @@ -678,6 +678,48 @@ def test_resolve_auto_includes_resolves_project_type_headers(tmp_path): assert cls.auto_include_headers == ["PottsMesh.hpp"] +def test_resolve_auto_includes_resolves_template_arg_headers(tmp_path): + """Project types named as template arguments resolve to their headers. + + CellsGenerator needs NoCellCycleModel.hpp even though + the model type never appears in a wrapped signature. Numeric args (the + dimension) resolve to nothing, and the class's own header is dropped. + """ + (tmp_path / "CellsGenerator.hpp").write_text( + "template class CellsGenerator {};\n" + ) + (tmp_path / "NoCellCycleModel.hpp").write_text("class NoCellCycleModel {};\n") + (tmp_path / "UniformCellCycleModel.hpp").write_text( + "class UniformCellCycleModel {};\n" + ) + + package = PackageInfo("pkg", {"source_root": str(tmp_path)}) + package.source_hpp_files = [ + str(tmp_path / "CellsGenerator.hpp"), + str(tmp_path / "NoCellCycleModel.hpp"), + str(tmp_path / "UniformCellCycleModel.hpp"), + ] + module = ModuleInfo("mod") + package.add_module(module) + + cls = CppClassInfo("CellsGenerator", {"auto_includes": True}) + cls.source_file = "CellsGenerator.hpp" + # The model types appear only as template arguments, never in a signature. + cls.template_arg_lists = [ + ["NoCellCycleModel", "2"], + ["UniformCellCycleModel", "3"], + ] + cls.decls = [_FakeDecl(), _FakeDecl()] + module.add_class(cls) + + package.resolve_auto_includes() + + assert cls.auto_include_headers == [ + "NoCellCycleModel.hpp", + "UniformCellCycleModel.hpp", + ] + + def test_resolve_auto_includes_noop_without_opt_in(tmp_path): """Without auto_includes enabled, no headers are resolved.""" (tmp_path / "PottsMesh.hpp").write_text("class PottsMesh {};\n")