From fcfb59f16e8932a22e4b39340b7f653ed63b4533 Mon Sep 17 00:00:00 2001 From: Frederik Hennig <frederik.hennig@fau.de> Date: Tue, 14 Jan 2025 17:24:54 +0100 Subject: [PATCH] Introduce CppTypeFactory. Extend documentation on cpptype. Write user guide on C++ API modelling. --- conftest.py | 4 +- docs/source/conf.py | 19 +++--- docs/source/index.md | 1 + docs/source/usage/api_modelling.md | 93 +++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/pystencilssfg/lang/types.py | 95 ++++++++++++++++++++++++++---- 6 files changed, 191 insertions(+), 23 deletions(-) create mode 100644 docs/source/usage/api_modelling.md diff --git a/conftest.py b/conftest.py index 1c85902..661e722 100644 --- a/conftest.py +++ b/conftest.py @@ -3,13 +3,15 @@ from os import path @pytest.fixture(autouse=True) -def prepare_composer(doctest_namespace): +def prepare_doctest_namespace(doctest_namespace): from pystencilssfg import SfgContext, SfgComposer + from pystencilssfg import lang # Place a composer object in the environment for doctests sfg = SfgComposer(SfgContext()) doctest_namespace["sfg"] = sfg + doctest_namespace["lang"] = lang DATA_DIR = path.join(path.split(__file__)[0], "tests/data") diff --git a/docs/source/conf.py b/docs/source/conf.py index 4bdf700..da6f4d7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -24,7 +24,7 @@ html_title = f"pystencils-sfg v{version} Documentation" # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ - "myst_parser", + "myst_nb", "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.autosummary", @@ -37,16 +37,8 @@ extensions = [ templates_path = ["_templates"] exclude_patterns = [] -source_suffix = { - ".rst": "restructuredtext", - ".md": "markdown", -} master_doc = "index" nitpicky = True -myst_enable_extensions = [ - "colon_fence", - "dollarmath" -] # -- Options for HTML output ------------------------------------------------- @@ -89,6 +81,15 @@ sfg = SfgComposer(SfgContext()) ''' +# -- Options for MyST / MyST-NB ---------------------------------------------- + +nb_execution_mode = "cache" # do not execute notebooks by default + +myst_enable_extensions = [ + "dollarmath", + "colon_fence", +] + # Prepare code generation examples def build_examples(): diff --git a/docs/source/index.md b/docs/source/index.md index ca35b36..0cab083 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -6,6 +6,7 @@ :caption: User Guide usage/generator_scripts +C++ API Modelling <usage/api_modelling> usage/project_integration usage/tips_n_tricks ``` diff --git a/docs/source/usage/api_modelling.md b/docs/source/usage/api_modelling.md new file mode 100644 index 0000000..565da7a --- /dev/null +++ b/docs/source/usage/api_modelling.md @@ -0,0 +1,93 @@ +--- +file_format: mystnb +kernelspec: + name: python3 +--- + +# Modelling C++ APIs in pystencils-sfg + +Pystencils-SFG is designed to help you generate C++ code that interfaces with pystencils on the one side, +and with your handwritten code on the other side. +This requires that the C++ classes and APIs of your framework or application be represented within the SFG system. +This guide shows how you can use the facilities of the {any}`pystencilssfg.lang` module to model your C++ interfaces +for use with the code generator. + +To begin, import the `lang` module: + +```{code-cell} ipython3 +from pystencilssfg import lang +``` + +## Defining C++ Types and Type Templates + +The first C++ entities that need to be mirrored for the SFGs are the types and type templates a library +or application uses or exposes. + +### Non-Templated Types + +To define a C++ type, we use {any}`pystencilssfg.lang.cpptype <pystencilssfg.lang.types.cpptype>`: + +```{code-cell} ipython3 +MyClassTypeFactory = lang.cpptype("my_namespace::MyClass", "MyClass.hpp") +MyClassTypeFactory +``` + +This defines two properties of the type: its fully qualified name, and the set of headers +that need to be included when working with the type. +Now, whenever this type occurs as the type of a variable given to pystencils-sfg, +the code generator will make sure that `MyClass.hpp` is included into the respective +generated code file. + +The object returned by `cpptype` is not the type itself, but a factory for instances of the type. +Even as `MyClass` does not have any template parameters, we can create different instances of it: +`const` and non-`const`, as well as references and non-references. +We do this by calling the factory: + +```{code-cell} ipython3 +MyClass = MyClassTypeFactory() +str(MyClass) +``` + +To produce a `const`-qualified version of the type: + +```{code-cell} ipython3 +MyClassConst = MyClassTypeFactory(const=True) +str(MyClassConst) +``` + +And finally, to produce a reference instead: + +```{code-cell} ipython3 +MyClassRef = MyClassTypeFactory(ref=True) +str(MyClassRef) +``` + +Of course, `const` and `ref` can also be combined to create a reference-to-const. + +### Types with Template Parameters + +We can add template parameters to our type by the use of +[Python format strings](https://docs.python.org/3/library/string.html#formatstrings): + +```{code-cell} ipython3 +MyClassTemplate = lang.cpptype("my_namespace::MyClass< {T1}, {T2} >", "MyClass.hpp") +MyClassTemplate +``` + +Here, the type parameters `T1` and `T2` are specified in braces. +For them, values must be provided when calling the factory to instantiate the type: + +```{code-cell} ipython3 +MyClassIntDouble = MyClassTemplate(T1="int", T2="double") +str(MyClassIntDouble) +``` + +The way type parameters are passed to the factory is identical to the behavior of {any}`str.format`, +except that it does not support attribute or element accesses. +In particular, this means that we can also use unnamed, implicit positional parameters: + +```{code-cell} ipython3 +MyClassTemplate = lang.cpptype("my_namespace::MyClass< {}, {} >", "MyClass.hpp") +MyClassIntDouble = MyClassTemplate("int", "double") +str(MyClassIntDouble) +``` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index da36a11..6ac0327 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ docs = [ "sphinx", "pydata-sphinx-theme==0.15.4", "sphinx-book-theme==1.1.3", # workaround for https://github.com/executablebooks/sphinx-book-theme/issues/865 - "myst-parser", + "myst-nb", "sphinx_design", "sphinx_autodoc_typehints", "sphinx-copybutton", diff --git a/src/pystencilssfg/lang/types.py b/src/pystencilssfg/lang/types.py index 4da3fa4..71c198a 100644 --- a/src/pystencilssfg/lang/types.py +++ b/src/pystencilssfg/lang/types.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import Any, Iterable, Sequence, Mapping, Callable +from typing import Any, Iterable, Sequence, Mapping, TypeVar, Generic from abc import ABC from dataclasses import dataclass from itertools import chain @@ -105,9 +105,87 @@ class CppType(PsCustomType, ABC): return set(str(h) for h in self.class_includes) +TypeClass_T = TypeVar("TypeClass_T", bound=CppType) +"""Python type variable bound to `CppType`.""" + + +class CppTypeFactory(Generic[TypeClass_T]): + """Type Factory returned by `cpptype`.""" + + def __init__(self, tclass: type[TypeClass_T]) -> None: + self._type_class = tclass + + @property + def includes(self) -> frozenset[HeaderFile]: + """Set of headers required by this factory's type""" + return self._type_class.class_includes + + @property + def template_string(self) -> str: + """Template string of this factory's type""" + return self._type_class.template_string + + def __str__(self) -> str: + return f"Factory for {self.template_string}` defined in {self.includes}" + + def __repr__(self) -> str: + return f"CppTypeFactory({self.template_string}, includes={{ {', '.join(str(i) for i in self.includes)} }})" + + def __call__(self, *args, ref: bool = False, **kwargs) -> TypeClass_T | Ref: + """Create a type object of this factory's C++ type template. + + Args: + args, kwargs: Positional and keyword arguments are forwarded to the template string formatter + ref: If ``True``, return a reference type + + Returns: + An instantiated type object + """ + + obj = self._type_class(*args, **kwargs) + if ref: + return Ref(obj) + else: + return obj + + def cpptype( - typestr: str, include: str | HeaderFile | Iterable[str | HeaderFile] = () -) -> Callable[..., CppType | Ref]: + template_str: str, include: str | HeaderFile | Iterable[str | HeaderFile] = () +) -> CppTypeFactory: + """Describe a C++ type template, associated with a set of required header files. + + This function allows users to define C++ type templates using + `Python format string syntax <https://docs.python.org/3/library/string.html#formatstrings>`_. + The types may furthermore be annotated with a set of header files that must be included + in order to use the type. + + >>> opt_template = lang.cpptype("std::optional< {T} >", "<optional>") + >>> opt_template.template_string + 'std::optional< {T} >' + + This function returns a `CppTypeFactory` object, which in turn can be called to create + an instance of the C++ type template. + Therein, the ``template_str`` argument is treated as a Python format string: + The positional and keyword arguments passed to the returned type factory are passed + through machinery that is based on `str.format` to produce the actual type name. + + >>> int_option = opt_template(T="int") + >>> int_option.c_string().strip() + 'std::optional< int >' + + The factory may also create reference types when the ``ref=True`` is specified. + + >>> int_option_ref = opt_template(T="int", ref=True) + >>> int_option_ref.c_string().strip() + 'std::optional< int >&' + + Args: + template_str: Format string defining the type template + include: Either the name of a header file, or a sequence of names of header files + + Returns: + CppTypeFactory: A factory used to instantiate the type template + """ headers: list[str | HeaderFile] if isinstance(include, (str, HeaderFile)): @@ -118,17 +196,10 @@ def cpptype( headers = list(include) class TypeClass(CppType): - template_string = typestr + template_string = template_str class_includes = frozenset(HeaderFile.parse(h) for h in headers) - def factory(*args, ref: bool = False, **kwargs): - obj = TypeClass(*args, **kwargs) - if ref: - return Ref(obj) - else: - return obj - - return staticmethod(factory) + return CppTypeFactory[TypeClass](TypeClass) class Ref(PsType): -- GitLab