Skip to content
Snippets Groups Projects
Commit 0381851b authored by Frederik Hennig's avatar Frederik Hennig
Browse files

Add `.params()` to `sfg.function`. Change `std.tuple` to take type params as...

Add `.params()` to `sfg.function`. Change `std.tuple` to take type params as `*args`. Extend composer guide. Add composer feature test.
parent 13f1e8ca
No related branches found
No related tags found
1 merge request!21Composer API Extensions and How-To Guide
Pipeline #74240 failed
......@@ -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
......@@ -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
......
......@@ -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,
......
......@@ -85,4 +85,162 @@ with SourceFileGenerator() as sfg:
)
```
For a list of all possible function qualifiers, see the reference of {any}`SfgFunctionSequencer`.
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
:::
......@@ -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)
......
......@@ -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,
......
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,
):
......
......@@ -47,6 +47,8 @@ SimpleClasses:
sfg-args:
output-mode: header-only
ComposerFeatures:
Conditionals:
expect-code:
cpp:
......
#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;
}
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);"
)
)
......@@ -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>"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment