diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c430eab4dac8e174f368d3cbfffe350ceaa83b8f..1420bd2b53e918cfce8e6b5491e94888ad36a0f9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -53,10 +53,11 @@ build-documentation: stage: "Documentation" needs: [] script: - - nox --session docs + - nox -s docs -- --fail-on-warnings artifacts: paths: - docs/build/html + when: always pages: image: alpine:latest diff --git a/docs/source/.gitignore b/docs/source/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..6577d5875e1b2de85630d5aa8f0af0f5ede45333 --- /dev/null +++ b/docs/source/.gitignore @@ -0,0 +1 @@ +**/_sfg_out \ No newline at end of file diff --git a/docs/source/_util/sfg_monkeypatch.py b/docs/source/_util/sfg_monkeypatch.py new file mode 100644 index 0000000000000000000000000000000000000000..0269d40f43492ea1540f51f49c8e78c5ebebf37d --- /dev/null +++ b/docs/source/_util/sfg_monkeypatch.py @@ -0,0 +1,68 @@ +import pystencilssfg +from pystencilssfg.config import SfgConfig + + +class DocsPatchedGenerator(pystencilssfg.SourceFileGenerator): + """Mockup wrapper around SourceFileGenerator for use in documentation + notebooks to print the generated code directly to the HTML + instead of writing it to file.""" + + scriptname: str = "demo" + glue: bool = False + display: bool = True + + @classmethod + def setup(cls, scriptname: str, glue: bool = False, display: bool = True): + cls.scriptname = scriptname + cls.glue = glue + cls.display = display + + def _scriptname(self) -> str: + return f"{DocsPatchedGenerator.scriptname}.py" + + def __init__( + self, sfg_config: SfgConfig | None = None, keep_unknown_argv: bool = False + ): + super().__init__(sfg_config, keep_unknown_argv=True) + + def __exit__(self, exc_type, exc_value, traceback): + if exc_type is None: + self._finish_files() + + header_code = self._emitter.dumps(self._header_file) + impl_code = ( + None + if self._impl_file is None + else self._emitter.dumps(self._impl_file) + ) + + mdcode = ":::::{tab-set}\n" + + mdcode += "::::{tab-item} Generated Header (.hpp)\n" + mdcode += ":::{code-block} C++\n\n" + mdcode += header_code + mdcode += "\n:::\n::::\n" + + if impl_code: + mdcode += "::::{tab-item} Generated Implementation (.cpp)\n" + mdcode += ":::{code-block} C++\n\n" + mdcode += impl_code + mdcode += "\n:::\n::::\n" + + mdcode += ":::::" + from IPython.display import Markdown + + mdobj = Markdown(mdcode) + + if self.glue: + from myst_nb import glue + + glue(f"sfg_out_{self.scriptname}", mdobj, display=False) + + if self.display: + from IPython.display import display + + display(mdobj) + + +pystencilssfg.SourceFileGenerator = DocsPatchedGenerator diff --git a/docs/source/api/composer.rst b/docs/source/api/composer.rst index 667b0de60f9564e98de46b562e0fcc0afe7cf2a1..124d0fb97ac8f94bc2a6d4c38815edee8403c65b 100644 --- a/docs/source/api/composer.rst +++ b/docs/source/api/composer.rst @@ -2,35 +2,58 @@ 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:: KernelsAdder + :members: + +.. autoclass:: SfgFunctionSequencer + :members: -.. autoclass:: pystencilssfg.composer.basic_composer.SfgNodeBuilder +.. autoclass:: SfgNodeBuilder :members: -.. autoclass:: pystencilssfg.composer.basic_composer.SfgBranchBuilder +.. autoclass:: SfgBranchBuilder + :members: + +.. autoclass:: SfgSwitchBuilder + :members: + +Context and Cursor +================== + +.. module:: pystencilssfg.context + +.. autoclass:: SfgContext :members: -.. autoclass:: pystencilssfg.composer.basic_composer.SfgSwitchBuilder +.. autoclass:: SfgCursor :members: diff --git a/docs/source/api/generation.rst b/docs/source/api/generation.rst index 935e722452b2755a98bee98063de12c0cc84c54f..8e7b0b4961b34154e3c20694f1e69c225de6623b 100644 --- a/docs/source/api/generation.rst +++ b/docs/source/api/generation.rst @@ -24,6 +24,9 @@ Categories, Parameter Types, and Special Values .. autoclass:: OutputMode :members: +.. autoclass:: FileExtensions + :members: + .. autoclass:: CodeStyle :members: diff --git a/docs/source/api/ir.rst b/docs/source/api/ir.rst index a9451f46c239b73b32e1f8a79b8a166d753ed8e6..653667b1b9f3fdc0216dea2fac034f4c83fdb6f6 100644 --- a/docs/source/api/ir.rst +++ b/docs/source/api/ir.rst @@ -1,8 +1,11 @@ Internal Code Representation (`pystencilssfg.ir`) ================================================= -.. autoclass:: pystencilssfg.SfgContext +.. automodule:: pystencilssfg.ir :members: -.. automodule:: pystencilssfg.ir +Postprocessing +-------------- + +.. automodule:: pystencilssfg.ir.postprocessing :members: diff --git a/docs/source/conf.py b/docs/source/conf.py index d6aab17bc3f667bbfc923c80c2d1c35b9e08a3d7..ecfaaefcedec5713a4e77ebe9ab4c110118e9bd7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -38,7 +38,6 @@ extensions = [ templates_path = ["_templates"] exclude_patterns = [] master_doc = "index" -nitpicky = True # -- Options for HTML output ------------------------------------------------- @@ -71,7 +70,10 @@ default_role = "any" autodoc_member_order = "bysource" autodoc_typehints = "description" -# autodoc_class_signature = "separated" +# autodoc_type_aliases = { +# "VarLike": "pystencilssfg.lang.expressions.VarLike", +# "ExprLike": "pystencilssfg.lang.expressions.ExprLike" +# } # Doctest Setup @@ -89,17 +91,4 @@ myst_enable_extensions = [ "dollarmath", "colon_fence", ] - -# Prepare code generation examples - -def build_examples(): - import subprocess - import os - - examples_dir = os.path.join("usage", "examples",) - - subprocess.run(["python", "build.py"], cwd=examples_dir).check_returncode() - - -print("Generating output of example scripts...") -build_examples() +nb_render_markdown_format = "myst" diff --git a/docs/source/getting_started.md b/docs/source/getting_started.md new file mode 100644 index 0000000000000000000000000000000000000000..b73a97bce75704d2689376f2daab5fd9e976b086 --- /dev/null +++ b/docs/source/getting_started.md @@ -0,0 +1,215 @@ +--- +file_format: mystnb +kernelspec: + name: python3 +--- + +(getting_started_guide)= +# Getting Started + +```{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 +``` + + +This guide will explain the basics of using pystencils-sfg through generator scripts. +Generator scripts are the primary way to run code generation with pystencils-sfg. +A generator script is a Python script that, when executed, produces one or more +C++ source files with the same base name, but different file extensions. + +## Writing a Basic Generator Script + +To start using pystencils-sfg, create a new empty Python file and populate it with the +following minimal skeleton: + +```{code-block} python +from pystencilssfg import SourceFileGenerator + +with SourceFileGenerator() as sfg: + ... +``` + +The above snippet defines the basic structure of a *generator script*. +When executed, the above will produce two (nearly) empty C++ files +in the current folder, both with the same name as your Python script +but with `.hpp` and `.cpp` file extensions instead. + +In the generator script, code generation is orchestrated by the `SourceFileGenerator` context manager. +When entering into the region controlled by the `SourceFileGenerator`, +it supplies us with a *composer object*, customarily called `sfg`. +Through the composer, we can declaratively populate the generated files with code. + +## Adding a pystencils Kernel + +One of the core applications of pystencils-sfg is to generate and wrap pystencils-kernels +for usage within C++ applications. +To register a kernel, pass its assignments to `sfg.kernels.create`, which returns a *kernel handle* object: + +```{code-block} python +src, dst = ps.fields("src, dst: double[1D]") +c = sp.Symbol("c") + +@ps.kernel +def scale(): + dst.center @= c * src.center() + +# Register the kernel for code generation +scale_kernel = sfg.kernels.create(scale, "scale_kernel") +``` + +In order to call the kernel, and expose it to the outside world, +we have to create a wrapper function for it, using `sfg.function`. +In its body, we use `sfg.call` to invoke the kernel: + +```{code-block} python +sfg.function("scale")( + sfg.call(scale_kernel) +) +``` + +The `function` composer has a special syntax that mimics the generated C++ code. +We call it twice in sequence, +first providing the name of the function, and then populating its body. + +Here's our full first generator script: + +```{code-cell} ipython3 +:tags: [remove-cell] + +DocsPatchedGenerator.scriptname = "add_kernel_demo" +DocsPatchedGenerator.glue = True +DocsPatchedGenerator.display = False +``` + +```{code-cell} ipython3 +from pystencilssfg import SourceFileGenerator +import pystencils as ps +import sympy as sp + +with SourceFileGenerator() as sfg: + # Define a copy kernel + src, dst = ps.fields("src, dst: double[1D]") + c = sp.Symbol("c") + + @ps.kernel + def scale(): + dst.center @= c * src.center() + + # Register the kernel for code generation + scale_kernel = sfg.kernels.create(scale, "scale_kernel") + + # Wrap it in a function + sfg.function("scale")( + sfg.call(scale_kernel) + ) + +``` + +When executing the above script, two files will be generated: a C++ header and implementation file containing +the `scale_kernel` and its wrapper function: + +:::{glue:md} sfg_out_add_kernel_demo +:format: myst +::: + +As you can see, the header file contains a declaration `void scale(...)` of a function +which is defined in the associated implementation file, +and there calls our generated numerical kernel. +As of now, it forwards the entire set of low-level kernel arguments -- array pointers and indexing information -- +to the outside. +In numerical applications, this information is most of the time hidden from the user by encapsulating +it in high-level C++ data structures. +Pystencils-sfg offers means of representing such data structures in the code generator, and supports the +automatic extraction of the low-level indexing information from them. + +## Mapping Fields to Data Structures + +Since C++23 there exists the archetypical [std::mdspan][mdspan], which represents a non-owning n-dimensional view +on a contiguous data array. +Pystencils-sfg offers native support for mapping pystencils fields onto `mdspan` instances in order to +hide their memory layout details. + +Import `std` from `pystencilssfg.lang.cpp` and use `std.mdspan.from_field` to create representations +of your pystencils fields as `std::mdspan` objects: + +```{code-block} python +from pystencilssfg.lang.cpp import std + +... + +src_mdspan = std.mdspan.from_field(src) +dst_mdspan = std.mdspan.from_field(dst) +``` + +Then, inside the wrapper function, instruct the SFG to map the fields onto their corresponding mdspans: + +```{code-block} python +sfg.function("scale")( + sfg.map_field(src, src_mdspan), + sfg.map_field(dst, dst_mdspan), + sfg.call(scale_kernel) +) +``` + +Here's the full script and its output: + +```{code-cell} ipython3 +:tags: [remove-cell] + +DocsPatchedGenerator.setup("mdspan_demo", False, True) +``` + + +```{code-cell} ipython3 +from pystencilssfg import SourceFileGenerator +import pystencils as ps +import sympy as sp + +from pystencilssfg.lang.cpp import std + +with SourceFileGenerator() as sfg: + # Define a copy kernel + src, dst = ps.fields("src, dst: double[1D]") + c = sp.Symbol("c") + + @ps.kernel + def scale(): + dst.center @= c * src.center() + + # Register the kernel for code generation + scale_kernel = sfg.kernels.create(scale, "scale_kernel") + + # Create mdspan objects + src_mdspan = std.mdspan.from_field(src) + dst_mdspan = std.mdspan.from_field(dst) + + # Wrap it in a function + sfg.function("scale")( + sfg.map_field(src, src_mdspan), + sfg.map_field(dst, dst_mdspan), + sfg.call(scale_kernel) + ) + +``` + +:::{note} + +As of early 2025, `std::mdspan` is still not fully adopted by standard library implementors +(see [cppreference.com][cppreference_compiler_support]); +most importantly, the GNU libstdc++ does not yet ship an implementation of it. +However, a reference implementation is available at https://github.com/kokkos/mdspan. +If you are using the reference implementation, refer to the documentation of {any}`StdMdspan` +for advice on how to configure the header file and namespace where the class is defined. +::: + + +[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 0cab08335824b9c6247052f243127b8d72287dfa..c16446dee6af49e2c30226351f695a07350d4325 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -1,259 +1,58 @@ # The pystencils Source File Generator -```{toctree} -:maxdepth: 1 -:hidden: -:caption: User Guide - -usage/generator_scripts -C++ API Modelling <usage/api_modelling> -usage/project_integration -usage/tips_n_tricks -``` - - -```{toctree} -:maxdepth: 1 -:hidden: -:caption: API Reference - -api/generation -api/composer -api/lang -api/ir -api/errors -``` [](https://i10git.cs.fau.de/pycodegen-/pystencils-sfg/commits/master) [](https://i10git.cs.fau.de/pycodegen-/pystencils-sfg/commits/master) [](https://i10git.cs.fau.de/pycodegen/pystencils-sfg/-/blob/master/LICENSE) -A bridge over the semantic gap between code emitted by [pystencils](https://pypi.org/project/pystencils/) -and your C/C++/Cuda/HIP framework. - -## Installation - -### From Git - -Install the package into your current Python environment from the git repository using pip -(usage of virtual environments is strongly encouraged!): - -```bash -pip install "git+https://i10git.cs.fau.de/pycodegen/pystencils-sfg.git" -``` - -````{caution} - -*pystencils-sfg* requires *pystencils 2.0* and is not compatible with *pystencils 1.3.x*. -However, *pystencils 2.0* is still under development and only available as a pre-release version. -To use *pystencils-sfg*, explicitly install *pystencils* from the v2.0 development branch: - -```bash -pip install "git+https://i10git.cs.fau.de/pycodegen/pystencils.git@v2.0-dev" -``` -```` - -### From PyPI - -Not yet available. - -## Primer - -With *pystencils-sfg*, including your *pystencils*-generated kernels with handwritten code becomes straightforward -and intuitive. To illustrate, generating a Jacobi smoother for the two-dimensional Poisson equation -and mapping it onto C++23 `std::mdspan`s takes just a few lines of code: - -```python -import sympy as sp - -from pystencils import fields, kernel - -from pystencilssfg import SourceFileGenerator -from pystencilssfg.lang.cpp import std - -with SourceFileGenerator() as sfg: - u_src, u_dst, f = fields("u_src, u_dst, f(1) : double[2D]", layout="fzyx") - h = sp.Symbol("h") - - @kernel - def poisson_jacobi(): - u_dst[0,0] @= (h**2 * f[0, 0] + u_src[1, 0] + u_src[-1, 0] + u_src[0, 1] + u_src[0, -1]) / 4 - - poisson_kernel = sfg.kernels.create(poisson_jacobi) - - sfg.function("jacobi_smooth")( - sfg.map_field(u_src, std.mdspan.from_field(u_src)), - sfg.map_field(u_dst, std.mdspan.from_field(u_dst)), - sfg.map_field(f, std.mdspan.from_field(f)), - sfg.call(poisson_kernel) - ) -``` - -The script above, and the code within the region controlled by the `SourceFileGenerator`, -constructs a C++ header/implementation file pair by describing its contents. -We first describe our Jacobi smoother symbolically using *pystencils* -and then pass it to the `sfg` to add it to the output file. -Then, a wrapper function `jacobi_smooth` is defined which maps the symbolic fields onto `std::mdspan` -objects and then executes the kernel. - -Take this code, store it into a file `poisson_smoother.py`, and execute the script from a terminal: +*A bridge over the semantic gap between [pystencils](https://pypi.org/project/pystencils/) and C++ HPC frameworks.* -```shell -python poisson_smoother.py -``` - -During execution, *pystencils-sfg* assembles the above constructs into an internal representation of the C++ files. -It then takes the name of your Python script, replaces `.py` with `.cpp` and `.hpp`, -and exports the constructed code to the files -`poisson_smoother.cpp` and `poisson_smoother.hpp` into the current directory, ready to be `#include`d. - -````{dropdown} poisson_smoother.hpp - -```C++ -#pragma once - -#include <cstdint> -#include <experimental/mdspan> - -#define RESTRICT __restrict__ - -void jacobi_smooth( - std::mdspan<double, std::extents<uint64_t, std::dynamic_extent, std::dynamic_extent, 1>> &f, - const double h, - std::mdspan<double, std::extents<uint64_t, std::dynamic_extent, std::dynamic_extent>> &u_dst, - std::mdspan<double, std::extents<uint64_t, std::dynamic_extent, std::dynamic_extent>> &u_src -); -``` - -```` - -````{dropdown} poisson_smoother.cpp - -```C++ -#include "poisson_smoother.hpp" +The pystencils Source File Generator is a code generation tool that allows you to +declaratively describe and automatically generate C++ code using its Python API. +It is part of the wider [pycodegen][pycodegen] family of packages for scientific code generation. -#include <math.h> +The primary purpose of pystencils-sfg is to embed the [pystencils][pystencils] code generator for +high-performance stencil computations into C++ HPC applications and frameworks of all scales. +Its features include: -#define FUNC_PREFIX inline + - Exporting pystencils kernels to C++ source files for use in larger projects + - Mapping of symbolic pystencils fields onto a wide variety of n-dimensional array data structures + - Orchestration of code generation as part of a Makefile or CMake project + - Declarative description of C++ code structure including functions and classes using the versatile composer API + - Reflection of C++ APIs in the code generator, including automatic tracking of variables and `#include`s -/************************************************************************************* - * Kernels - *************************************************************************************/ -namespace kernels { +## Table of Contents -FUNC_PREFIX void kernel(const int64_t _size_f_0, const int64_t _size_f_1, - const int64_t _stride_f_0, const int64_t _stride_f_1, - const int64_t _stride_u_dst_0, - const int64_t _stride_u_dst_1, - const int64_t _stride_u_src_0, - const int64_t _stride_u_src_1, double *const f_data, - const double h, double *const u_dst_data, - double *const u_src_data) { - const double __c_1_0o4_0 = 1.0 / 4.0; - for (int64_t ctr_1 = 1LL; ctr_1 < _size_f_1 - 1LL; ctr_1 += 1LL) { - for (int64_t ctr_0 = 1LL; ctr_0 < _size_f_0 - 1LL; ctr_0 += 1LL) { - u_dst_data[ctr_0 * _stride_u_dst_0 + ctr_1 * _stride_u_dst_1] = - __c_1_0o4_0 * u_src_data[(ctr_0 + 1LL) * _stride_u_src_0 + - ctr_1 * _stride_u_src_1] + - __c_1_0o4_0 * u_src_data[ctr_0 * _stride_u_src_0 + - (ctr_1 + 1LL) * _stride_u_src_1] + - __c_1_0o4_0 * u_src_data[ctr_0 * _stride_u_src_0 + - (ctr_1 + -1LL) * _stride_u_src_1] + - __c_1_0o4_0 * u_src_data[(ctr_0 + -1LL) * _stride_u_src_0 + - ctr_1 * _stride_u_src_1] + - __c_1_0o4_0 * (h * h) * - f_data[ctr_0 * _stride_f_0 + ctr_1 * _stride_f_1]; - } - } -} - -} // namespace kernels - -/************************************************************************************* - * Functions - *************************************************************************************/ - -void jacobi_smooth( - std::mdspan<double, std::extents<uint64_t, std::dynamic_extent, std::dynamic_extent, 1>> &f, - const double h, - std::mdspan<double, std::extents<uint64_t, std::dynamic_extent, std::dynamic_extent>> &u_dst, - std::mdspan<double, std::extents<uint64_t, std::dynamic_extent, std::dynamic_extent>> &u_src) -{ - double *const u_src_data{u_src.data_handle()}; - const int64_t _stride_u_src_0{u_src.stride(0)}; - const int64_t _stride_u_src_1{u_src.stride(1)}; - double *const u_dst_data{u_dst.data_handle()}; - const int64_t _stride_u_dst_0{u_dst.stride(0)}; - const int64_t _stride_u_dst_1{u_dst.stride(1)}; - double *const f_data{f.data_handle()}; - const int64_t _size_f_0{f.extents().extent(0)}; - const int64_t _size_f_1{f.extents().extent(1)}; - /* f.extents().extent(2) == 1 */ - const int64_t _stride_f_0{f.stride(0)}; - const int64_t _stride_f_1{f.stride(1)}; - kernels::kernel(_size_f_0, _size_f_1, _stride_f_0, _stride_f_1, - _stride_u_dst_0, _stride_u_dst_1, _stride_u_src_0, - _stride_u_src_1, f_data, h, u_dst_data, u_src_data); -} -``` - -```` - -The above is what we call a *generator script*; a Python script that, when executed, produces a pair -of source files of the same name, but with different extensions. -Generator scripts are the primary front-end pattern of *pystencils-sfg*; to learn more about them, -read the [Usage Guide](usage/generator_scripts.md). - -## CMake Integration - -*Pystencils-sfg* comes with a CMake module to register generator scripts for on-the-fly code generation. -With the module loaded, use the function `pystencilssfg_generate_target_sources` inside your `CMakeLists.txt` -to register one or multiple generator scripts; their outputs will automatically be added to the specified target. +```{toctree} +:maxdepth: 1 -```CMake -pystencilssfg_generate_target_sources( <target name> - SCRIPTS kernels.py ... - FILE_EXTENSIONS .h .cpp -) +installation +getting_started ``` -*Pystencils-sfg* makes sure that all generated files are on the project's include path. -To `#include` them, add the prefix `gen/<target name>`: +```{toctree} +:maxdepth: 1 +:caption: User Guide -```C++ -#include "gen/<target name>/kernels.h" +usage/how_to_composer +usage/api_modelling +usage/config_and_cli +usage/project_integration +usage/tips_n_tricks ``` -For details on how to add *pystencils-sfg* to your CMake project, refer to -[the project integration guide](#guide_project_integration). - -## Learn To Use pystencils-sfg - -Here is an overview of user guides for pystencils-sfg available on this site. -A basic understanding of [pystencils](https://pycodegen.pages.i10git.cs.fau.de/pystencils/index.html) -is required. - -```{card} Writing Generator Scripts -:link: guide:generator_scripts -:link-type: ref - -Learn about *generator scripts*, the primary usage idiom of *pystencils-sfg*: -Embedd *pystencils*-generated kernels into C++ source files and augment them with -arbitrary C++ glue code. -``` -```{card} CLI and Build System Integration -:link: guide_project_integration -:link-type: ref +```{toctree} +:maxdepth: 1 +:caption: API Reference -Learn how to control code generation from the command line -and how to embedd *pystencils-sfg* into your build system. +api/generation +api/composer +api/lang +api/ir +api/errors ``` -```{card} Tips and Tricks -:link: guide:tips_n_tricks -:link-type: ref - -A collection of various tricks that might come in handy when working with *pystencils-sfg*. -``` +[pycodegen]: https://pycodegen.pages.i10git.cs.fau.de +[pystencils]: https://pycodegen.pages.i10git.cs.fau.de/docs/pystencils/2.0dev diff --git a/docs/source/installation.md b/docs/source/installation.md new file mode 100644 index 0000000000000000000000000000000000000000..b60dfb462b1adecd6e7b8d474b7ce6b14737badd --- /dev/null +++ b/docs/source/installation.md @@ -0,0 +1,45 @@ +# Installation and Setup + +## Prequesites + +To use pystencils-sfg, you will need at least Python 3.10. +You will also need the appropriate compilers for building the generated code, +such as + - a modern C++ compiler (e.g. GCC, clang) + - `nvcc` for CUDA or `hipcc` for HIP + - Intel OneAPI or AdaptiveCpp for SYCL + +Furthermore, an installation of clang-format for automatic code formatting is strongly recommended. + +## Install the Latest Development Revision + +As pystencils-sfg is still unreleased, it can at this time only be obtained directly +from its Git repository. + +Create a fresh [virtual environment](https://docs.python.org/3/library/venv.html) or activate +an existing one. Install both the pystencils 2.0 and pystencils-sfg development revisions from Git: + +```{code-block} bash +pip install "git+https://i10git.cs.fau.de/pycodegen/pystencils.git@v2.0-dev" +pip install "git+https://i10git.cs.fau.de/pycodegen/pystencils-sfg.git" +``` + +````{caution} + +*pystencils-sfg* is not compatible with the *pystencils 1.3.x* releases available from PyPI; +at the moment, you will still have to manually install the latest version of pystencils 2.0. +```` + +## Check your Installation + +To verify that the SFG was successfully installed, execute the following command: + +```{code-block} bash +sfg-cli version +``` + +You should see an output like `0.1a4+...`. + +## Next Steps + +Move on to [](#getting_started_guide) for a guide on how to author simple generator scripts. diff --git a/docs/source/usage/api_modelling.md b/docs/source/usage/api_modelling.md index 60d37aee23b1b92a720e1c1d2991ff0b072f01de..cfb0e10776ae11072c13423c1ef5b78a72fd8459 100644 --- a/docs/source/usage/api_modelling.md +++ b/docs/source/usage/api_modelling.md @@ -4,7 +4,8 @@ kernelspec: name: python3 --- -# Modelling C++ APIs in pystencils-sfg +(how_to_cpp_api_modelling)= +# How To Reflect C++ APIs 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. @@ -228,3 +229,11 @@ b = lang.AugExpr("double").var("b") expr = MyClass(T1="int", T2="double").ctor(a, b) expr, lang.depends(expr), lang.includes(expr) ``` + +(field_data_structure_reflection)= +## Reflecting Field Data Structures + +:::{admonition} To Do + +Write guide on field data structure reflection +::: diff --git a/docs/source/usage/config_and_cli.md b/docs/source/usage/config_and_cli.md new file mode 100644 index 0000000000000000000000000000000000000000..1fdc9df1e7fdb3468fc74de1de3c7b4b720e8ef1 --- /dev/null +++ b/docs/source/usage/config_and_cli.md @@ -0,0 +1,116 @@ + +(how_to_generator_scripts_config)= +# Generator Script Configuration and Command-Line Interface + +There are several ways to affect the behavior and output of a generator script. +For one, the `SourceFileGenerator` itself may be configured from the combination of three +different configuration sources: + +- **Inline Configuration:** The generator script may set up an {any}`SfgConfig` object, + which is passed to the `SourceFileGenerator` at its creation; see [Inline Configuration](#inline_config) +- **Command-Line Options:** The `SourceFileGenerator` parses the command line arguments of + the generator script to set some of its configuration options; see [Command-Line Options](#cmdline_options) +- **Project Configuration:** When embedded into a larger project, using a build system such as CMake, generator scripts + may be configured globally within that project by the use of a *configuration module*. + Settings specified inside that configuration module are always overridden by the former to configuration sources. + For details on configuration modules, refer to the guide on [Project and Build System Integration](#guide_project_integration). + +(inline_config)= +## Inline Configuration + +To configure the source file generator within your generator script, import the {any}`SfgConfig` from `pystencilssfg`. +You may then set up the configuration object before passing it to the `SourceFileGenerator` constructor. +To illustrate, the following snippet alters the code indentation width and changes the output directory +of the generator script to `gen_src`: + +```{literalinclude} examples/guide_generator_scripts/inline_config/kernels.py +``` + +For a selection of common configuration options, see [below](#config_options). +The inline configuration will override any values set by the [project configuration](#config_module) +and must not conflict with any [command line arguments](#custom_cli_args). + +(config_options)= +## Configuration Options + +Here is a selection of common configuration options to be set in the [inline configuration](#inline_config) or +[project configuration](#config_module). + +### Output Options + +The file extensions of the generated files can be modified through +{any}`cfg.extensions.header <FileExtensions.header>` +and {any}`cfg.extensions.impl <FileExtensions.impl>`; +and the output directory of the code generator can be set through {any}`cfg.output_directory <SfgConfig.output_directory>`. + +:::{danger} + +When running generator scripts through [CMake](#cmake_integration), you should *never* set the file extensions +and the output directory in the inline configuration. +Both are managed by the pystencils-sfg CMake module, and setting them manually inside the script will +lead to an error. +::: + +### Outer Namespace + +To specify the outer namespace to which all generated code should be emitted, +set {any}`cfg.outer_namespace <SfgConfig.outer_namespace>`. + +### Code Style and Formatting + + - Modify the values in the {any}`cfg.code_style <CodeStyle>` category to affect + certain formatting aspects of the generated code. + - To change, enforce, or disable auto-formatting of generated code through `clang-format`, + take a look at the {any}`cfg.clang_format <ClangFormatOptions>` category. + - Clang-format will, by default, sort `#include` statements alphabetically and separate + local and system header includes. + To override this, you can set a custom sorting key for `#include` sorting via + {any}`cfg.code_style.includes_sorting_key <CodeStyle.includes_sorting_key>`. + +(cmdline_options)= +## Command-Line Options + +The `SourceFileGenerator` consumes a number of command-line parameters that may be passed to the script +on invocation. These include: + +- `--sfg-output-dir <path>`: Set the output directory of the generator script. This corresponds to {any}`SfgConfig.output_directory`. +- `--sfg-file-extensions <exts>`: Set the file extensions used for the generated files; + `exts` must be a comma-separated list not containing any spaces. Corresponds to {any}`SfgConfig.extensions`. +- `--sfg-output-mode <mode>`: Set the output mode of the generator script. Corresponds to {any}`SfgConfig.output_mode`. + +If any configuration option is set to conflicting values on the command line and in the inline configuration, +the generator script will terminate with an error. + +You may examine the full set of possible command line parameters by invoking a generator script +with the `--help` flag: + +```bash +$ python kernels.py --help +``` + +(custom_cli_args)= +## Adding Custom Command-Line Options + +Sometimes, you might want to add your own command-line options to a generator script +in order to affect its behavior from the shell, +for instance by using {any}`argparse` to set up an argument parser. +If you parse your options directly from {any}`sys.argv`, +as {any}`parse_args <argparse.ArgumentParser.parse_args>` does by default, +your parser will also receive any options meant for the `SourceFileGenerator`. +To filter these out of the argument list, +pass the additional option `keep_unknown_argv=True` to your `SourceFileGenerator`. +This will instruct it to store any unknown command line arguments into `sfg.context.argv`, +where you can then retrieve them from and pass on to your custom parser: + +```{literalinclude} examples/guide_generator_scripts/custom_cmdline_args/kernels.py +``` + +Any SFG-specific arguments will already have been filtered out of this argument list. +As a consequence of the above, if the generator script is invoked with a typo in some SFG-specific argument, +which the `SourceFileGenerator` therefore does not recognize, +that argument will be passed on to your downstream parser instead. + +:::{important} +If you do *not* pass on `sfg.context.argv` to a downstream parser, make sure that `keep_unknown_argv` is set to +`False` (which is the default), such that typos or illegal arguments will not be ignored. +::: diff --git a/docs/source/usage/generator_scripts.md b/docs/source/usage/generator_scripts.md deleted file mode 100644 index 3141feec607c7eff68394288624b67a192d84ad0..0000000000000000000000000000000000000000 --- a/docs/source/usage/generator_scripts.md +++ /dev/null @@ -1,347 +0,0 @@ -(guide:generator_scripts)= -# Generator Scripts - -Writing generator scripts is the primary usage idiom of *pystencils-sfg*. -A generator script is a Python script, say `kernels.py`, which contains *pystencils-sfg* -code at the top level that, when executed, emits source code to a pair of files `kernels.hpp` -and `kernels.cpp`. This guide describes how to write such a generator script, its structure, and how -it can be used to generate code. - -## Anatomy - -The code generation process in a generator script is controlled by the `SourceFileGenerator` context manager. -It configures the code generator by combining configuration options from the -environment (e.g. a CMake build system) with options specified in the script, -and infers the names of the output files from the script's name. -It then returns a {py:class}`composer <pystencilssfg.composer.SfgComposer>` to the user, -which provides a convenient interface for constructing the source files. - -To start, place the following code in a Python script, e.g. `kernels.py`: - -```{literalinclude} examples/guide_generator_scripts/01/kernels.py -``` - -The source file is constructed within the context manager's managed region. -During execution of the script, when the region ends, a header/source file pair -`kernels.hpp` and `kernels.cpp` will be written to disk next to your script. -Execute the script as-is and inspect the generated files, which will of course still be empty: - -``````{dropdown} Generated Files -`````{tab-set} - -````{tab-item} kernels.hpp -```{literalinclude} examples/guide_generator_scripts/01/kernels.hpp -``` -```` - -````{tab-item} kernels.cpp -```{literalinclude} examples/guide_generator_scripts/01/kernels.cpp -``` -```` -````` -`````` - -## Using the Composer - -The object `sfg` constructed in above snippet is an instance of [SfgComposer](#pystencilssfg.composer.SfgComposer). -The composer is the central part of the user front-end of *pystencils-sfg*. -It provides an interface for constructing source files that closely mimics -C++ syntactic structures within Python. - -::::{dropdown} Composer API Overview -```{eval-rst} -.. currentmodule:: pystencilssfg.composer -``` - -Structure and Verbatim Code: - -```{eval-rst} - -.. autosummary:: - :nosignatures: - - SfgBasicComposer.prelude - SfgBasicComposer.include - SfgBasicComposer.namespace - SfgBasicComposer.code -``` - -Kernels and Kernel Namespaces: - -```{eval-rst} - -.. autosummary:: - :nosignatures: - - SfgBasicComposer.kernels - SfgBasicComposer.kernel_namespace - SfgBasicComposer.kernel_function -``` - -Function definition, parameters, and header inclusion: - -```{eval-rst} - -.. autosummary:: - :nosignatures: - - SfgBasicComposer.function - SfgBasicComposer.params - SfgBasicComposer.require -``` - -Variables, expressions, and variable initialization: - -```{eval-rst} - -.. autosummary:: - :nosignatures: - - SfgBasicComposer.var - SfgBasicComposer.vars - SfgBasicComposer.expr - SfgBasicComposer.init - - SfgBasicComposer.map_field - SfgBasicComposer.set_param -``` - -Parameter mappings: - -```{eval-rst} - -.. autosummary:: - :nosignatures: - - SfgBasicComposer.set_param - SfgBasicComposer.map_field - SfgBasicComposer.map_vector -``` - -Control Flow: - -```{eval-rst} - -.. autosummary:: - :nosignatures: - - SfgBasicComposer.branch - SfgBasicComposer.switch -``` - -Kernel Invocation: - -```{eval-rst} - -.. autosummary:: - :nosignatures: - - SfgBasicComposer.call - SfgBasicComposer.cuda_invoke -``` -:::: - -### Includes and Definitions - -With {any}`include <SfgBasicComposer.include>`, the code generator can be instructed to include header files. -As in C++, you can use the `<>` delimiters for system headers, and omit them for project headers. - -`````{tab-set} - -````{tab-item} kernels.py -```{literalinclude} examples/guide_generator_scripts/02/kernels.py -``` -```` - -````{tab-item} kernels.hpp -```{literalinclude} examples/guide_generator_scripts/02/kernels.hpp -``` -```` - -````{tab-item} kernels.cpp -```{literalinclude} examples/guide_generator_scripts/02/kernels.cpp -``` -```` -````` - -### Adding Kernels - -[pystencils](https://pycodegen.pages.i10git.cs.fau.de/pystencils/)-generated kernels are managed in *kernel namespaces*. -The default kernel namespace is called `kernels` and is available via -[`sfg.kernels`](#pystencilssfg.composer.SfgBasicComposer.kernels). -Adding an existing *pystencils* AST, or creating one from a list of assignments, is possible through -[`kernels.add`](#pystencilssfg.ir.SfgKernelNamespace.add) -and -[`kernels.create`](#pystencilssfg.ir.SfgKernelNamespace.create). -The latter is a wrapper around -[`pystencils.create_kernel`]( -https://pycodegen.pages.i10git.cs.fau.de/pystencils/sphinx/kernel_compile_and_call.html#pystencils.create_kernel -). -Both functions return a [kernel handle](#pystencilssfg.ir.SfgKernelHandle) -through which the kernel can be accessed, e.g. for calling it in a function. - -To access other kernel namespaces than the default one, -the [`sfg.kernel_namespace`](#pystencilssfg.composer.SfgBasicComposer.kernel_namespace) method can be used. - -`````{tab-set} - -````{tab-item} kernels.py -```{literalinclude} examples/guide_generator_scripts/03/kernels.py -``` -```` - -````{tab-item} kernels.hpp -```{literalinclude} examples/guide_generator_scripts/03/kernels.hpp -``` -```` - -````{tab-item} kernels.cpp -```{literalinclude} examples/guide_generator_scripts/03/kernels.cpp -``` -```` -````` - -### Building Functions - -Through the composer, you can define free functions in your generated C++ file. -These may contain arbitrary code; -their primary intended task however is to wrap kernel calls with the necessary boilerplate code -to integrate them into a framework. -The composer provides an interface for constructing functions that tries to mimic the look of the generated C++ code. -Use `sfg.function` to create a function, and `sfg.call` to call a kernel: - -`````{tab-set} - -````{tab-item} kernels.py -```{literalinclude} examples/guide_generator_scripts/04/kernels.py -:start-after: start -:end-before: end -``` -```` - -````{tab-item} kernels.hpp -```{literalinclude} examples/guide_generator_scripts/04/kernels.hpp -``` -```` - -````{tab-item} kernels.cpp -```{literalinclude} examples/guide_generator_scripts/04/kernels.cpp -``` -```` -````` - -Note the special syntax: To mimic the look of a C++ function, the composer uses a sequence of two calls -to construct the function. - -The function body can furthermore be populated with code to embedd the generated kernel into -the target C++ application. -If you examine the generated files of the previous example, you will notice that your -function `scale_kernel` has lots of raw pointers and integer indices in its interface. -We can wrap those up into proper C++ data structures, -such as, for example, `std::span` or `std::vector`, like this: - -`````{tab-set} - -````{tab-item} kernels.py -```{literalinclude} examples/guide_generator_scripts/05/kernels.py -:start-after: start -:end-before: end -``` -```` - -````{tab-item} kernels.hpp -```{literalinclude} examples/guide_generator_scripts/05/kernels.hpp -``` -```` - -````{tab-item} kernels.cpp -```{literalinclude} examples/guide_generator_scripts/05/kernels.cpp -``` -```` -````` - -If you now inspect the generated code, you will see that the interface of your function is -considerably simplified. -Also, all the necessary code was added to its body to extract the low-level information required -by the actual kernel from the data structures. - -The `sfg.map_field` API can be used to map pystencils fields to a variety of different data structures. -The pystencils-sfg provides modelling support for a number of C++ standard library classes -(see {any}`pystencilssfg.lang.cpp.std`). -It also provides the necessary infrastructure for modelling the data structures of any C++ framework -in a similar manner. - - -## Configuration and Invocation - -There are several ways to affect the behavior and output of a generator script. -For one, the `SourceFileGenerator` itself may be configured from the combination of three -different configuration sources: - -- **Inline Configuration:** The generator script may set up an {any}`SfgConfig` object, - which is passed to the `SourceFileGenerator` at its creation; see [Inline Configuration](#inline_config) -- **Command-Line Options:** The `SourceFileGenerator` parses the command line arguments of - the generator script to set some of its configuration options; see [Command-Line Options](#cmdline_options) -- **Project Configuration:** When embedded into a larger project, using a build system such as CMake, generator scripts - may be configured globally within that project by the use of a *configuration module*. - Settings specified inside that configuration module are always overridden by the former to configuration sources. - For details on configuration modules, refer to the guide on [Project and Build System Integration](#guide_project_integration). - -(inline_config)= -### Inline Configuration - -To configure the source file generator within your generator script, import the {any}`SfgConfig` from `pystencilssfg`. -You may then set up the configuration object before passing it to the `SourceFileGenerator` constructor. -To illustrate, the following snippet alters the code indentation width and changes the output directory -of the generator script to `gen_src`: - -```{literalinclude} examples/guide_generator_scripts/inline_config/kernels.py -``` - -(cmdline_options)= -### Command-Line Options - -The `SourceFileGenerator` consumes a number of command-line parameters that may be passed to the script -on invocation. These include: - -- `--sfg-output-dir <path>`: Set the output directory of the generator script. This corresponds to {any}`SfgConfig.output_directory`. -- `--sfg-file-extensions <exts>`: Set the file extensions used for the generated files; - `exts` must be a comma-separated list not containing any spaces. Corresponds to {any}`SfgConfig.extensions`. -- `--sfg-output-mode <mode>`: Set the output mode of the generator script. Corresponds to {any}`SfgConfig.output_mode`. - -If any configuration option is set to conflicting values on the command line and in the inline configuration, -the generator script will terminate with an error. - -You may examine the full set of possible command line parameters by invoking a generator script -with the `--help` flag: - -```bash -$ python kernels.py --help -``` - -(custom_cli_args)= -## Adding Custom Command-Line Options - -Sometimes, you might want to add your own command-line options to a generator script -in order to affect its behavior from the shell, -for instance by using {any}`argparse` to set up an argument parser. -If you parse your options directly from {any}`sys.argv`, -as {any}`parse_args <argparse.ArgumentParser.parse_args>` does by default, -your parser will also receive any options meant for the `SourceFileGenerator`. -To filter these out of the argument list, -pass the additional option `keep_unknown_argv=True` to your `SourceFileGenerator`. -This will instruct it to store any unknown command line arguments into `sfg.context.argv`, -where you can then retrieve them from and pass on to your custom parser: - -```{literalinclude} examples/guide_generator_scripts/custom_cmdline_args/kernels.py -``` - -Any SFG-specific arguments will already have been filtered out of this argument list. -As a consequence of the above, if the generator script is invoked with a typo in some SFG-specific argument, -which the `SourceFileGenerator` therefore does not recognize, -that argument will be passed on to your downstream parser instead. - -:::{important} -If you do *not* pass on `sfg.context.argv` to a downstream parser, make sure that `keep_unknown_argv` is set to -`False` (which is the default), such that typos or illegal arguments will not be ignored. -::: diff --git a/docs/source/usage/how_to_composer.md b/docs/source/usage/how_to_composer.md new file mode 100644 index 0000000000000000000000000000000000000000..7f08829605abad35f5603b502e2cc454df90124a --- /dev/null +++ b/docs/source/usage/how_to_composer.md @@ -0,0 +1,409 @@ +--- +file_format: mystnb +kernelspec: + name: python3 +--- + +(composer_guide)= +# How To Use the Composer + +```{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) + ) +``` + +(how_to_namespaces)= +## Namespaces + +C++ uses namespaces to structure code and group entities. +By default, pystencils-sfg emits all code into the global namespace. +For instructions on how to change the outermost namespace used by the `SourceFileGenerator`, +see [](#how_to_generator_scripts_config). + +Starting from the outermost namespace, nested namespaces can be entered and exited during +code generation. +To enter a new namespace, use `sfg.namespace` in one of two ways: + + - Simply calling `sfg.namespace("my_namespace")` and ignoring its return value will cause the + generator script to use the given namespace for the rest of its execution; + - Calling `sfg.namespace("my_namespace")` in a `with` statement will activate the given namespace + only for the duration of the managed block. + +To illustrate, the following snippet activates the namespace `mylib::generated` for the entire +length of the generator script, and then enters and exits the nested namespace `mylib::generated::detail`: + +```{code-cell} ipython3 +with SourceFileGenerator() as sfg: + sfg.namespace("mylib::generated") + + sfg.code("/* Generated code in outer namespace */") + + with sfg.namespace("detail"): + sfg.code("/* Implementation details in the inner namespace */") + + sfg.code("/* More code in the outer namespace */") +``` + +## Kernels and Parameter Mappings + +The original purpose of pystencils-sfg is to simplify the embedding of *pystencils*-generated +numerical kernels into C++ applications. +This section discusses how to register kernels with the source file generator, +how to call them in wrapper code, +and how to automatically map symbolic pystencils fields onto nd-array data structures. + +### Registering Kernels + +In the generated files, kernels are organized in *kernel namespaces*. +The composer gives us access to the default kernel namespace (`<current_namespace>::kernels`) +via `sfg.kernels`. + +To add a kernel, + - either pass its assignments and the pystencils code generator configuration directly to {any}`kernels.reate() <KernelsAdder.create>`, + - or create the kernel separately through {any}`pystencils.create_kernel <pystencils.codegen.create_kernel>` and register it using + {any}`kernels.add() <KernelsAdder.add>`. + +Both functions return a kernel handle, through which the kernel may later be invoked. + +You may create and access custom-named kernel namespaces using {any}`sfg.kernel_namespace() <SfgBasicComposer.kernel_namespace>`. +This gives you a {any}`KernelsAdder` object with the same interface as `sfg.kernels`. + +:::{note} + +A kernel namespace is not a regular namespace; if you attempt to create both a regular and a kernel namespace with the same name, +the composer will raise an error. +::: + +Here's an example with two kernels being registered in different kernel namespace, +once using `add`, and once using `create`. + +```{code-cell} ipython3 +import pystencils as ps + +with SourceFileGenerator() as sfg: + # Create symbolic fields + f, g = ps.fields("f, g: double[2D]") + + # Define and create the first kernel + asm1 = ps.Assignment(f(0), g(0)) + cfg1 = ps.CreateKernelConfig() + cfg1.cpu.openmp.enable = True + khandle_1 = sfg.kernels.create(asm1, "first_kernel", cfg1) + + # Define the second kernel and its codegen configuration + asm2 = ps.Assignment(f(0), 3.0 * g(0)) + cfg2 = ps.CreateKernelConfig(target=ps.Target.CUDA) + + # Create and register the second kernel at a custom namespace + kernel2 = ps.create_kernel(asm2, cfg2) + khandle_2 = sfg.kernel_namespace("gpu_kernels").add(kernel2, "second_kernel") +``` + +### Writing Kernel Wrapper Functions + +By default, kernel definitions are only visible in the generated implementation file; +kernels are supposed to not be called directly, but through wrapper functions. +This serves to hide their fairly lenghty and complicated low-level function interface. + +#### Invoking CPU Kernels + +To call a CPU kernel from a function, use `sfg.call` on a kernel handle: + +```{code-block} python +sfg.function("kernel_wrapper")( + sfg.call(khandle) +) +``` + +This will expose all parameters of the kernel into the wrapper function and, in turn, +cause them to be added to its signature. +We don't want to expose this complexity, but instead hide it by using appropriate data structures. +The next section explains how that is achieved in pystencils-sfg. + +#### Mapping Fields to Data Structures + +Pystencils kernels operate on n-dimensional contiguous or strided arrays, +There exist many classes with diverse APIs modelling such arrays throughout the scientific +computing landscape, including [Kokkos Views][kokkos_view], [C++ std::mdspan][mdspan], +[SYCL buffers][sycl_buffer], and many framework-specific custom-built classes. +Using the protocols behind {any}`sfg.map_field <SfgBasicComposer.map_field>`, +it is possible to automatically emit code +that extracts the indexing information required by a kernel from any of these classes +- provided a suitable API reflection is available. + +:::{seealso} +[](#field_data_structure_reflection) for instructions on how to set up field API +reflection for a custom nd-array data structure. +::: + +Pystencils-sfg natively provides field extraction for a number of C++ STL-classes, +such as `std::vector` and `std::span` (for 1D fields) and `std::mdspan`. +Import any of them from `pystencilssfg.lang.cpp.std` and create an instance for a given +field using `.from_field()`. +Then, inside the wrapper function, pass the symbolic field and its associated data structure to +{any}`sfg.map_field <SfgBasicComposer.map_field>`. +before calling the kernel: + +```{code-cell} ipython3 +import pystencils as ps +from pystencilssfg.lang.cpp import std + +with SourceFileGenerator() as sfg: + # Create symbolic fields + f, g = ps.fields("f, g: double[1D]") + + # Create data structure reflections + f_vec = std.vector.from_field(f) + g_span = std.span.from_field(g) + + # Create the kernel + asm = ps.Assignment(f(0), g(0)) + khandle = sfg.kernels.create(asm, "my_kernel") + + # Create the wrapper function + sfg.function("call_my_kernel")( + sfg.map_field(f, f_vec), + sfg.map_field(g, g_span), + sfg.call(khandle) + ) +``` + +(exposed_inline_kernels)= +### Exposed and Inline Kernels + +:::{admonition} To Do + + - Creating and calling kernels + - Invoking GPU kernels and the CUDA API Mirror + - Defining classes, their fields constructors, and methods + +::: + + +[kokkos_view]: https://kokkos.org/kokkos-core-wiki/ProgrammingGuide/View.html +[mdspan]: https://en.cppreference.com/w/cpp/container/mdspan +[sycl_buffer]: https://registry.khronos.org/SYCL/specs/sycl-2020/html/sycl-2020.html#subsec:buffers diff --git a/noxfile.py b/noxfile.py index a725b7bafcb43caee4f90e60bc509c1413a061a9..e4f8b95c3055be131dffa614bc0a23672700588f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -13,11 +13,7 @@ def add_pystencils_git(session: nox.Session): pystencils_dir = cache_dir / "pystencils" if pystencils_dir.exists(): with session.chdir(pystencils_dir): - session.run_install( - "git", - "pull", - external=True - ) + session.run_install("git", "pull", external=True) else: session.run_install( "git", @@ -76,7 +72,18 @@ def testsuite(session: nox.Session): def docs(session: nox.Session): """Build the documentation pages""" editable_install(session, ["docs"]) + + env = {} + + session_args = session.posargs + if "--fail-on-warnings" in session_args: + env["SPHINXOPTS"] = "-W --keep-going" + session.chdir("docs") + + if "--clean" in session_args: + session.run("make", "clean", external=True) + session.run("make", "html", external=True) diff --git a/src/pystencilssfg/composer/basic_composer.py b/src/pystencilssfg/composer/basic_composer.py index e75f0e29e4d41825a8d0d8cf1547bab046c1731a..49b6c73e8bd06b0d5fb44402b8af285362072b29 100644 --- a/src/pystencilssfg/composer/basic_composer.py +++ b/src/pystencilssfg/composer/basic_composer.py @@ -1,15 +1,22 @@ from __future__ import annotations + from typing import Sequence, TypeAlias from abc import ABC, abstractmethod import sympy as sp from functools import reduce from warnings import warn -from pystencils import Field, CreateKernelConfig, create_kernel +from pystencils import ( + Field, + CreateKernelConfig, + create_kernel, + Assignment, + AssignmentCollection, +) 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, @@ -79,6 +86,8 @@ SequencerArg: TypeAlias = tuple | ExprLike | SfgCallTreeNode | SfgNodeBuilder class KernelsAdder: + """Handle on a kernel namespace that permits registering kernels.""" + def __init__(self, ctx: SfgContext, loc: SfgNamespaceBlock): self._ctx = ctx self._loc = loc @@ -115,12 +124,12 @@ class KernelsAdder: def create( self, - assignments, + assignments: Assignment | Sequence[Assignment] | AssignmentCollection, name: str | None = None, config: CreateKernelConfig | None = None, ): """Creates a new pystencils kernel from a list of assignments and a configuration. - This is a wrapper around `pystencils.create_kernel` + This is a wrapper around `create_kernel <pystencils.codegen.create_kernel>` with a subsequent call to `add`. """ if config is None: @@ -135,7 +144,6 @@ class KernelsAdder: config.function_name = name - # type: ignore kernel = create_kernel(assignments, config=config) return self.add(kernel) @@ -171,7 +179,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 +196,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 @@ -257,10 +271,10 @@ class SfgBasicComposer(SfgIComposer): return self.kernel_namespace("kernels") def kernel_namespace(self, name: str) -> KernelsAdder: - """Return the kernel namespace of the given name, creating it if it does not exist yet.""" - kns = self._cursor.get_entity("kernels") + """Return a view on a kernel namespace in order to add kernels to it.""" + kns = self._cursor.get_entity(name) if kns is None: - kns = SfgKernelNamespace("kernels", self._cursor.current_namespace) + kns = SfgKernelNamespace(name, self._cursor.current_namespace) self._cursor.add_entity(kns) elif not isinstance(kns, SfgKernelNamespace): raise ValueError( @@ -318,10 +332,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 +346,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 - - 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) + seq.returns(return_type) - 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. @@ -508,7 +504,7 @@ class SfgBasicComposer(SfgIComposer): Args: switch_arg: Argument to the `switch()` statement - autobreak: Whether to automatically print a `break;` at the end of each case block + autobreak: Whether to automatically print a ``break;`` at the end of each case block """ return SfgSwitchBuilder(switch_arg, autobreak=autobreak) @@ -523,7 +519,7 @@ class SfgBasicComposer(SfgIComposer): Args: field: The pystencils field to be mapped - src_object: A `IFieldIndexingProvider` object representing a field data structure. + index_provider: An expression representing a field, or a field extraction provider instance cast_indexing_symbols: Whether to always introduce explicit casts for indexing symbols """ return SfgDeferredFieldMapping( @@ -561,7 +557,8 @@ def make_sequence(*args: SequencerArg) -> SfgSequence: """Construct a sequence of C++ code from various kinds of arguments. `make_sequence` is ubiquitous throughout the function building front-end; - among others, it powers the syntax of `SfgComposer.function` and `SfgComposer.branch`. + among others, it powers the syntax of `SfgBasicComposer.function` + and `SfgBasicComposer.branch`. `make_sequence` constructs an abstract syntax tree for code within a function body, accepting various types of arguments which then get turned into C++ code. These are @@ -621,6 +618,94 @@ def make_sequence(*args: SequencerArg) -> SfgSequence: return SfgSequence(children) +class SfgFunctionSequencerBase: + """Common base class for function and method sequencers. + + This builder uses call sequencing to specify the function or method'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 + self._params: list[SfgVar] | None = None + + # Qualifiers + self._inline: bool = False + self._constexpr: bool = False + + # Attributes + self._attributes: list[str] = [] + + def returns(self, rtype: UserTypeSpec): + """Set the return type of the function""" + self._return_type = create_type(rtype) + return self + + def params(self, *args: VarLike): + """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): + """Mark this function as ``inline``.""" + self._inline = True + return self + + def constexpr(self): + """Mark this function as ``constexpr``.""" + self._constexpr = True + return self + + def attr(self, *attrs: str): + """Add attributes to this function""" + self._attributes += attrs + return self + + +class SfgFunctionSequencer(SfgFunctionSequencerBase): + """Sequencer for constructing functions.""" + + 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, + required_params=self._params, + ) + 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/composer/class_composer.py b/src/pystencilssfg/composer/class_composer.py index 0a72e8089ecd5e32be53cd335df57b58b21ec578..7787150c5bffd4e7332aecd2ff7a4a66cac0adc6 100644 --- a/src/pystencilssfg/composer/class_composer.py +++ b/src/pystencilssfg/composer/class_composer.py @@ -3,9 +3,9 @@ from typing import Sequence from itertools import takewhile, dropwhile import numpy as np -from pystencils.types import PsCustomType, UserTypeSpec, create_type +from pystencils.types import create_type -from ..context import SfgContext +from ..context import SfgContext, SfgCursor from ..lang import ( VarLike, ExprLike, @@ -32,9 +32,69 @@ from .mixin import SfgComposerMixIn from .basic_composer import ( make_sequence, SequencerArg, + SfgFunctionSequencerBase, ) +class SfgMethodSequencer(SfgFunctionSequencerBase): + def __init__(self, cursor: SfgCursor, name: str) -> None: + super().__init__(cursor, name) + + self._const: bool = False + self._static: bool = False + self._virtual: bool = False + self._override: bool = False + + self._tree: SfgCallTreeNode + + def const(self): + """Mark this method as ``const``.""" + self._const = True + return self + + def static(self): + """Mark this method as ``static``.""" + self._static = True + return self + + def virtual(self): + """Mark this method as ``virtual``.""" + self._virtual = True + return self + + def override(self): + """Mark this method as ``override``.""" + self._override = True + return self + + def __call__(self, *args: SequencerArg): + self._tree = make_sequence(*args) + return self + + def _resolve(self, ctx: SfgContext, cls: SfgClass, vis_block: SfgVisibilityBlock): + method = SfgMethod( + self._name, + cls, + self._tree, + return_type=self._return_type, + inline=self._inline, + const=self._const, + static=self._static, + constexpr=self._constexpr, + virtual=self._virtual, + override=self._override, + attributes=self._attributes, + required_params=self._params, + ) + cls.add_member(method, vis_block.visibility) + + if self._inline: + vis_block.elements.append(SfgEntityDef(method)) + else: + vis_block.elements.append(SfgEntityDecl(method)) + ctx._cursor.write_impl(SfgEntityDef(method)) + + class SfgClassComposer(SfgComposerMixIn): """Composer for classes and structs. @@ -53,7 +113,7 @@ class SfgClassComposer(SfgComposerMixIn): def __init__(self, visibility: SfgVisibility): self._visibility = visibility self._args: tuple[ - SfgClassComposer.MethodSequencer + SfgMethodSequencer | SfgClassComposer.ConstructorBuilder | VarLike | str, @@ -63,10 +123,7 @@ class SfgClassComposer(SfgComposerMixIn): def __call__( self, *args: ( - SfgClassComposer.MethodSequencer - | SfgClassComposer.ConstructorBuilder - | VarLike - | str + SfgMethodSequencer | SfgClassComposer.ConstructorBuilder | VarLike | str ), ): self._args = args @@ -76,10 +133,7 @@ class SfgClassComposer(SfgComposerMixIn): vis_block = SfgVisibilityBlock(self._visibility) for arg in self._args: match arg: - case ( - SfgClassComposer.MethodSequencer() - | SfgClassComposer.ConstructorBuilder() - ): + case SfgMethodSequencer() | SfgClassComposer.ConstructorBuilder(): arg._resolve(ctx, cls, vis_block) case str(): vis_block.elements.append(arg) @@ -90,43 +144,6 @@ class SfgClassComposer(SfgComposerMixIn): vis_block.elements.append(SfgEntityDef(member_var)) return vis_block - class MethodSequencer: - def __init__( - self, - name: str, - returns: UserTypeSpec = PsCustomType("void"), - inline: bool = False, - const: bool = False, - ) -> None: - self._name = name - self._returns = create_type(returns) - self._inline = inline - self._const = const - self._tree: SfgCallTreeNode - - def __call__(self, *args: SequencerArg): - self._tree = make_sequence(*args) - return self - - def _resolve( - self, ctx: SfgContext, cls: SfgClass, vis_block: SfgVisibilityBlock - ): - method = SfgMethod( - self._name, - cls, - self._tree, - return_type=self._returns, - inline=self._inline, - const=self._const, - ) - cls.add_member(method, vis_block.visibility) - - if self._inline: - vis_block.elements.append(SfgEntityDef(method)) - else: - vis_block.elements.append(SfgEntityDecl(method)) - ctx._cursor.write_impl(SfgEntityDef(method)) - class ConstructorBuilder: """Composer syntax for constructor building. @@ -197,9 +214,7 @@ class SfgClassComposer(SfgComposerMixIn): """ return self._class(class_name, SfgClassKeyword.STRUCT, bases) - def numpy_struct( - self, name: str, dtype: np.dtype, add_constructor: bool = True - ): + def numpy_struct(self, name: str, dtype: np.dtype, add_constructor: bool = True): """Add a numpy structured data type as a C++ struct Returns: @@ -230,24 +245,15 @@ class SfgClassComposer(SfgComposerMixIn): """ return SfgClassComposer.ConstructorBuilder(*params) - def method( - self, - name: str, - returns: UserTypeSpec = PsCustomType("void"), - inline: bool = False, - const: bool = False, - ): + def method(self, name: str): """In a class or struct body or visibility block, add a method. The usage is similar to :any:`SfgBasicComposer.function`. Args: name: The method name - returns: The method's return type - inline: Whether or not the method should be defined in-line. - const: Whether or not the method is const-qualified. """ - return SfgClassComposer.MethodSequencer(name, returns, inline, const) + return SfgMethodSequencer(self._cursor, name) # INTERNALS @@ -270,7 +276,7 @@ class SfgClassComposer(SfgComposerMixIn): def sequencer( *args: ( SfgClassComposer.VisibilityBlockSequencer - | SfgClassComposer.MethodSequencer + | SfgMethodSequencer | SfgClassComposer.ConstructorBuilder | VarLike | str diff --git a/src/pystencilssfg/composer/custom.py b/src/pystencilssfg/composer/custom.py index 7df364c6cd78c1a56d68283f3c617092938a4dcf..10644c6aade9115572096d15622107734eeb8f2b 100644 --- a/src/pystencilssfg/composer/custom.py +++ b/src/pystencilssfg/composer/custom.py @@ -8,7 +8,7 @@ if TYPE_CHECKING: class CustomGenerator(ABC): """Abstract base class for custom code generators that may be passed to - `SfgComposer.generate`.""" + `SfgBasicComposer.generate`.""" @abstractmethod def generate(self, sfg: SfgComposer) -> None: ... diff --git a/src/pystencilssfg/config.py b/src/pystencilssfg/config.py index 3b63f4f121d27b53a3973a93996a6d0a99fe09e6..bbe2389946c4b426dcbd14c5e3e4205b81cef976 100644 --- a/src/pystencilssfg/config.py +++ b/src/pystencilssfg/config.py @@ -52,7 +52,7 @@ class OutputMode(Enum): """Generate only a header file. At the moment, header-only mode does not support generation of kernels and requires that all functions - and methods are marked `inline`. + and methods are marked ``inline``. """ @@ -64,7 +64,7 @@ class CodeStyle(ConfigBase): """The number of spaces successively nested blocks should be indented with""" includes_sorting_key: BasicOption[Callable[[HeaderFile], Any]] = BasicOption() - """Key function that will be used to sort `#include` statements in generated files. + """Key function that will be used to sort ``#include`` statements in generated files. Pystencils-sfg will instruct clang-tidy to forego include sorting if this option is set. """ diff --git a/src/pystencilssfg/emission/emitter.py b/src/pystencilssfg/emission/emitter.py index c1b6e9c79e09bf1d5604dbd6fca9304190e85272..909ae168538fbec727ab8de2023b94c772480946 100644 --- a/src/pystencilssfg/emission/emitter.py +++ b/src/pystencilssfg/emission/emitter.py @@ -13,15 +13,15 @@ class SfgCodeEmitter: def __init__( self, output_directory: Path, - code_style: CodeStyle, - clang_format: ClangFormatOptions, + code_style: CodeStyle = CodeStyle(), + clang_format: ClangFormatOptions = ClangFormatOptions(), ): self._output_dir = output_directory self._code_style = code_style self._clang_format_opts = clang_format self._printer = SfgFilePrinter(code_style) - def emit(self, file: SfgSourceFile): + def dumps(self, file: SfgSourceFile) -> str: code = self._printer(file) if self._code_style.get_option("includes_sorting_key") is not None: @@ -33,6 +33,10 @@ class SfgCodeEmitter: code, self._clang_format_opts, sort_includes=sort_includes ) + return code + + def emit(self, file: SfgSourceFile): + code = self.dumps(file) self._output_dir.mkdir(parents=True, exist_ok=True) fpath = self._output_dir / file.name fpath.write_text(code) diff --git a/src/pystencilssfg/emission/file_printer.py b/src/pystencilssfg/emission/file_printer.py index 6ab98eb89aba143dd567fc0c176199ff9d1444fe..765bf70550504fd499746504236f1adaa224664a 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,22 @@ class SfgFilePrinter: def _func_signature(self, func: SfgFunction | SfgMethod, inclass: bool): code = "" - if func.inline: + + if func.attributes: + code += "[[" + ", ".join(func.attributes) + "]]" + + if func.inline and not inclass: code += "inline " + + if isinstance(func, SfgMethod) and inclass: + if func.static: + code += "static " + if func.virtual: + code += "virtual " + + 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 @@ -187,7 +203,10 @@ class SfgFilePrinter: code += f"{func.owning_class.name}::" code += f"{func.name}({params_str})" - if isinstance(func, SfgMethod) and func.const: - code += " const" + if isinstance(func, SfgMethod): + if func.const: + code += " const" + if func.override and inclass: + code += " override" return code diff --git a/src/pystencilssfg/generator.py b/src/pystencilssfg/generator.py index f3f67a02f4da7a44ae324ddd41f5585645cadb58..91a124a8439c06dcb22853975cf08a5e9526ecde 100644 --- a/src/pystencilssfg/generator.py +++ b/src/pystencilssfg/generator.py @@ -18,8 +18,8 @@ class SourceFileGenerator: """Context manager that controls the code generation process in generator scripts. The `SourceFileGenerator` must be used as a context manager by calling it within - a ``with`` statement in the top-level code of a generator script (see :ref:`guide:generator_scripts`). - Upon entry to its context, it creates an :class:`SfgComposer` which can be used to populate the generated files. + a ``with`` statement in the top-level code of a generator script. + Upon entry to its context, it creates an `SfgComposer` which can be used to populate the generated files. When the managed region finishes, the code files are generated and written to disk at the locations defined by the configuration. Existing copies of the target files are deleted on entry to the managed region, @@ -29,17 +29,10 @@ class SourceFileGenerator: sfg_config: Inline configuration for the code generator keep_unknown_argv: If `True`, any command line arguments given to the generator script that the `SourceFileGenerator` does not understand are stored in - `sfg.context.argv`. + `sfg.context.argv <SfgContext.argv>`. """ - def __init__( - self, - sfg_config: SfgConfig | None = None, - keep_unknown_argv: bool = False, - ): - if sfg_config and not isinstance(sfg_config, SfgConfig): - raise TypeError("sfg_config is not an SfgConfiguration.") - + def _scriptname(self) -> str: import __main__ if not hasattr(__main__, "__file__"): @@ -50,7 +43,17 @@ class SourceFileGenerator: ) scriptpath = Path(__main__.__file__) - scriptname = scriptpath.name + return scriptpath.name + + def __init__( + self, + sfg_config: SfgConfig | None = None, + keep_unknown_argv: bool = False, + ): + if sfg_config and not isinstance(sfg_config, SfgConfig): + raise TypeError("sfg_config is not an SfgConfiguration.") + + scriptname = self._scriptname() basename = scriptname.rsplit(".")[0] from argparse import ArgumentParser @@ -146,32 +149,35 @@ class SourceFileGenerator: if impl_path.exists(): impl_path.unlink() + def _finish_files(self) -> None: + if self._output_mode == OutputMode.INLINE: + assert self._impl_file is not None + self._header_file.elements.append(f'#include "{self._impl_file.name}"') + + from .ir import collect_includes + + header_includes = collect_includes(self._header_file) + self._header_file.includes = list( + set(self._header_file.includes) | header_includes + ) + self._header_file.includes.sort(key=self._include_sort_key) + + if self._impl_file is not None: + impl_includes = collect_includes(self._impl_file) + # If some header is already included by the generated header file, do not duplicate that inclusion + impl_includes -= header_includes + self._impl_file.includes = list( + set(self._impl_file.includes) | impl_includes + ) + self._impl_file.includes.sort(key=self._include_sort_key) + def __enter__(self) -> SfgComposer: self.clean_files() return SfgComposer(self._context) def __exit__(self, exc_type, exc_value, traceback): if exc_type is None: - if self._output_mode == OutputMode.INLINE: - assert self._impl_file is not None - self._header_file.elements.append(f'#include "{self._impl_file.name}"') - - from .ir import collect_includes - - header_includes = collect_includes(self._header_file) - self._header_file.includes = list( - set(self._header_file.includes) | header_includes - ) - self._header_file.includes.sort(key=self._include_sort_key) - - if self._impl_file is not None: - impl_includes = collect_includes(self._impl_file) - # If some header is already included by the generated header file, do not duplicate that inclusion - impl_includes -= header_includes - self._impl_file.includes = list( - set(self._impl_file.includes) | impl_includes - ) - self._impl_file.includes.sort(key=self._include_sort_key) + self._finish_files() self._emitter.emit(self._header_file) if self._impl_file is not None: diff --git a/src/pystencilssfg/ir/call_tree.py b/src/pystencilssfg/ir/call_tree.py index 4cee2f526c3a26e34f6b122a3dd1d8a15dd11563..24a315d5a0ae0319cbc1b906f98deacc72828176 100644 --- a/src/pystencilssfg/ir/call_tree.py +++ b/src/pystencilssfg/ir/call_tree.py @@ -18,13 +18,6 @@ class SfgCallTreeNode(ABC): For extensibility, code printing is implemented inside the call tree. Therefore, every instantiable call tree node must implement the method `get_code`. By convention, the string returned by `get_code` should not contain a trailing newline. - - ## Branching Structure - - The branching structure of the call tree is managed uniformly through the `children` interface - of SfgCallTreeNode. Each subclass must ensure that access to and modification of - the branching structure through the `children` property and the `child` and `set_child` - methods is possible, if necessary by overriding the property and methods. """ def __init__(self) -> None: self._includes: set[HeaderFile] = set() @@ -50,7 +43,7 @@ class SfgCallTreeNode(ABC): class SfgCallTreeLeaf(SfgCallTreeNode, ABC): """A leaf node of the call tree. - Leaf nodes must implement `required_parameters` for automatic parameter collection. + Leaf nodes must implement ``depends`` for automatic parameter collection. """ def __init__(self): @@ -69,7 +62,7 @@ class SfgCallTreeLeaf(SfgCallTreeNode, ABC): class SfgEmptyNode(SfgCallTreeLeaf): """A leaf node that does not emit any code. - Empty nodes must still implement `required_parameters`. + Empty nodes must still implement ``depends``. """ def __init__(self): @@ -304,6 +297,8 @@ class SfgBranch(SfgCallTreeNode): class SfgSwitchCase(SfgCallTreeNode): DefaultCaseType = NewType("DefaultCaseType", object) + """Sentinel type representing the ``default`` case.""" + Default = DefaultCaseType(object()) def __init__(self, label: str | SfgSwitchCase.DefaultCaseType, body: SfgSequence): diff --git a/src/pystencilssfg/ir/entities.py b/src/pystencilssfg/ir/entities.py index 62ae1eb7611065c7b1a12b77d6894143aed341dd..40abb148d90eb35300afd0af4e25d2f8df1c091e 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 ( @@ -61,7 +62,7 @@ class SfgCodeEntity: class SfgNamespace(SfgCodeEntity): """A C++ namespace. - Each namespace has a `name` and a `parent`; its fully qualified name is given as + Each namespace has a name and a parent; its fully qualified name is given as ``<parent.name>::<name>``. Args: @@ -77,7 +78,7 @@ class SfgNamespace(SfgCodeEntity): def get_entity(self, qual_name: str) -> SfgCodeEntity | None: """Find an entity with the given qualified name within this namespace. - If `qual_name` contains any qualifying delimiters ``::``, + If ``qual_name`` contains any qualifying delimiters ``::``, each component but the last is interpreted as a namespace. """ tokens = qual_name.split("::", 1) @@ -203,10 +204,40 @@ class SfgKernelNamespace(SfgNamespace): self._kernels[kernel.name] = kernel -class SfgFunction(SfgCodeEntity): +@dataclass(frozen=True, match_args=False) +class CommonFunctionProperties: + tree: SfgCallTreeNode + parameters: tuple[SfgVar, ...] + return_type: PsType + inline: bool + constexpr: bool + attributes: Sequence[str] + + @staticmethod + def collect_params(tree: SfgCallTreeNode, required_params: Sequence[SfgVar] | None): + from .postprocessing import CallTreePostProcessing + + param_collector = CallTreePostProcessing() + 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)) + + return parameters + + +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,38 +246,24 @@ class SfgFunction(SfgCodeEntity): tree: SfgCallTreeNode, return_type: PsType = void, inline: bool = False, + constexpr: bool = False, + attributes: Sequence[str] = (), + required_params: Sequence[SfgVar] | None = None, ): super().__init__(name, namespace) - self._tree = tree - self._return_type = return_type - self._inline = inline - - self._parameters: tuple[SfgVar, ...] + parameters = self.collect_params(tree, required_params) - from .postprocessing import CallTreePostProcessing - - param_collector = CallTreePostProcessing() - self._parameters = tuple( - sorted(param_collector(self._tree).function_params, key=lambda p: p.name) + CommonFunctionProperties.__init__( + self, + tree, + parameters, + return_type, + inline, + constexpr, + attributes, ) - @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 - class SfgVisibility(Enum): """Visibility qualifiers of C++""" @@ -323,10 +340,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,22 +353,31 @@ class SfgMethod(SfgClassMember): return_type: PsType = void, inline: bool = False, const: bool = False, + static: bool = False, + constexpr: bool = False, + virtual: bool = False, + override: bool = False, + attributes: Sequence[str] = (), + required_params: Sequence[SfgVar] | None = None, ): super().__init__(cls) self._name = name - self._tree = tree - self._return_type = return_type - self._inline = inline + self._static = static 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) + self._virtual = virtual + self._override = override + + parameters = self.collect_params(tree, required_params) + + CommonFunctionProperties.__init__( + self, + tree, + parameters, + return_type, + inline, + constexpr, + attributes, ) @property @@ -359,24 +385,20 @@ class SfgMethod(SfgClassMember): return self._name @property - def parameters(self) -> tuple[SfgVar, ...]: - return self._parameters - - @property - def tree(self) -> SfgCallTreeNode: - return self._tree + def static(self) -> bool: + return self._static @property - def return_type(self) -> PsType: - return self._return_type + def const(self) -> bool: + return self._const @property - def inline(self) -> bool: - return self._inline + def virtual(self) -> bool: + return self._virtual @property - def const(self) -> bool: - return self._const + def override(self) -> bool: + return self._override class SfgConstructor(SfgClassMember): diff --git a/src/pystencilssfg/ir/postprocessing.py b/src/pystencilssfg/ir/postprocessing.py index 5563783a5fa14f637e0f16fd0579f266cc9c0c27..469383c123d7403ca448efb40dad5692d46d0a3c 100644 --- a/src/pystencilssfg/ir/postprocessing.py +++ b/src/pystencilssfg/ir/postprocessing.py @@ -217,6 +217,8 @@ class SfgDeferredParamSetter(SfgDeferredNode): class SfgDeferredFieldMapping(SfgDeferredNode): + """Deferred mapping of a pystencils field to a field data structure.""" + def __init__( self, psfield: Field, diff --git a/src/pystencilssfg/lang/cpp/std_tuple.py b/src/pystencilssfg/lang/cpp/std_tuple.py index 58a3530b9e98e2c39e205fd7dac9845b4ff35bda..7ea0e2416b2280042521b871b1b40a623971b187 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/src/pystencilssfg/lang/expressions.py b/src/pystencilssfg/lang/expressions.py index d0b4978a49c9247eb57edb44c5568468d5a38442..67bd3eb977e7b786c2dcff7b97e8b8a729ab49a2 100644 --- a/src/pystencilssfg/lang/expressions.py +++ b/src/pystencilssfg/lang/expressions.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import Iterable, TypeAlias, Any, cast from itertools import chain from abc import ABC, abstractmethod @@ -13,19 +14,6 @@ from ..exceptions import SfgException from .headers import HeaderFile from .types import strip_ptr_ref, CppType, CppTypeFactory, cpptype -__all__ = [ - "SfgVar", - "AugExpr", - "CppClass", - "VarLike", - "ExprLike", - "asvar", - "depends", - "IFieldExtraction", - "SrcField", - "SrcVector", -] - class SfgVar: """C++ Variable. @@ -98,7 +86,7 @@ class DependentExpression: Args: expr: C++ Code string of the expression - depends: Iterable of variables and/or `AugExpr`s from which variable and header dependencies are collected + depends: Iterable of variables and/or `AugExpr` from which variable and header dependencies are collected includes: Iterable of header files which this expression additionally depends on """ @@ -374,8 +362,9 @@ def cppclass( ): """ Convience class decorator for CppClass. - It adds to the decorate class the variable `template` via `lang.cpptype` - and sets `lang.CppClass` as a base clase. + It adds to the decorated class the variable ``template`` via `cpptype` + and sets `CppClass` as a base clase. + >>> @cppclass("MyClass", "MyClass.hpp") ... class MyClass: ... pass diff --git a/tests/generator_scripts/index.yaml b/tests/generator_scripts/index.yaml index 0e08e228702bf5c762120e9bc66117a5892bf66d..68352fe2c2904c11e551955eeb29a6bf9424e126 100644 --- a/tests/generator_scripts/index.yaml +++ b/tests/generator_scripts/index.yaml @@ -47,6 +47,12 @@ SimpleClasses: sfg-args: output-mode: header-only +ComposerFeatures: + expect-code: + hpp: + - regex: >- + \[\[nodiscard\]\]\s*static\s*double\s*geometric\(\s*double\s*q,\s*uint64_t\s*k\) + 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 0000000000000000000000000000000000000000..4ca651aef66e714d39cf65cf624a5ba9de52dabd --- /dev/null +++ b/tests/generator_scripts/source/ComposerFeatures.harness.cpp @@ -0,0 +1,35 @@ +#include "ComposerFeatures.hpp" + +#include <cmath> + +#undef NDEBUG +#include <cassert> + +/* Evaluate constexpr functions 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 ); + +static_assert( ConstexprMath::abs(ConstexprMath::geometric(0.5, 0) - 1.0) < 1e-10 ); +static_assert( ConstexprMath::abs(ConstexprMath::geometric(0.5, 1) - 1.5) < 1e-10 ); +static_assert( ConstexprMath::abs(ConstexprMath::geometric(0.5, 2) - 1.75) < 1e-10 ); +static_assert( ConstexprMath::abs(ConstexprMath::geometric(0.5, 3) - 1.875) < 1e-10 ); + +int main(void) { + assert( std::fabs(Series::geometric(0.5, 0) - 1.0) < 1e-10 ); + assert( std::fabs(Series::geometric(0.5, 1) - 1.5) < 1e-10 ); + assert( std::fabs(Series::geometric(0.5, 2) - 1.75) < 1e-10 ); + assert( std::fabs(Series::geometric(0.5, 3) - 1.875) < 1e-10 ); + + inheritance_test::Parent p; + assert( p.compute() == 24 ); + + inheritance_test::Child c; + assert( c.compute() == 31 ); + + auto & cp = dynamic_cast< inheritance_test::Parent & >(c); + assert( cp.compute() == 31 ); +} diff --git a/tests/generator_scripts/source/ComposerFeatures.py b/tests/generator_scripts/source/ComposerFeatures.py new file mode 100644 index 0000000000000000000000000000000000000000..ab97d7acc01884c679b2645ccce149b15005569c --- /dev/null +++ b/tests/generator_scripts/source/ComposerFeatures.py @@ -0,0 +1,69 @@ +from pystencilssfg import SourceFileGenerator + + +with SourceFileGenerator() as sfg: + + sfg.function("factorial").params(sfg.var("n", "uint64")).returns( + "uint64" + ).inline().constexpr()( + sfg.branch("n == 0")("return 1;")("return n * factorial(n - 1);") + ) + + q = sfg.var("q", "double") + k = sfg.var("k", "uint64_t") + x = sfg.var("x", "double") + + sfg.include("<cmath>") + + sfg.struct("Series")( + sfg.method("geometric") + .static() + .attr("nodiscard") + .params(q, k) + .returns("double")( + sfg.branch("k == 0")( + "return 1.0;" + )( + "return Series::geometric(q, k - 1) + std::pow(q, k);" + ) + ) + ) + + sfg.struct("ConstexprMath")( + sfg.method("abs").static().constexpr().inline() + .params(x) + .returns("double") + ( + "if (x >= 0.0) return x; else return -x;" + ), + + sfg.method("geometric") + .static() + .constexpr() + .inline() + .params(q, k) + .returns("double")( + sfg.branch("k == 0")( + "return 1.0;" + )( + "return 1 + q * ConstexprMath::geometric(q, k - 1);" + ) + ) + ) + + with sfg.namespace("inheritance_test"): + sfg.klass("Parent")( + sfg.public( + sfg.method("compute").returns("int").virtual().const()( + "return 24;" + ) + ) + ) + + sfg.klass("Child", bases=["public Parent"])( + sfg.public( + sfg.method("compute").returns("int").override().const()( + "return 31;" + ) + ) + ) diff --git a/tests/generator_scripts/source/Conditionals.py b/tests/generator_scripts/source/Conditionals.py index 9016b73744f78fef504bb4f09b6742a630a6b12d..6c2490808f9617e6780b1ffcf94af614d254fd02 100644 --- a/tests/generator_scripts/source/Conditionals.py +++ b/tests/generator_scripts/source/Conditionals.py @@ -22,7 +22,7 @@ with SourceFileGenerator() as sfg: ) ) - sfg.function("getRating", "int32")( + sfg.function("getRating").returns("int32")( sfg.switch(noodle, autobreak=False) .case("Noodles::RIGATONI")( "return 13;" @@ -39,7 +39,7 @@ with SourceFileGenerator() as sfg: "return 0;" ) - sfg.function("isItalian", return_type="bool")( + sfg.function("isItalian").returns("bool")( sfg.branch( sfg.expr("{0} == Noodles::RIGATONI || {0} == Noodles::SPAGHETTI", noodle) )( diff --git a/tests/generator_scripts/source/SimpleClasses.py b/tests/generator_scripts/source/SimpleClasses.py index 26502f0e149c11d470e700269f5ff526aff3ce85..d2bc6da77c01914d0c477977209efbef005da39b 100644 --- a/tests/generator_scripts/source/SimpleClasses.py +++ b/tests/generator_scripts/source/SimpleClasses.py @@ -12,7 +12,7 @@ with SourceFileGenerator() as sfg: sfg.klass("Point")( sfg.public( sfg.constructor(x, y, z).init(x_)(x).init(y_)(y).init(z_)(z), - sfg.method("getX", returns="const int64_t", const=True, inline=True)( + sfg.method("getX").returns("const int64_t").const().inline()( "return this->x_;" ), ), @@ -22,7 +22,7 @@ with SourceFileGenerator() as sfg: sfg.klass("SpecialPoint", bases=["public Point"])( sfg.public( "using Point::Point;", - sfg.method("getY", returns="const int64_t", const=True, inline=True)( + sfg.method("getY").returns("const int64_t").const().inline()( "return this->y_;" ), ) diff --git a/tests/lang/test_cpp_stl_classes.py b/tests/lang/test_cpp_stl_classes.py index 47966e2279cf6982d6b91884abc65bca50a2bcfb..fee7c70a6e387cba9d813215d753c518b6e115b5 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>"