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