From 89d01c91f362fc9e2bf40420967b7434342d4ab9 Mon Sep 17 00:00:00 2001 From: Frederik Hennig <frederik.hennig@fau.de> Date: Mon, 17 Feb 2025 16:31:10 +0100 Subject: [PATCH] extend docs - Write separate installation guide - Extend Getting Started guide - Rewrite landing page - Update code generator monkeypatch to use advanced features of myst-nb --- docs/source/_util/sfg_mockup.py | 19 -- docs/source/_util/sfg_monkeypatch.py | 68 ++++++ docs/source/conf.py | 1 + docs/source/getting_started.md | 194 +++++++++++----- docs/source/index.md | 263 +++------------------- docs/source/installation.md | 45 ++++ docs/source/usage/generator_scripts.md | 294 +------------------------ src/pystencilssfg/emission/emitter.py | 6 +- src/pystencilssfg/generator.py | 43 ++-- 9 files changed, 310 insertions(+), 623 deletions(-) delete mode 100644 docs/source/_util/sfg_mockup.py create mode 100644 docs/source/_util/sfg_monkeypatch.py create mode 100644 docs/source/installation.md diff --git a/docs/source/_util/sfg_mockup.py b/docs/source/_util/sfg_mockup.py deleted file mode 100644 index 4bcaad1..0000000 --- a/docs/source/_util/sfg_mockup.py +++ /dev/null @@ -1,19 +0,0 @@ -from pystencilssfg import SourceFileGenerator -from pystencilssfg.config import SfgConfig - - -class DocsMockupGenerator(SourceFileGenerator): - scriptname: str = "script" - - def _scriptname(self) -> str: - return f"{DocsMockupGenerator.scriptname}.py" - - def __init__( - self, sfg_config: SfgConfig | None = None, keep_unknown_argv: bool = False - ): - if sfg_config is None: - sfg_config = SfgConfig() - - sfg_config.output_directory = "_sfg_out" - - super().__init__(sfg_config, keep_unknown_argv=True) diff --git a/docs/source/_util/sfg_monkeypatch.py b/docs/source/_util/sfg_monkeypatch.py new file mode 100644 index 0000000..0269d40 --- /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/conf.py b/docs/source/conf.py index db8f782..9b6dcf4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -89,3 +89,4 @@ myst_enable_extensions = [ "dollarmath", "colon_fence", ] +nb_render_markdown_format = "myst" diff --git a/docs/source/getting_started.md b/docs/source/getting_started.md index 822ea0b..008585a 100644 --- a/docs/source/getting_started.md +++ b/docs/source/getting_started.md @@ -4,47 +4,26 @@ kernelspec: name: python3 --- +(getting_started)= # Getting Started -## 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 +```{code-cell} ipython3 +:tags: [remove-cell] -As pystencils-sfg is still unreleased, it can at this time only be obtained directly -from its Git repository. +import sys +from pathlib import Path -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: +mockup_path = Path("_util").resolve() +sys.path.append(str(mockup_path)) -```{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" +from sfg_monkeypatch import DocsPatchedGenerator # monkeypatch SFG for docs ``` -````{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+...`. +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 @@ -63,8 +42,7 @@ 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. -Generator scripts are the primary mode of using pystencils-sfg; -in them, code generation is orchestrated by the `SourceFileGenerator` context manager. +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. @@ -73,33 +51,52 @@ Through the composer, we can declaratively populate the generated files with cod 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`. -This gives you a *kernel handle*, through which you can call the kernel from a function: +To register a kernel, pass its assignments to `sfg.kernels.create`, which returns a *kernel handle* object: -```{code-cell} ipython3 -:tags: [remove-cell] +```{code-block} python +src, dst = ps.fields("src, dst: double[1D]") +c = sp.Symbol("c") -import sys -from pathlib import Path +@ps.kernel +def scale(): + dst.center @= c * src.center() -mockup_path = Path("_util").resolve() -sys.path.append(str(mockup_path)) +# Register the kernel for code generation +scale_kernel = sfg.kernels.create(scale, "scale_kernel") +``` -from sfg_mockup import DocsMockupGenerator as SourceFileGenerator +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] -SourceFileGenerator.scriptname = "demo1" + +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: [1D]") + src, dst = ps.fields("src, dst: double[1D]") c = sp.Symbol("c") @ps.kernel @@ -116,21 +113,108 @@ with SourceFileGenerator() as sfg: ``` -::::{tab-set} +When executing the above script, two files will be generated: a C++ header and implementation file containing +the `scale_kernel` and its wrapper function: -:::{tab-item} Generated Header -```{literalinclude} _sfg_out/demo1.hpp -:language: C++ -``` +:::{glue:md} sfg_out_add_kernel_demo +:format: myst ::: -:::{tab-item} Generated Implementation -```{literalinclude} _sfg_out/demo1.cpp -:language: C++ +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 +:tags: [hide-input] + +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. ::: -:::: ## Next Steps + +[mdspan]: https://en.cppreference.com/w/cpp/container/mdspan +[cppreference_compiler_support]: https://en.cppreference.com/w/cpp/compiler_support diff --git a/docs/source/index.md b/docs/source/index.md index b11cb53..4b60903 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -1,15 +1,38 @@ # The pystencils Source File Generator + +[](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 [pystencils](https://pypi.org/project/pystencils/) and C++ HPC frameworks.* + +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. + +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: + + - 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 + + +## Table of Contents + ```{toctree} :maxdepth: 1 -:hidden: +installation getting_started ``` ```{toctree} :maxdepth: 1 -:hidden: :caption: User Guide usage/generator_scripts @@ -21,7 +44,6 @@ usage/tips_n_tricks ```{toctree} :maxdepth: 1 -:hidden: :caption: API Reference api/generation @@ -31,236 +53,5 @@ 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: - -```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" - -#include <math.h> - -#define FUNC_PREFIX inline - -/************************************************************************************* - * Kernels - *************************************************************************************/ - -namespace kernels { - -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. - -```CMake -pystencilssfg_generate_target_sources( <target name> - SCRIPTS kernels.py ... - FILE_EXTENSIONS .h .cpp -) -``` - -*Pystencils-sfg* makes sure that all generated files are on the project's include path. -To `#include` them, add the prefix `gen/<target name>`: - -```C++ -#include "gen/<target name>/kernels.h" -``` - -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 - -Learn how to control code generation from the command line -and how to embedd *pystencils-sfg* into your build system. -``` - -```{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 0000000..77c379b --- /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) for a guide on how to author simple generator scripts. diff --git a/docs/source/usage/generator_scripts.md b/docs/source/usage/generator_scripts.md index 343fd74..59f376d 100644 --- a/docs/source/usage/generator_scripts.md +++ b/docs/source/usage/generator_scripts.md @@ -2,296 +2,6 @@ (guide:generator_scripts)= # Generator Script Configuration and Command-Line Interface -Generator scripts are the primary mode of using pystencils-sfg. -A generator script is a Python script, say `kernels.py`, which uses the pystencils-sfg API -at the top level that, when executed, emits source code to a pair of files `kernels.hpp` -and `kernels.cpp`. -This guide describes the basic structure of generator scripts, their execution from the command line, -the configuration of the code generator both inline and from the shell, -as well as customization of their command-line options. - -## Anatomy - -At minimum, each generator script must contain the following code: - -```{code-block} python -from pystencilssfg import SourceFileGenerator - -with SourceFileGenerator() as sfg: - ... -``` - -The code generation process begins as the above code enters the region controlled by the -`SourceFileGenerator`. -The object `sfg` in the above snippet is the *composer*, which exposes the primary API -for interacting with the code generator. - - -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: @@ -306,7 +16,7 @@ different configuration sources: For details on configuration modules, refer to the guide on [Project and Build System Integration](#guide_project_integration). (inline_config)= -### Inline Configuration +## 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. @@ -317,7 +27,7 @@ of the generator script to `gen_src`: ``` (cmdline_options)= -### Command-Line Options +## Command-Line Options The `SourceFileGenerator` consumes a number of command-line parameters that may be passed to the script on invocation. These include: diff --git a/src/pystencilssfg/emission/emitter.py b/src/pystencilssfg/emission/emitter.py index 7f8870b..909ae16 100644 --- a/src/pystencilssfg/emission/emitter.py +++ b/src/pystencilssfg/emission/emitter.py @@ -21,7 +21,7 @@ class SfgCodeEmitter: 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/generator.py b/src/pystencilssfg/generator.py index 379b24d..ebb84b3 100644 --- a/src/pystencilssfg/generator.py +++ b/src/pystencilssfg/generator.py @@ -149,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: -- GitLab