From b0ee22474a457e86e6013f39f4655118b7dcdb65 Mon Sep 17 00:00:00 2001 From: Frederik Hennig <frederik.hennig@fau.de> Date: Fri, 15 Dec 2023 20:34:33 +0100 Subject: [PATCH] Vast extensions to documentation --- docs/api/composer.md | 8 + docs/api/{composition.md => context.md} | 2 - docs/api/emission.md | 14 ++ docs/api/generator.md | 5 +- docs/api/index.md | 18 ++ docs/api/source_components.md | 36 ++- mkdocs.yml | 6 +- src/pystencilssfg/cli.py | 4 +- src/pystencilssfg/composer/__init__.py | 10 + .../basic_composer.py} | 184 ++------------ src/pystencilssfg/composer/class_composer.py | 230 ++++++++++++++++++ src/pystencilssfg/composer/composer.py | 22 ++ src/pystencilssfg/configuration.py | 33 --- src/pystencilssfg/context.py | 83 ++++++- src/pystencilssfg/emission/__init__.py | 4 +- src/pystencilssfg/emission/clang_format.py | 18 ++ ...der_source_pair.py => header_impl_pair.py} | 8 +- src/pystencilssfg/generator.py | 54 +++- 18 files changed, 523 insertions(+), 216 deletions(-) create mode 100644 docs/api/composer.md rename docs/api/{composition.md => context.md} (57%) create mode 100644 docs/api/emission.md create mode 100644 src/pystencilssfg/composer/__init__.py rename src/pystencilssfg/{composer.py => composer/basic_composer.py} (68%) create mode 100644 src/pystencilssfg/composer/class_composer.py create mode 100644 src/pystencilssfg/composer/composer.py rename src/pystencilssfg/emission/{header_source_pair.py => header_impl_pair.py} (78%) diff --git a/docs/api/composer.md b/docs/api/composer.md new file mode 100644 index 0000000..c0542aa --- /dev/null +++ b/docs/api/composer.md @@ -0,0 +1,8 @@ + +::: pystencilssfg.composer.SfgComposer + +::: pystencilssfg.composer.SfgBasicComposer + +::: pystencilssfg.composer.SfgClassComposer + +::: pystencilssfg.composer.make_sequence \ No newline at end of file diff --git a/docs/api/composition.md b/docs/api/context.md similarity index 57% rename from docs/api/composition.md rename to docs/api/context.md index 4c58610..fcc4948 100644 --- a/docs/api/composition.md +++ b/docs/api/context.md @@ -1,4 +1,2 @@ ::: pystencilssfg.context.SfgContext - -::: pystencilssfg.composer diff --git a/docs/api/emission.md b/docs/api/emission.md new file mode 100644 index 0000000..fa6211e --- /dev/null +++ b/docs/api/emission.md @@ -0,0 +1,14 @@ + +## Output Configuration + +::: pystencilssfg.configuration.SfgOutputSpec + +## Header-Implementation-Pair Emission + +::: pystencilssfg.emission.HeaderImplPairEmitter + +## Code Style and `clang-format` + +::: pystencilssfg.configuration.SfgCodeStyle + +::: pystencilssfg.emission.clang_format.invoke_clang_format \ No newline at end of file diff --git a/docs/api/generator.md b/docs/api/generator.md index 32a0b8e..07128fd 100644 --- a/docs/api/generator.md +++ b/docs/api/generator.md @@ -1,4 +1,5 @@ -::: pystencilssfg.configuration +::: pystencilssfg.generator.SourceFileGenerator + +::: pystencilssfg.configuration.SfgConfiguration -::: pystencilssfg.generator diff --git a/docs/api/index.md b/docs/api/index.md index e69de29..3dd4689 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -0,0 +1,18 @@ +# API Documentation + +These pages document the public API of *pystencils-sfg*. + +### Front End + + - [Source File Generator](generator.md) + - [Code Generation Context](context.md) + - [Composer](composer.md) + +### Source File Modelling + + - [Source File Components](source_components.md) + - [Kernel Call Tree](tree.md) + +### Code Generation + + - [Emission and Printing](emission.md) \ No newline at end of file diff --git a/docs/api/source_components.md b/docs/api/source_components.md index a7d1a59..29448be 100644 --- a/docs/api/source_components.md +++ b/docs/api/source_components.md @@ -1,2 +1,36 @@ -::: pystencilssfg.source_components +## Kernels and Kernel Namespaces + +::: pystencilssfg.source_components.SfgKernelNamespace + +::: pystencilssfg.source_components.SfgKernelHandle + +## Includes + +::: pystencilssfg.source_components.SfgHeaderInclude + +## Functions + +::: pystencilssfg.source_components.SfgFunction + +## Classes + +::: pystencilssfg.source_components.SfgClassKeyword + +::: pystencilssfg.source_components.SfgClass + +### Visibility + +::: pystencilssfg.source_components.SfgVisibility + +::: pystencilssfg.source_components.SfgVisibilityBlock + +### Members + +::: pystencilssfg.source_components.SfgClassMember + +::: pystencilssfg.source_components.SfgInClassDefinition + +::: pystencilssfg.source_components.SfgConstructor + +::: pystencilssfg.source_components.SfgMethod diff --git a/mkdocs.yml b/mkdocs.yml index c962ad4..e92f517 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,7 +24,7 @@ plugins: members_order: source group_by_category: False show_root_heading: True - show_root_full_path: False + show_root_full_path: True show_symbol_type_heading: True show_symbol_type_toc: True show_source: False @@ -46,6 +46,8 @@ nav: - 'API Documentation': - 'Overview': api/index.md - 'Source File Generator': api/generator.md - - 'Composer': api/composition.md + - 'Code Generation Context': api/context.md + - 'Composer': api/composer.md - 'Source File Components': api/source_components.md - 'Kernel Call Tree': api/tree.md + - 'Emission and Printing': api/emission.md diff --git a/src/pystencilssfg/cli.py b/src/pystencilssfg/cli.py index 933c28a..9cf4d4a 100644 --- a/src/pystencilssfg/cli.py +++ b/src/pystencilssfg/cli.py @@ -72,9 +72,9 @@ def list_files(args): _, scriptname = path.split(args.codegen_script) basename = path.splitext(scriptname)[0] - from .emission import HeaderSourcePairEmitter + from .emission import HeaderImplPairEmitter - emitter = HeaderSourcePairEmitter(config.get_output_spec(basename)) + emitter = HeaderImplPairEmitter(config.get_output_spec(basename)) print(args.sep.join(emitter.output_files), end=os.linesep if args.newline else '') diff --git a/src/pystencilssfg/composer/__init__.py b/src/pystencilssfg/composer/__init__.py new file mode 100644 index 0000000..e895fe6 --- /dev/null +++ b/src/pystencilssfg/composer/__init__.py @@ -0,0 +1,10 @@ +from .composer import SfgComposer +from .basic_composer import SfgBasicComposer, make_sequence +from .class_composer import SfgClassComposer + +__all__ = [ + 'SfgComposer', + "make_sequence", + "SfgBasicComposer", + "SfgClassComposer" +] diff --git a/src/pystencilssfg/composer.py b/src/pystencilssfg/composer/basic_composer.py similarity index 68% rename from src/pystencilssfg/composer.py rename to src/pystencilssfg/composer/basic_composer.py index cb51c56..29b8d96 100644 --- a/src/pystencilssfg/composer.py +++ b/src/pystencilssfg/composer/basic_composer.py @@ -6,7 +6,7 @@ import numpy as np from pystencils import Field from pystencils.astnodes import KernelFunction -from .tree import ( +from ..tree import ( SfgCallTreeNode, SfgKernelCallNode, SfgStatements, @@ -15,45 +15,42 @@ from .tree import ( SfgSequence, SfgBlock, ) -from .tree.deferred_nodes import SfgDeferredFieldMapping -from .tree.conditional import SfgCondition, SfgCustomCondition, SfgBranch, SfgSwitch -from .source_components import ( +from ..tree.deferred_nodes import SfgDeferredFieldMapping +from ..tree.conditional import SfgCondition, SfgCustomCondition, SfgBranch, SfgSwitch +from ..source_components import ( SfgFunction, SfgHeaderInclude, SfgKernelNamespace, SfgKernelHandle, SfgClass, - SfgClassMember, - SfgInClassDefinition, SfgConstructor, - SfgMethod, SfgMemberVariable, SfgClassKeyword, - SfgVisibility, - SfgVisibilityBlock, ) -from .source_concepts import SrcObject, SrcField, TypedSymbolOrObject, SrcVector -from .types import cpp_typename, SrcType -from .exceptions import SfgException +from ..source_concepts import SrcObject, SrcField, TypedSymbolOrObject, SrcVector +from ..types import cpp_typename, SrcType +from ..exceptions import SfgException if TYPE_CHECKING: - from .context import SfgContext + from ..context import SfgContext -class SfgComposer: - """Primary interface for constructing source files in pystencils-sfg.""" +class SfgBasicComposer: + """Composer for basic source components.""" def __init__(self, ctx: SfgContext): - self._ctx = ctx + self._ctx: SfgContext = ctx @property def context(self): return self._ctx def prelude(self, content: str): - """Add a string to the code file's prelude. + """Append a string to the prelude comment, to be printed at the top of both generated files. - Do not wrap the given string in comment syntax.""" + The string should not contain C/C++ comment delimiters, since these will be added automatically + during code generation. + """ self._ctx.append_to_prelude(content) def define(self, definition: str): @@ -83,6 +80,13 @@ class SfgComposer: return kns def include(self, header_file: str, private: bool = False): + """Include a header file. + + Args: + header_file: Path to the header file. Enclose in `<>` for a system header. + private: If `True`, in header-implementation code generation, the header file is + only included in the implementation file. + """ self._ctx.add_include(parse_include(header_file, private)) def numpy_struct( @@ -367,150 +371,6 @@ def parse_include(incl: str | SfgHeaderInclude, private: bool = False): return SfgHeaderInclude(incl, system_header=system_header, private=private) -class SfgClassComposer: - def __init__(self, ctx: SfgContext): - self._ctx = ctx - - class VisibilityContext: - def __init__(self, visibility: SfgVisibility): - self._vis_block = SfgVisibilityBlock(visibility) - - def members(self): - yield from self._vis_block.members() - - def __call__( - self, - *args: ( - SfgClassMember | SfgClassComposer.ConstructorBuilder | SrcObject | str - ), - ): - for arg in args: - self._vis_block.append_member(SfgClassComposer._resolve_member(arg)) - - return self - - def resolve(self, cls: SfgClass) -> None: - cls.append_visibility_block(self._vis_block) - - class ConstructorBuilder: - def __init__(self, *params: SrcObject): - self._params = params - self._initializers: list[str] = [] - self._body = "" - - def init(self, initializer: str) -> SfgClassComposer.ConstructorBuilder: - self._initializers.append(initializer) - return self - - def body(self, body: str): - self._body = body - return self - - def resolve(self) -> SfgConstructor: - return SfgConstructor( - parameters=self._params, - initializers=self._initializers, - body=self._body, - ) - - def klass(self, class_name: str, bases: Sequence[str] = ()): - return self._class(class_name, SfgClassKeyword.CLASS, bases) - - def struct(self, class_name: str, bases: Sequence[str] = ()): - return self._class(class_name, SfgClassKeyword.STRUCT, bases) - - @property - def public(self) -> SfgClassComposer.VisibilityContext: - return SfgClassComposer.VisibilityContext(SfgVisibility.PUBLIC) - - @property - def protected(self) -> SfgClassComposer.VisibilityContext: - return SfgClassComposer.VisibilityContext(SfgVisibility.PROTECTED) - - @property - def private(self) -> SfgClassComposer.VisibilityContext: - return SfgClassComposer.VisibilityContext(SfgVisibility.PRIVATE) - - def var(self, name: str, dtype: SrcType): - return SfgMemberVariable(name, dtype) - - def constructor(self, *params): - return SfgClassComposer.ConstructorBuilder(*params) - - def method( - self, - name: str, - returns: SrcType = SrcType("void"), - inline: bool = False, - const: bool = False, - ): - def sequencer(*args: str | tuple | SfgCallTreeNode | SfgNodeBuilder): - tree = make_sequence(*args) - return SfgMethod( - name, tree, return_type=returns, inline=inline, const=const - ) - - return sequencer - - # INTERNALS - - def _class(self, class_name: str, keyword: SfgClassKeyword, bases: Sequence[str]): - if self._ctx.get_class(class_name) is not None: - raise ValueError(f"Class or struct {class_name} already exists.") - - cls = SfgClass(class_name, class_keyword=keyword, bases=bases) - self._ctx.add_class(cls) - - def sequencer( - *args: ( - SfgClassComposer.VisibilityContext - | SfgClassMember - | SfgClassComposer.ConstructorBuilder - | SrcObject - | str - ), - ): - default_ended = False - - for arg in args: - if isinstance(arg, SfgClassComposer.VisibilityContext): - default_ended = True - arg.resolve(cls) - elif isinstance( - arg, - ( - SfgClassMember, - SfgClassComposer.ConstructorBuilder, - SrcObject, - str, - ), - ): - if default_ended: - raise SfgException( - "Composer Syntax Error: " - "Cannot add members with default visibility after a visibility block." - ) - else: - cls.default.append_member(self._resolve_member(arg)) - else: - raise SfgException(f"{arg} is not a valid class member.") - - return sequencer - - @staticmethod - def _resolve_member( - arg: (SfgClassMember | SfgClassComposer.ConstructorBuilder | SrcObject | str), - ): - if isinstance(arg, SrcObject): - return SfgMemberVariable(arg.name, arg.dtype) - elif isinstance(arg, str): - return SfgInClassDefinition(arg) - elif isinstance(arg, SfgClassComposer.ConstructorBuilder): - return arg.resolve() - else: - return arg - - def struct_from_numpy_dtype( struct_name: str, dtype: np.dtype, add_constructor: bool = True ): diff --git a/src/pystencilssfg/composer/class_composer.py b/src/pystencilssfg/composer/class_composer.py new file mode 100644 index 0000000..e326292 --- /dev/null +++ b/src/pystencilssfg/composer/class_composer.py @@ -0,0 +1,230 @@ +from __future__ import annotations +from typing import TYPE_CHECKING, Sequence + +from ..tree import SfgCallTreeNode +from ..source_components import ( + SfgClass, + SfgClassMember, + SfgInClassDefinition, + SfgConstructor, + SfgMethod, + SfgMemberVariable, + SfgClassKeyword, + SfgVisibility, + SfgVisibilityBlock, +) +from ..source_concepts import SrcObject +from ..types import SrcType +from ..exceptions import SfgException + +from .basic_composer import SfgBasicComposer, SfgNodeBuilder, make_sequence + +if TYPE_CHECKING: + from ..context import SfgContext + + +class SfgClassComposer: + """Composer for classes and structs. + + + This class cannot be instantiated on its own but must be mixed in with + [SfgBasicComposer][pystencilssfg.composer.SfgBasicComposer]. + Use through [SfgComposer][pystencilssfg.SfgComposer]. + """ + + def __init__(self, ctx: SfgContext): + if not isinstance(self, SfgBasicComposer): + raise Exception("SfgClassComposer must be mixed-in with SfgBasicComposer.") + self._ctx: SfgContext = ctx + + class VisibilityContext: + """Represent a visibility block in the composer syntax. + + Returned by + [private][pystencilssfg.composer.SfgClassComposer.private], + [public][pystencilssfg.composer.SfgClassComposer.public] and + [protected][pystencilssfg.composer.SfgClassComposer.protected]. + """ + + def __init__(self, visibility: SfgVisibility): + self._vis_block = SfgVisibilityBlock(visibility) + + def members(self): + yield from self._vis_block.members() + + def __call__( + self, + *args: ( + SfgClassMember | SfgClassComposer.ConstructorBuilder | SrcObject | str + ), + ): + for arg in args: + self._vis_block.append_member(SfgClassComposer._resolve_member(arg)) + + return self + + def resolve(self, cls: SfgClass) -> None: + cls.append_visibility_block(self._vis_block) + + class ConstructorBuilder: + """Composer syntax for constructor building. + + Returned by [constructor][pystencilssfg.composer.SfgClassComposer.constructor]. + """ + + def __init__(self, *params: SrcObject): + self._params = params + self._initializers: list[str] = [] + self._body: str | None = None + + def init(self, initializer: str) -> SfgClassComposer.ConstructorBuilder: + """Add an initialization expression to the constructor's initializer list.""" + self._initializers.append(initializer) + return self + + def body(self, body: str): + """Define the constructor body""" + if self._body is not None: + raise SfgException("Multiple definitions of constructor body.") + self._body = body + return self + + def resolve(self) -> SfgConstructor: + return SfgConstructor( + parameters=self._params, + initializers=self._initializers, + body=self._body if self._body is not None else "", + ) + + def klass(self, class_name: str, bases: Sequence[str] = ()): + """Create a class and add it to the underlying context. + + Args: + class_name: Name of the class + bases: List of base classes + """ + return self._class(class_name, SfgClassKeyword.CLASS, bases) + + def struct(self, class_name: str, bases: Sequence[str] = ()): + """Create a struct and add it to the underlying context. + + Args: + class_name: Name of the struct + bases: List of base classes + """ + return self._class(class_name, SfgClassKeyword.STRUCT, bases) + + @property + def public(self) -> SfgClassComposer.VisibilityContext: + """Create a `public` visibility block in a class body""" + return SfgClassComposer.VisibilityContext(SfgVisibility.PUBLIC) + + @property + def protected(self) -> SfgClassComposer.VisibilityContext: + """Create a `protected` visibility block in a class or struct body""" + return SfgClassComposer.VisibilityContext(SfgVisibility.PROTECTED) + + @property + def private(self) -> SfgClassComposer.VisibilityContext: + """Create a `private` visibility block in a class or struct body""" + return SfgClassComposer.VisibilityContext(SfgVisibility.PRIVATE) + + def var(self, name: str, dtype: SrcType): + """In a class or struct body or visibility block, and a member variable. + + Args: + name: The variable's name + dtype: The variable's data type + """ + return SfgMemberVariable(name, dtype) + + def constructor(self, *params: SrcObject): + """In a class or struct body or visibility block, add a constructor. + + Args: + params: List of constructor parameters + """ + return SfgClassComposer.ConstructorBuilder(*params) + + def method( + self, + name: str, + returns: SrcType = SrcType("void"), + inline: bool = False, + const: bool = False, + ): + """In a class or struct body or visibility block, add a method. + The usage is similar to [SfgBasicComposer.function][pystencilssfg.composer.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. + """ + + def sequencer(*args: str | tuple | SfgCallTreeNode | SfgNodeBuilder): + tree = make_sequence(*args) + return SfgMethod( + name, tree, return_type=returns, inline=inline, const=const + ) + + return sequencer + + # INTERNALS + + def _class(self, class_name: str, keyword: SfgClassKeyword, bases: Sequence[str]): + if self._ctx.get_class(class_name) is not None: + raise ValueError(f"Class or struct {class_name} already exists.") + + cls = SfgClass(class_name, class_keyword=keyword, bases=bases) + self._ctx.add_class(cls) + + def sequencer( + *args: ( + SfgClassComposer.VisibilityContext + | SfgClassMember + | SfgClassComposer.ConstructorBuilder + | SrcObject + | str + ), + ): + default_ended = False + + for arg in args: + if isinstance(arg, SfgClassComposer.VisibilityContext): + default_ended = True + arg.resolve(cls) + elif isinstance( + arg, + ( + SfgClassMember, + SfgClassComposer.ConstructorBuilder, + SrcObject, + str, + ), + ): + if default_ended: + raise SfgException( + "Composer Syntax Error: " + "Cannot add members with default visibility after a visibility block." + ) + else: + cls.default.append_member(self._resolve_member(arg)) + else: + raise SfgException(f"{arg} is not a valid class member.") + + return sequencer + + @staticmethod + def _resolve_member( + arg: (SfgClassMember | SfgClassComposer.ConstructorBuilder | SrcObject | str), + ): + if isinstance(arg, SrcObject): + return SfgMemberVariable(arg.name, arg.dtype) + elif isinstance(arg, str): + return SfgInClassDefinition(arg) + elif isinstance(arg, SfgClassComposer.ConstructorBuilder): + return arg.resolve() + else: + return arg diff --git a/src/pystencilssfg/composer/composer.py b/src/pystencilssfg/composer/composer.py new file mode 100644 index 0000000..3ed79c5 --- /dev/null +++ b/src/pystencilssfg/composer/composer.py @@ -0,0 +1,22 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + +from .basic_composer import SfgBasicComposer +from .class_composer import SfgClassComposer + +if TYPE_CHECKING: + from ..context import SfgContext + + +class SfgComposer(SfgBasicComposer, SfgClassComposer): + """Primary interface for constructing source files in pystencils-sfg. + + The SfgComposer combines the [SfgBasicComposer][pystencilssfg.composer.SfgBasicComposer] + for the basic components (kernel namespaces, includes, definitions, and functions) + and the [SfgClassComposer][pystencilssfg.composer.SfgClassComposer] for constructing + `struct`s and `class`es. + """ + + def __init__(self, ctx: SfgContext): + SfgBasicComposer.__init__(self, ctx) + SfgClassComposer.__init__(self, ctx) diff --git a/src/pystencilssfg/configuration.py b/src/pystencilssfg/configuration.py index a7bced4..c6c7422 100644 --- a/src/pystencilssfg/configuration.py +++ b/src/pystencilssfg/configuration.py @@ -1,36 +1,3 @@ -""" -The [source file generator][pystencilssfg.SourceFileGenerator] draws configuration from a total of four sources: - - - The [default configuration][pystencilssfg.configuration.DEFAULT_CONFIG]; - - The project configuration; - - Command-line arguments; - - The user configuration passed to the constructor of `SourceFileGenerator`. - -They take precedence in the following way: - - - Project configuration overrides the default configuration - - Command line arguments override the project configuration - - User configuration overrides default and project configuration, - and must not conflict with command-line arguments; otherwise, an error is thrown. - -### Project Configuration via Configurator Script - -Currently, the only way to define the project configuration is via a configuration module. -A configurator module is a Python file defining the following function at the top-level: - -```Python -from pystencilssfg import SfgConfiguration - -def sfg_config() -> SfgConfiguration: - # ... - return SfgConfiguration( - # ... - ) -``` - -The configuration module is passed to the code generation script via the command-line argument -`--sfg-config-module`. -""" # mypy: strict_optional=False from __future__ import annotations diff --git a/src/pystencilssfg/context.py b/src/pystencilssfg/context.py index 222b313..968be89 100644 --- a/src/pystencilssfg/context.py +++ b/src/pystencilssfg/context.py @@ -11,12 +11,51 @@ from .exceptions import SfgException class SfgContext: + """Represents a header/implementation file pair in the code generator. + + ## Source File Properties and Components + + The SfgContext collects all properties and components of a header/implementation + file pair (or just the header file, if header-only generation is used). + These are: + + - The code namespace, which is combined from the [outer_namespace][pystencilssfg.SfgContext.outer_namespace] + and the [inner_namespace][pystencilssfg.SfgContext.inner_namespace]. The outer namespace is meant to be set + externally e.g. by the project configuration, while the inner namespace is meant to be set by the generator + script. + - The [prelude comment][pystencilssfg.SfgContext.prelude_comment] is a block of text printed as a comment block + at the top of both generated files. Typically, it contains authorship and licence information. + - The set of [Included header files][pystencilssfg.SfgContext.includes]. + - Custom [definitions][pystencilssfg.SfgContext.definitions], which are just arbitrary code strings. + - Any number of [kernel namespaces][pystencilssfg.SfgContext.kernel_namespaces], within which *pystencils* + kernels are managed. + - Any number of [functions][pystencilssfg.SfgContext.functions], which are meant to serve as wrappers + around kernel calls. + - Any number of [classes][pystencilssfg.SfgContext.classes], which can be used to build more extensive wrappers + around kernels. + + ## Order of Definitions + + To honor C/C++ use-after-declare rules, the context preserves the order in which definitions, functions and classes + are added to it. + The header file printers implemented in *pystencils-sfg* will print the declarations accordingly. + The declarations can retrieved in order of definition via + [declarations_ordered][pystencilssfg.SfgContext.declarations_ordered]. + """ + def __init__( self, outer_namespace: str | None = None, codestyle: SfgCodeStyle = SfgCodeStyle(), argv: Sequence[str] | None = None, ): + """ + Args: + outer_namespace: Qualified name of the outer code namespace + codestyle: Code style that should be used by the code emitter + argv: The generator script's command line arguments; + reserved for internal use by the [SourceFileGenerator][pystencilssfg.SourceFileGenerator]. + """ self._argv = argv self._default_kernel_namespace = SfgKernelNamespace(self, "kernels") @@ -54,14 +93,17 @@ class SfgContext: @property def outer_namespace(self) -> str | None: + """Outer code namespace. Set by constructor argument `outer_namespace`.""" return self._outer_namespace @property def inner_namespace(self) -> str | None: + """Inner code namespace. Set by `set_namespace`.""" return self._inner_namespace @property def fully_qualified_namespace(self) -> str | None: + """Combined outer and inner namespaces, as `outer_namespace::inner_namespace`.""" match (self.outer_namespace, self.inner_namespace): case None, None: return None @@ -76,6 +118,7 @@ class SfgContext: @property def codestyle(self) -> SfgCodeStyle: + """The code style object for this generation context.""" return self._codestyle # ---------------------------------------------------------------------------------------------- @@ -88,6 +131,12 @@ class SfgContext: return self._prelude def append_to_prelude(self, code_str: str): + """Append a string to the prelude comment. + + The string should not contain + C/C++ comment delimiters, since these will be added automatically during + code generation. + """ if self._prelude: self._prelude += "\n" @@ -105,14 +154,19 @@ class SfgContext: self._includes.add(include) def definitions(self) -> Generator[str, None, None]: - """Definitions are code lines printed at the top of the header file, after the includes.""" + """Definitions are arbitrary custom lines of code.""" yield from self._definitions def add_definition(self, definition: str): + """Add a custom code string to the header file.""" self._definitions.append(definition) self._declarations_ordered.append(definition) def set_namespace(self, namespace: str): + """Set the inner code namespace. + + Throws an exception if the namespace was already set. + """ if self._inner_namespace is not None: raise SfgException("The code namespace was already set.") @@ -124,15 +178,22 @@ class SfgContext: @property def default_kernel_namespace(self) -> SfgKernelNamespace: + """The default kernel namespace.""" return self._default_kernel_namespace def kernel_namespaces(self) -> Generator[SfgKernelNamespace, None, None]: + """Iterator over all registered kernel namespaces.""" yield from self._kernel_namespaces.values() def get_kernel_namespace(self, str) -> SfgKernelNamespace | None: + """Retrieve a kernel namespace by name, or `None` if it does not exist.""" return self._kernel_namespaces.get(str) def add_kernel_namespace(self, namespace: SfgKernelNamespace): + """Adds a new kernel namespace. + + If a kernel namespace of the same name already exists, throws an exception. + """ if namespace.name in self._kernel_namespaces: raise ValueError(f"Duplicate kernel namespace: {namespace.name}") @@ -143,13 +204,19 @@ class SfgContext: # ---------------------------------------------------------------------------------------------- def functions(self) -> Generator[SfgFunction, None, None]: + """Iterator over all registered functions.""" yield from self._functions.values() def get_function(self, name: str) -> SfgFunction | None: + """Retrieve a function by name. Returns `None` if no function of the given name exists.""" return self._functions.get(name, None) def add_function(self, func: SfgFunction): - if func.name in self._functions: + """Adds a new function. + + If a function or class with the same name exists already, throws an exception. + """ + if func.name in self._functions or func.name in self._classes: raise SfgException(f"Duplicate function: {func.name}") self._functions[func.name] = func @@ -160,13 +227,19 @@ class SfgContext: # ---------------------------------------------------------------------------------------------- def classes(self) -> Generator[SfgClass, None, None]: + """Iterator over all registered classes.""" yield from self._classes.values() def get_class(self, name: str) -> SfgClass | None: + """Retrieve a class by name, or `None` if the class does not exist.""" return self._classes.get(name, None) def add_class(self, cls: SfgClass): - if cls.class_name in self._classes: + """Add a class. + + Throws an exception if a class or function of the same name exists already. + """ + if cls.class_name in self._classes or cls.class_name in self._functions: raise SfgException(f"Duplicate class: {cls.class_name}") self._classes[cls.class_name] = cls @@ -176,7 +249,9 @@ class SfgContext: # Declarations in order of addition # ---------------------------------------------------------------------------------------------- - def declarations_ordered(self) -> Generator[str | SfgFunction | SfgClass, None, None]: + def declarations_ordered( + self, + ) -> Generator[str | SfgFunction | SfgClass, None, None]: """All declared definitions, classes and functions in the order they were added. Awareness about order is necessary due to the C++ declare-before-use rules.""" diff --git a/src/pystencilssfg/emission/__init__.py b/src/pystencilssfg/emission/__init__.py index fc3c4f6..74314be 100644 --- a/src/pystencilssfg/emission/__init__.py +++ b/src/pystencilssfg/emission/__init__.py @@ -1,5 +1,5 @@ -from .header_source_pair import HeaderSourcePairEmitter +from .header_impl_pair import HeaderImplPairEmitter __all__ = [ - "HeaderSourcePairEmitter" + "HeaderImplPairEmitter" ] diff --git a/src/pystencilssfg/emission/clang_format.py b/src/pystencilssfg/emission/clang_format.py index f0970ae..5c5084c 100644 --- a/src/pystencilssfg/emission/clang_format.py +++ b/src/pystencilssfg/emission/clang_format.py @@ -6,6 +6,24 @@ from ..exceptions import SfgException def invoke_clang_format(code: str, codestyle: SfgCodeStyle) -> str: + """Call the `clang-format` command-line tool to format the given code string + according to the given style arguments. + + Args: + code: Code string to format + codestyle: [SfgCodeStyle][pystencilssfg.configuration.SfgCodeStyle] object + defining the `clang-format` binary and the desired code style. + + Returns: + The formatted code, if `clang-format` was run sucessfully. + Otherwise, the original unformatted code, unless `codestyle.force_clang_format` + was set to true. In the latter case, an exception is thrown. + + Forced Formatting: + If `codestyle.force_clang_format` was set to true but the formatter could not + be executed (binary not found, or error during exection), the function will + throw an exception. + """ args = [codestyle.clang_format_binary, f"--style={codestyle.code_style}"] if not shutil.which(codestyle.clang_format_binary): diff --git a/src/pystencilssfg/emission/header_source_pair.py b/src/pystencilssfg/emission/header_impl_pair.py similarity index 78% rename from src/pystencilssfg/emission/header_source_pair.py rename to src/pystencilssfg/emission/header_impl_pair.py index 8eaef4e..53b6857 100644 --- a/src/pystencilssfg/emission/header_source_pair.py +++ b/src/pystencilssfg/emission/header_impl_pair.py @@ -7,8 +7,11 @@ from .printers import SfgHeaderPrinter, SfgImplPrinter from .clang_format import invoke_clang_format -class HeaderSourcePairEmitter: +class HeaderImplPairEmitter: + """Emits a header-implementation file pair.""" + def __init__(self, output_spec: SfgOutputSpec): + """Create a `HeaderImplPairEmitter` from an [SfgOutputSpec][pystencilssfg.configuration.SfgOutputSpec].""" self._basename = output_spec.basename self._output_directory = output_spec.output_directory self._header_filename = output_spec.get_header_filename() @@ -18,12 +21,15 @@ class HeaderSourcePairEmitter: @property def output_files(self) -> tuple[str, str]: + """The files that will be written by `write_files`.""" return ( path.join(self._output_directory, self._header_filename), path.join(self._output_directory, self._impl_filename), ) def write_files(self, ctx: SfgContext): + """Write the code represented by the given [SfgContext][pystencilssfg.SfgContext] to the files + specified by the output specification.""" ctx = prepare_context(ctx) header_printer = SfgHeaderPrinter(ctx, self._ospec) diff --git a/src/pystencilssfg/generator.py b/src/pystencilssfg/generator.py index 1cd56bd..99339ff 100644 --- a/src/pystencilssfg/generator.py +++ b/src/pystencilssfg/generator.py @@ -5,17 +5,58 @@ import sys import os from os import path -from .configuration import SfgConfiguration, config_from_commandline, merge_configurations +from .configuration import ( + SfgConfiguration, + config_from_commandline, + merge_configurations, +) from .context import SfgContext class SourceFileGenerator: - """Context manager that controls the code generation process in generator scripts.""" + """Context manager that controls the code generation process in generator scripts. + + ## Configuration + + The [source file generator][pystencilssfg.SourceFileGenerator] draws configuration from a total of four sources: + + - The [default configuration][pystencilssfg.configuration.DEFAULT_CONFIG]; + - The project configuration; + - Command-line arguments; + - The user configuration passed to the constructor of `SourceFileGenerator`. + + They take precedence in the following way: + + - Project configuration overrides the default configuration + - Command line arguments override the project configuration + - User configuration overrides default and project configuration, + and must not conflict with command-line arguments; otherwise, an error is thrown. + + ### Project Configuration via Configurator Script + + Currently, the only way to define the project configuration is via a configuration module. + A configurator module is a Python file defining the following function at the top-level: + + ```Python + from pystencilssfg import SfgConfiguration + + def sfg_config() -> SfgConfiguration: + # ... + return SfgConfiguration( + # ... + ) + ``` + + The configuration module is passed to the code generation script via the command-line argument + `--sfg-config-module`. + """ + def __init__(self, sfg_config: SfgConfiguration | None = None): if sfg_config and not isinstance(sfg_config, SfgConfiguration): raise TypeError("sfg_config is not an SfgConfiguration.") import __main__ + scriptpath = __main__.__file__ scriptname = path.split(scriptpath)[1] basename = path.splitext(scriptname)[0] @@ -24,10 +65,13 @@ class SourceFileGenerator: config = merge_configurations(project_config, cmdline_config, sfg_config) - self._context = SfgContext(config.outer_namespace, config.codestyle, argv=script_args) + self._context = SfgContext( + config.outer_namespace, config.codestyle, argv=script_args + ) + + from .emission import HeaderImplPairEmitter - from .emission import HeaderSourcePairEmitter - self._emitter = HeaderSourcePairEmitter(config.get_output_spec(basename)) + self._emitter = HeaderImplPairEmitter(config.get_output_spec(basename)) def clean_files(self): for file in self._emitter.output_files: -- GitLab