diff --git a/docs/source/api/composer.rst b/docs/source/api/composer.rst index 667b0de60f9564e98de46b562e0fcc0afe7cf2a1..8d3cdce4ac64bd9c689acd127ab03eb32e9b4598 100644 --- a/docs/source/api/composer.rst +++ b/docs/source/api/composer.rst @@ -2,35 +2,44 @@ Composer API (``pystencilssfg.composer``) ***************************************** -.. autoclass:: pystencilssfg.composer.SfgComposer +.. module:: pystencilssfg.composer + +.. autoclass:: SfgComposer :members: -.. autoclass:: pystencilssfg.composer.SfgIComposer +.. autoclass:: SfgIComposer :members: -.. autoclass:: pystencilssfg.composer.SfgBasicComposer +.. autoclass:: SfgBasicComposer :members: -.. autoclass:: pystencilssfg.composer.SfgClassComposer +.. autoclass:: SfgClassComposer :members: Custom Generators ================= -.. autoclass:: pystencilssfg.composer.custom.CustomGenerator +.. module:: pystencilssfg.composer.custom + +.. autoclass:: CustomGenerator :members: Helper Methods and Builders =========================== -.. autofunction:: pystencilssfg.composer.make_sequence +.. module:: pystencilssfg.composer.basic_composer + +.. autofunction:: make_sequence + +.. autoclass:: SfgFunctionSequencer + :members: -.. autoclass:: pystencilssfg.composer.basic_composer.SfgNodeBuilder +.. autoclass:: SfgNodeBuilder :members: -.. autoclass:: pystencilssfg.composer.basic_composer.SfgBranchBuilder +.. autoclass:: SfgBranchBuilder :members: -.. autoclass:: pystencilssfg.composer.basic_composer.SfgSwitchBuilder +.. autoclass:: SfgSwitchBuilder :members: diff --git a/docs/source/index.md b/docs/source/index.md index 4b60903beee1426937961c3c107d584ac755a13d..9ffc4f0c547f181565496e9054447645fc8e767c 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -35,8 +35,9 @@ getting_started :maxdepth: 1 :caption: User Guide -usage/generator_scripts +usage/composer C++ API Modelling <usage/api_modelling> +usage/config_and_cli usage/project_integration usage/tips_n_tricks ``` diff --git a/docs/source/usage/composer.md b/docs/source/usage/composer.md new file mode 100644 index 0000000000000000000000000000000000000000..fa96539ef95ee75a2b71ce90646c3283d0d820d4 --- /dev/null +++ b/docs/source/usage/composer.md @@ -0,0 +1,88 @@ +--- +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/generator_scripts.md b/docs/source/usage/config_and_cli.md similarity index 100% rename from docs/source/usage/generator_scripts.md rename to docs/source/usage/config_and_cli.md diff --git a/src/pystencilssfg/composer/basic_composer.py b/src/pystencilssfg/composer/basic_composer.py index e75f0e29e4d41825a8d0d8cf1547bab046c1731a..58c28e7efcc73ba9c0c3469a8d31b4336406a849 100644 --- a/src/pystencilssfg/composer/basic_composer.py +++ b/src/pystencilssfg/composer/basic_composer.py @@ -7,9 +7,9 @@ from warnings import warn from pystencils import Field, CreateKernelConfig, create_kernel from pystencils.codegen import Kernel -from pystencils.types import create_type, UserTypeSpec +from pystencils.types import create_type, UserTypeSpec, PsType -from ..context import SfgContext +from ..context import SfgContext, SfgCursor from .custom import CustomGenerator from ..ir import ( SfgCallTreeNode, @@ -171,7 +171,7 @@ class SfgBasicComposer(SfgIComposer): else: f.prelude += content + end - def code(self, *code: str): + def code(self, *code: str, impl: bool = False): """Add arbitrary lines of code to the generated header file. :Example: @@ -188,9 +188,15 @@ class SfgBasicComposer(SfgIComposer): #define PI 3.14 // more than enough for engineers using namespace std; + Args: + code: Sequence of code strings to be written to the output file + impl: If `True`, write the code to the implementation file; otherwise, to the header file. """ for c in code: - self._cursor.write_header(c) + if impl: + self._cursor.write_impl(c) + else: + self._cursor.write_header(c) def define(self, *definitions: str): from warnings import warn @@ -318,10 +324,8 @@ class SfgBasicComposer(SfgIComposer): def function( self, name: str, - returns: UserTypeSpec = void, - inline: bool = False, return_type: UserTypeSpec | None = None, - ): + ) -> SfgFunctionSequencer: """Add a function. The syntax of this function adder uses a chain of two calls to mimic C++ syntax: @@ -334,33 +338,17 @@ class SfgBasicComposer(SfgIComposer): The function body is constructed via sequencing (see `make_sequence`). """ + seq = SfgFunctionSequencer(self._cursor, name) + if return_type is not None: warn( "The parameter `return_type` to `function()` is deprecated and will be removed by version 0.1. " - "Setting it will override the value of the `returns` parameter. " - "Use `returns` instead.", + "Use `.returns()` instead.", FutureWarning, ) - returns = return_type + seq.returns(return_type) - def sequencer(*args: SequencerArg): - tree = make_sequence(*args) - func = SfgFunction( - name, - self._cursor.current_namespace, - tree, - return_type=create_type(returns), - inline=inline, - ) - self._cursor.add_entity(func) - - if inline: - self._cursor.write_header(SfgEntityDef(func)) - else: - self._cursor.write_header(SfgEntityDecl(func)) - self._cursor.write_impl(SfgEntityDef(func)) - - return sequencer + return seq def call(self, kernel_handle: SfgKernelHandle) -> SfgCallTreeNode: """Use inside a function body to directly call a kernel. @@ -621,6 +609,77 @@ def make_sequence(*args: SequencerArg) -> SfgSequence: return SfgSequence(children) +class SfgFunctionSequencer: + """Sequencer for constructing free functions. + + This builder uses call sequencing to specify the function's properties. + + Example: + + >>> sfg.function( + ... "myFunction" + ... ).returns( + ... "float32" + ... ).attr( + ... "nodiscard", "maybe_unused" + ... ).inline().constexpr()( + ... "return 31.2;" + ... ) + """ + + def __init__(self, cursor: SfgCursor, name: str) -> None: + self._cursor = cursor + self._name = name + self._return_type: PsType = void + + # Qualifiers + self._inline: bool = False + self._constexpr: bool = False + + # Attributes + self._attributes: list[str] = [] + + def returns(self, rtype: UserTypeSpec) -> SfgFunctionSequencer: + """Set the return type of the function""" + self._return_type = create_type(rtype) + return self + + def inline(self) -> SfgFunctionSequencer: + """Mark this function as ``inline``.""" + self._inline = True + return self + + def constexpr(self) -> SfgFunctionSequencer: + """Mark this function as ``constexpr``.""" + self._constexpr = True + return self + + def attr(self, *attrs: str) -> SfgFunctionSequencer: + """Add attributes to this function""" + self._attributes += attrs + return self + + def __call__(self, *args: SequencerArg) -> None: + """Populate the function body""" + tree = make_sequence(*args) + func = SfgFunction( + self._name, + self._cursor.current_namespace, + tree, + return_type=self._return_type, + inline=self._inline, + constexpr=self._constexpr, + attributes=self._attributes, + ) + self._cursor.add_entity(func) + + if self._inline: + self._cursor.write_header(SfgEntityDef(func)) + else: + self._cursor.write_header(SfgEntityDecl(func)) + self._cursor.write_impl(SfgEntityDef(func)) + + class SfgBranchBuilder(SfgNodeBuilder): """Multi-call builder for C++ ``if/else`` statements.""" diff --git a/src/pystencilssfg/emission/file_printer.py b/src/pystencilssfg/emission/file_printer.py index 6ab98eb89aba143dd567fc0c176199ff9d1444fe..dea6605b10dddc792df316eefc50c40d01a73126 100644 --- a/src/pystencilssfg/emission/file_printer.py +++ b/src/pystencilssfg/emission/file_printer.py @@ -34,16 +34,16 @@ class SfgFilePrinter: def __call__(self, file: SfgSourceFile) -> str: code = "" - if file.file_type == SfgSourceFileType.HEADER: - code += "#pragma once\n\n" - if file.prelude: comment = "/**\n" - comment += indent(file.prelude, " * ") + comment += indent(file.prelude, " * ", predicate=lambda _: True) comment += " */\n\n" code += comment + if file.file_type == SfgSourceFileType.HEADER: + code += "#pragma once\n\n" + for header in file.includes: incl = str(header) if header.system_header else f'"{str(header)}"' code += f"#include {incl}\n" @@ -148,7 +148,9 @@ class SfgFilePrinter: code += f" {defined_entity.owning_class.name}::" code += f" {name}" if defined_entity.default_init is not None: - args_str = ", ".join(str(expr) for expr in defined_entity.default_init) + args_str = ", ".join( + str(expr) for expr in defined_entity.default_init + ) code += "{" + args_str + "}" code += ";" return code @@ -177,8 +179,19 @@ class SfgFilePrinter: def _func_signature(self, func: SfgFunction | SfgMethod, inclass: bool): code = "" + + if func.attributes: + code += "[[" + ", ".join(func.attributes) + "]]" + if func.inline: code += "inline " + + if isinstance(func, SfgMethod) and func.static: + code += "static " + + if func.constexpr: + code += "constexpr " + code += func.return_type.c_string() + " " params_str = ", ".join( f"{param.dtype.c_string()} {param.name}" for param in func.parameters diff --git a/src/pystencilssfg/ir/entities.py b/src/pystencilssfg/ir/entities.py index 62ae1eb7611065c7b1a12b77d6894143aed341dd..d97e81022e611c9e3ac87ba14e69a8f6ebdfb46b 100644 --- a/src/pystencilssfg/ir/entities.py +++ b/src/pystencilssfg/ir/entities.py @@ -1,5 +1,6 @@ from __future__ import annotations +from dataclasses import dataclass from abc import ABC from enum import Enum, auto from typing import ( @@ -203,10 +204,20 @@ class SfgKernelNamespace(SfgNamespace): self._kernels[kernel.name] = kernel -class SfgFunction(SfgCodeEntity): +@dataclass(frozen=True) +class CommonFunctionProperties: + tree: SfgCallTreeNode + parameters: tuple[SfgVar, ...] + return_type: PsType + inline: bool + constexpr: bool + attributes: Sequence[str] + + +class SfgFunction(SfgCodeEntity, CommonFunctionProperties): """A free function.""" - __match_args__ = ("name", "tree", "parameters", "return_type") + __match_args__ = ("name", "tree", "parameters", "return_type") # type: ignore def __init__( self, @@ -215,37 +226,27 @@ class SfgFunction(SfgCodeEntity): tree: SfgCallTreeNode, return_type: PsType = void, inline: bool = False, + constexpr: bool = False, + attributes: Sequence[str] = (), ): super().__init__(name, namespace) - self._tree = tree - self._return_type = return_type - self._inline = inline - - self._parameters: tuple[SfgVar, ...] - from .postprocessing import CallTreePostProcessing param_collector = CallTreePostProcessing() - self._parameters = tuple( - sorted(param_collector(self._tree).function_params, key=lambda p: p.name) + parameters = tuple( + sorted(param_collector(tree).function_params, key=lambda p: p.name) ) - @property - def parameters(self) -> tuple[SfgVar, ...]: - return self._parameters - - @property - def tree(self) -> SfgCallTreeNode: - return self._tree - - @property - def return_type(self) -> PsType: - return self._return_type - - @property - def inline(self) -> bool: - return self._inline + CommonFunctionProperties.__init__( + self, + tree, + parameters, + return_type, + inline, + constexpr, + attributes, + ) class SfgVisibility(Enum): @@ -323,10 +324,10 @@ class SfgMemberVariable(SfgVar, SfgClassMember): return self._default_init -class SfgMethod(SfgClassMember): +class SfgMethod(SfgClassMember, CommonFunctionProperties): """Instance method of a class""" - __match_args__ = ("name", "tree", "parameters", "return_type") + __match_args__ = ("name", "tree", "parameters", "return_type") # type: ignore def __init__( self, @@ -336,43 +337,35 @@ class SfgMethod(SfgClassMember): return_type: PsType = void, inline: bool = False, const: bool = False, + static: bool = False, + constexpr: bool = False, + attributes: Sequence[str] = (), ): super().__init__(cls) - self._name = name - self._tree = tree - self._return_type = return_type - self._inline = inline - self._const = const - - self._parameters: tuple[SfgVar, ...] - from .postprocessing import CallTreePostProcessing param_collector = CallTreePostProcessing() - self._parameters = tuple( - sorted(param_collector(self._tree).function_params, key=lambda p: p.name) + parameters = tuple( + sorted(param_collector(tree).function_params, key=lambda p: p.name) ) - @property - def name(self) -> str: - return self._name - - @property - def parameters(self) -> tuple[SfgVar, ...]: - return self._parameters - - @property - def tree(self) -> SfgCallTreeNode: - return self._tree + self._static = static + self._const = const - @property - def return_type(self) -> PsType: - return self._return_type + CommonFunctionProperties.__init__( + self, + tree, + parameters, + return_type, + inline, + constexpr, + attributes, + ) @property - def inline(self) -> bool: - return self._inline + def static(self) -> bool: + return self._static @property def const(self) -> bool: