From 0381851b1117fb76a8afd8acc22d61cbaf8819f5 Mon Sep 17 00:00:00 2001
From: Frederik Hennig <frederik.hennig@fau.de>
Date: Tue, 18 Feb 2025 17:42:00 +0100
Subject: [PATCH] Add `.params()` to `sfg.function`. Change `std.tuple` to take
 type params as `*args`. Extend composer guide. Add composer feature test.

---
 docs/source/getting_started.md                |   3 -
 docs/source/index.md                          |   2 +-
 docs/source/usage/api_modelling.md            |   1 +
 docs/source/usage/composer.md                 |  88 -------
 docs/source/usage/how_to_composer.md          | 246 ++++++++++++++++++
 src/pystencilssfg/composer/basic_composer.py  |  13 +
 src/pystencilssfg/ir/entities.py              |  16 +-
 src/pystencilssfg/lang/cpp/std_tuple.py       |   4 +-
 tests/generator_scripts/index.yaml            |   2 +
 .../source/ComposerFeatures.harness.cpp       |  13 +
 .../source/ComposerFeatures.py                |  13 +
 tests/lang/test_cpp_stl_classes.py            |   2 +-
 12 files changed, 304 insertions(+), 99 deletions(-)
 delete mode 100644 docs/source/usage/composer.md
 create mode 100644 docs/source/usage/how_to_composer.md
 create mode 100644 tests/generator_scripts/source/ComposerFeatures.harness.cpp
 create mode 100644 tests/generator_scripts/source/ComposerFeatures.py

diff --git a/docs/source/getting_started.md b/docs/source/getting_started.md
index 008585a..5653928 100644
--- a/docs/source/getting_started.md
+++ b/docs/source/getting_started.md
@@ -213,8 +213,5 @@ for advice on how to configure the header file and namespace where the class is
 :::
 
 
-## Next Steps
-
-
 [mdspan]: https://en.cppreference.com/w/cpp/container/mdspan
 [cppreference_compiler_support]: https://en.cppreference.com/w/cpp/compiler_support
diff --git a/docs/source/index.md b/docs/source/index.md
index 9ffc4f0..48df441 100644
--- a/docs/source/index.md
+++ b/docs/source/index.md
@@ -35,7 +35,7 @@ getting_started
 :maxdepth: 1
 :caption: User Guide
 
-usage/composer
+usage/how_to_composer
 C++ API Modelling <usage/api_modelling>
 usage/config_and_cli
 usage/project_integration
diff --git a/docs/source/usage/api_modelling.md b/docs/source/usage/api_modelling.md
index 60d37ae..d9a7f04 100644
--- a/docs/source/usage/api_modelling.md
+++ b/docs/source/usage/api_modelling.md
@@ -4,6 +4,7 @@ kernelspec:
   name: python3
 ---
 
+(how_to_cpp_api_modelling)=
 # Modelling C++ APIs in pystencils-sfg
 
 Pystencils-SFG is designed to help you generate C++ code that interfaces with pystencils on the one side,
diff --git a/docs/source/usage/composer.md b/docs/source/usage/composer.md
deleted file mode 100644
index fa96539..0000000
--- a/docs/source/usage/composer.md
+++ /dev/null
@@ -1,88 +0,0 @@
----
-file_format: mystnb
-kernelspec:
-  name: python3
----
-
-(composer_guide)=
-# How To Use the Composer API
-
-```{code-cell} ipython3
-:tags: [remove-cell]
-
-import sys
-from pathlib import Path
-
-mockup_path = Path("../_util").resolve()
-sys.path.append(str(mockup_path))
-
-from sfg_monkeypatch import DocsPatchedGenerator  # monkeypatch SFG for docs
-
-from pystencilssfg import SourceFileGenerator
-```
-
-The *composer API* is the interface by which C++ code is constructed in pystencils-sfg.
-It is exposed through the ubiquitous *composer object* returned by the `SourceFileGenerator`
-upon entry into its managed region.
-This guide is meant to illustrate the various constructions possible through the composer,
-starting from things as simple as `#include` directives and plain code strings,
-up to entire classes and their members.
-
-## Basic Functionality
-
-### Prelude Comment
-
-You can equip your generated files with a prelude comment that will be printed at their very top:
-
-```{code-cell} ipython3
-import datetime
-
-now = datetime.datetime.now()
-
-with SourceFileGenerator() as sfg:
-    sfg.prelude(f"This file was generated using pystencils-sfg at {now}.")
-```
-
-### `#include` Directives
-
-Use `sfg.include` to add `#include` directives to your generated files.
-For a system-header include, delimit the header name with `<>`.
-If the directive should be printed not into the header, but the implementation file,
-set `private = True`:
-
-```{code-cell} ipython3
-with SourceFileGenerator() as sfg:
-    sfg.include("my_header.hpp")
-    sfg.include("<memory>")
-    sfg.include("detail_header.hpp", private=True)
-```
-
-### Plain Code Strings
-
-It is always possible to print out plain code strings verbatim.
-Use `sfg.code()` to write code directly to the generated header file.
-To emit the code to the implementation file instead, use `sfg.code(..., impl=True)`.
-
-```{code-cell} ipython3
-with SourceFileGenerator() as sfg:
-    sfg.code("int THE_ANSWER;")
-    sfg.code("int THE_ANSWER = 42;", impl=True)
-```
-
-## Defining Functions
-
-Free functions can be declared and defined using the `sfg.function` sequencer.
-It uses *builder syntax* to declare the various properties of the function in arbitrary
-order via a sequence of calls. This sequence must end with a plain pair of parentheses `( ... )`
-within which the function body will be defined.
-For example, the following will create a function `getValue` with return type `int32` which is marked with the `nodiscard`
-attribute:
-
-```{code-cell} ipython3
-with SourceFileGenerator() as sfg:
-    sfg.function("getValue").returns("int32").attr("nodiscard")(
-        "return 42;"
-    )
-```
-
-For a list of all possible function qualifiers, see the reference of {any}`SfgFunctionSequencer`.
diff --git a/docs/source/usage/how_to_composer.md b/docs/source/usage/how_to_composer.md
new file mode 100644
index 0000000..23f07ce
--- /dev/null
+++ b/docs/source/usage/how_to_composer.md
@@ -0,0 +1,246 @@
+---
+file_format: mystnb
+kernelspec:
+  name: python3
+---
+
+(composer_guide)=
+# How To Use the Composer API
+
+```{code-cell} ipython3
+:tags: [remove-cell]
+
+import sys
+from pathlib import Path
+
+mockup_path = Path("../_util").resolve()
+sys.path.append(str(mockup_path))
+
+from sfg_monkeypatch import DocsPatchedGenerator  # monkeypatch SFG for docs
+
+from pystencilssfg import SourceFileGenerator
+```
+
+The *composer API* is the interface by which C++ code is constructed in pystencils-sfg.
+It is exposed through the ubiquitous *composer object* returned by the `SourceFileGenerator`
+upon entry into its managed region.
+This guide is meant to illustrate the various constructions possible through the composer,
+starting from things as simple as `#include` directives and plain code strings,
+up to entire classes and their members.
+
+## Basic Functionality
+
+### Prelude Comment
+
+You can equip your generated files with a prelude comment that will be printed at their very top:
+
+```{code-cell} ipython3
+import datetime
+
+now = datetime.datetime.now()
+
+with SourceFileGenerator() as sfg:
+    sfg.prelude(f"This file was generated using pystencils-sfg at {now}.")
+```
+
+### `#include` Directives
+
+Use `sfg.include` to add `#include` directives to your generated files.
+For a system-header include, delimit the header name with `<>`.
+If the directive should be printed not into the header, but the implementation file,
+set `private = True`:
+
+```{code-cell} ipython3
+with SourceFileGenerator() as sfg:
+    sfg.include("my_header.hpp")
+    sfg.include("<memory>")
+    sfg.include("detail_header.hpp", private=True)
+```
+
+### Plain Code Strings
+
+It is always possible to print out plain code strings verbatim.
+Use `sfg.code()` to write code directly to the generated header file.
+To emit the code to the implementation file instead, use `sfg.code(..., impl=True)`.
+
+```{code-cell} ipython3
+with SourceFileGenerator() as sfg:
+    sfg.code("int THE_ANSWER;")
+    sfg.code("int THE_ANSWER = 42;", impl=True)
+```
+
+## Defining Functions
+
+Free functions can be declared and defined using the `sfg.function` sequencer.
+It uses *builder syntax* to declare the various properties of the function in arbitrary
+order via a sequence of calls. This sequence must end with a plain pair of parentheses `( ... )`
+within which the function body will be defined.
+For example, the following will create a function `getValue` with return type `int32` which is marked with the `nodiscard`
+attribute:
+
+```{code-cell} ipython3
+with SourceFileGenerator() as sfg:
+    sfg.function("getValue").returns("int32").attr("nodiscard")(
+        "return 42;"
+    )
+```
+
+For a list of all available function qualifiers, see the reference of {any}`SfgFunctionSequencer`.
+
+### Populate the Function Body
+
+The function body sequencer takes an arbitrary list of arguments of different types
+which are then interpreted as C++ code.
+The simplest case are plain strings, which will be printed out verbatim,
+in order, each string argument on its own line:
+
+```{code-cell} ipython3
+with SourceFileGenerator() as sfg:
+    sfg.function("factorial").params(
+        sfg.var("n", "uint64")
+    ).returns("uint64")(
+        "if(n == 0) return 1;",
+        "else return n * factorial(n - 1);"
+    )
+```
+
+However, to make life easier, the composer API offers various APIs to model C++ code programmatically.
+
+:::{note}
+Observe that the code generated from the above snippet contains line breaks after the `if()` and `else` keywords
+that where not part of the input.
+This happens because `pystencils-sfg` passes its generated code through `clang-format` for beautification.
+:::
+
+#### Conditionals
+
+To emit an if-else conditional statement, use {any}`sfg.branch <SfgBasicComposer.branch>`.
+The syntax of `sfg.branch` mimics the C++ `if () {} else {}` construct by a sequence of
+two (or three, with an `else`-branch) pairs of parentheses:
+
+```{code-cell} ipython3
+with SourceFileGenerator() as sfg:
+    sfg.function("factorial").params(
+        sfg.var("n", "uint64")
+    ).returns("uint64")(
+        sfg.branch("n == 0")(  # Condition
+            #   then-block
+            "return 1;"
+        )(
+            #   else-block
+            "return n * factorial(n - 1);"
+        )
+    )
+```
+
+#### Variables and Automatic Collection of Function Parameters
+
+Pystencils-sfg's versatile expression system can keep track of free variables
+in a function body, and then automatically exposes these variables as function parameters.
+To cast a code string as an expression depending on variables, we need to do two things:
+
+ - Create an object for each variable using {any}`sfg.var <SfgBasicComposer.var>`.
+   This method takes the name and data type of the variable.
+ - Create the expression through {any}`sfg.expr <SfgBasicComposer.expr>` by interpolating a
+   Python format string (see {any}`str.format`) with variables or other expressions.
+
+For example, here's the expression in the `else`-block of the `factorial` function modelled this way:
+
+```{code-block} python
+n = sfg.var("n", "uint64")
+...
+sfg.expr("return {0} * factorial({0} - 1);", n)
+```
+
+Using this, we can omit the manually specified parameter list for `factorial`:
+
+```{code-cell} ipython3
+with SourceFileGenerator() as sfg:
+    n = sfg.var("n", "uint64")
+
+    sfg.function("factorial").returns("uint64")(
+        sfg.branch(sfg.expr("{} == 0", n))(  # Condition
+            #   then-block
+            "return 1;"
+        )(
+            #   else-block, with interpolated expression
+            sfg.expr("return {0} * factorial({0} - 1);", n)
+        )
+    )
+```
+
+#### Manual Parameter Lists
+
+When function parameters are collected from the function body, the composer will always order them
+alphabetically. If this is not desired, e.g. if a generated function is expected to have a specific interface
+with a fixed parameter order, you will need to specify the parameter list manually using `.params(...)`.
+
+#### Variables of C++ Class Type
+
+`sfg.var` should only be used for the most basic data types: it parses its second argument as a data type using
+{any}`create_type <pystencils.types.create_type>`, which is restricted to primitive and fixed-width C types.
+For more complex C++ classes, class templates, and their APIs, pystencils-sfg provides its own modelling system,
+implemented in `pystencilssfg.lang`.
+This system is used, for instance, by `pystencilssfg.lang.cpp.std`, which mirrors (a small part of) the C++ standard library.
+
+:::{seealso}
+[](#how_to_cpp_api_modelling)
+:::
+
+To create a variable of a class template represented using the `pystencilssfg.lang` modelling system,
+first instantiate the class (with any template arguments, as well as optional `const` and `ref` qualifiers)
+and then call `var` on it:
+
+```{code-cell} ipython3
+from pystencilssfg.lang.cpp import std
+
+data = std.vector("float64", const=True, ref=True).var("data")
+str(data), str(data.dtype)
+```
+
+#### Initializing Variables
+
+To emit an initializer statement for a variable, use `sfg.init`:
+
+```{code-block} python
+from pystencilssfg.lang.cpp import std
+
+result = std.tuple("int32", "int32").var("result")
+n, m = sfg.vars("n, m", "int32")
+
+sfg.init(result)(
+    sfg.expr("{} / {}", n, m),
+    sfg.expr("{} % {}", n, m)
+)
+```
+
+This will be recognized by the parameter collector:
+variables that are defined using `init` before they are used will be considered *bound*
+and will not end up in the function signature.
+Also, any variables passed to the braced initializer-expression (by themselves or inside `sfg.expr`)
+will be found and tracked by the parameter collector:
+
+```{code-cell} ipython3
+from pystencilssfg.lang.cpp import std
+
+with SourceFileGenerator() as sfg:
+    result = std.tuple("int32", "int32").var("result")
+    n, m = sfg.vars("n, m", "int32")
+
+    sfg.function("div_rem").params(n, m).returns(result.dtype)(
+        sfg.init(result)(
+            sfg.expr("{} / {}", n, m),
+            sfg.expr("{} % {}", n, m)
+        ),
+        sfg.expr("return {}", result)
+    )
+```
+
+:::{admonition} To Do
+
+ - Creating and calling kernels
+ - Invoking GPU kernels and the CUDA API Mirror
+ - Defining classes, their fields constructors, and methods
+
+:::
+
diff --git a/src/pystencilssfg/composer/basic_composer.py b/src/pystencilssfg/composer/basic_composer.py
index 58c28e7..78f92b8 100644
--- a/src/pystencilssfg/composer/basic_composer.py
+++ b/src/pystencilssfg/composer/basic_composer.py
@@ -631,6 +631,7 @@ class SfgFunctionSequencer:
         self._cursor = cursor
         self._name = name
         self._return_type: PsType = void
+        self._params: list[SfgVar] | None = None
 
         #   Qualifiers
         self._inline: bool = False
@@ -644,6 +645,17 @@ class SfgFunctionSequencer:
         self._return_type = create_type(rtype)
         return self
 
+    def params(self, *args: VarLike) -> SfgFunctionSequencer:
+        """Specify the parameters for this function.
+
+        Use this to manually specify the function's parameter list.
+
+        If any free variables collected from the function body are not contained
+        in the parameter list, an error will be raised.
+        """
+        self._params = [asvar(v) for v in args]
+        return self
+
     def inline(self) -> SfgFunctionSequencer:
         """Mark this function as ``inline``."""
         self._inline = True
@@ -670,6 +682,7 @@ class SfgFunctionSequencer:
             inline=self._inline,
             constexpr=self._constexpr,
             attributes=self._attributes,
+            required_params=self._params,
         )
         self._cursor.add_entity(func)
 
diff --git a/src/pystencilssfg/ir/entities.py b/src/pystencilssfg/ir/entities.py
index 8d79825..3cb828c 100644
--- a/src/pystencilssfg/ir/entities.py
+++ b/src/pystencilssfg/ir/entities.py
@@ -228,15 +228,25 @@ class SfgFunction(SfgCodeEntity, CommonFunctionProperties):
         inline: bool = False,
         constexpr: bool = False,
         attributes: Sequence[str] = (),
+        required_params: Sequence[SfgVar] | None = None,
     ):
         super().__init__(name, namespace)
 
         from .postprocessing import CallTreePostProcessing
 
         param_collector = CallTreePostProcessing()
-        parameters = tuple(
-            sorted(param_collector(tree).function_params, key=lambda p: p.name)
-        )
+        params_set = param_collector(tree).function_params
+
+        if required_params is not None:
+            if not (params_set <= set(required_params)):
+                extras = params_set - set(required_params)
+                raise SfgException(
+                    "Extraenous function parameters: "
+                    f"Found free variables {extras} that were not listed in manually specified function parameters."
+                )
+            parameters = tuple(required_params)
+        else:
+            parameters = tuple(sorted(params_set, key=lambda p: p.name))
 
         CommonFunctionProperties.__init__(
             self,
diff --git a/src/pystencilssfg/lang/cpp/std_tuple.py b/src/pystencilssfg/lang/cpp/std_tuple.py
index 58a3530..7ea0e24 100644
--- a/src/pystencilssfg/lang/cpp/std_tuple.py
+++ b/src/pystencilssfg/lang/cpp/std_tuple.py
@@ -1,5 +1,3 @@
-from typing import Sequence
-
 from pystencils.types import UserTypeSpec, create_type
 
 from ...lang import SrcVector, AugExpr, cpptype
@@ -10,7 +8,7 @@ class StdTuple(SrcVector):
 
     def __init__(
         self,
-        element_types: Sequence[UserTypeSpec],
+        *element_types: UserTypeSpec,
         const: bool = False,
         ref: bool = False,
     ):
diff --git a/tests/generator_scripts/index.yaml b/tests/generator_scripts/index.yaml
index 0e08e22..57cee91 100644
--- a/tests/generator_scripts/index.yaml
+++ b/tests/generator_scripts/index.yaml
@@ -47,6 +47,8 @@ SimpleClasses:
   sfg-args:
     output-mode: header-only
 
+ComposerFeatures:
+
 Conditionals:
   expect-code:
     cpp:
diff --git a/tests/generator_scripts/source/ComposerFeatures.harness.cpp b/tests/generator_scripts/source/ComposerFeatures.harness.cpp
new file mode 100644
index 0000000..f24b726
--- /dev/null
+++ b/tests/generator_scripts/source/ComposerFeatures.harness.cpp
@@ -0,0 +1,13 @@
+#include "ComposerFeatures.hpp"
+
+/* factorial is constexpr -> evaluate at compile-time */
+static_assert( factorial(0) == 1 );
+static_assert( factorial(1) == 1 );
+static_assert( factorial(2) == 2 );
+static_assert( factorial(3) == 6 );
+static_assert( factorial(4) == 24 );
+static_assert( factorial(5) == 120 );
+
+int main(void) {
+    return 0;
+}
diff --git a/tests/generator_scripts/source/ComposerFeatures.py b/tests/generator_scripts/source/ComposerFeatures.py
new file mode 100644
index 0000000..fd64de5
--- /dev/null
+++ b/tests/generator_scripts/source/ComposerFeatures.py
@@ -0,0 +1,13 @@
+from pystencilssfg import SourceFileGenerator
+
+
+with SourceFileGenerator() as sfg:
+
+    #   Inline constexpr function with explicit parameter list
+    sfg.function("factorial").params(sfg.var("n", "uint64")).returns("uint64").inline().constexpr()(
+        sfg.branch("n == 0")(
+            "return 1;"
+        )(
+            "return n * factorial(n - 1);"
+        )
+    )
diff --git a/tests/lang/test_cpp_stl_classes.py b/tests/lang/test_cpp_stl_classes.py
index 47966e2..fee7c70 100644
--- a/tests/lang/test_cpp_stl_classes.py
+++ b/tests/lang/test_cpp_stl_classes.py
@@ -19,7 +19,7 @@ def test_stl_containers():
     assert no_spaces(expr.get_dtype().c_string()) == "conststd::vector<double>&"
     assert includes(expr) == {HeaderFile.parse("<vector>")}
 
-    expr = std.tuple(("float64", "int32", "uint16", "bool")).var("t")
+    expr = std.tuple("float64", "int32", "uint16", "bool").var("t")
     assert (
         no_spaces(expr.get_dtype().c_string())
         == "std::tuple<double,int32_t,uint16_t,bool>"
-- 
GitLab