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