diff --git a/src/pystencilssfg/composer/basic_composer.py b/src/pystencilssfg/composer/basic_composer.py index 97966247d4be769dfcb800e0b25bcd631bdda69e..9f0673de3c109bbdbbbfa1eaf1a3ebec51af87cf 100644 --- a/src/pystencilssfg/composer/basic_composer.py +++ b/src/pystencilssfg/composer/basic_composer.py @@ -1,13 +1,19 @@ from __future__ import annotations -from typing import Sequence +from typing import Sequence, TypeAlias from abc import ABC, abstractmethod import numpy as np import sympy as sp from functools import reduce -from pystencils import Field +from pystencils import Field, TypedSymbol from pystencils.backend import KernelParameter, KernelFunction -from pystencils.types import create_type, UserTypeSpec, PsCustomType, PsPointerType +from pystencils.types import ( + create_type, + UserTypeSpec, + PsCustomType, + PsPointerType, + PsType, +) from ..context import SfgContext from .custom import CustomGenerator @@ -58,8 +64,23 @@ class SfgNodeBuilder(ABC): pass -ExprLike = str | SfgVar | AugExpr -SequencerArg = tuple | str | AugExpr | SfgCallTreeNode | SfgNodeBuilder +_ExprLike = (str, AugExpr, TypedSymbol) +ExprLike: TypeAlias = str | AugExpr | TypedSymbol +"""Things that may act as a C++ expression. + +Expressions need not necesserily have a known data type. +""" + +_VarLike = (TypedSymbol, AugExpr) +VarLike: TypeAlias = TypedSymbol | AugExpr +"""Things that may act as a variable. + +Variables must always define their name *and* data type. +""" + +_SequencerArg = (tuple, ExprLike, SfgCallTreeNode, SfgNodeBuilder) +SequencerArg: TypeAlias = tuple | ExprLike | SfgCallTreeNode | SfgNodeBuilder +"""Valid arguments to `make_sequence` and any sequencer that uses it.""" class SfgBasicComposer(SfgIComposer): @@ -208,7 +229,11 @@ class SfgBasicComposer(SfgIComposer): num_blocks_str = str(num_blocks) tpb_str = str(threads_per_block) stream_str = str(stream) if stream is not None else None - depends = _depends(num_blocks) | _depends(threads_per_block) | _depends(stream) + + depends = _depends(num_blocks) | _depends(threads_per_block) + if stream is not None: + depends |= _depends(stream) + return SfgCudaKernelInvocation( kernel_handle, num_blocks_str, tpb_str, stream_str, depends ) @@ -217,9 +242,9 @@ class SfgBasicComposer(SfgIComposer): """Syntax sequencing. For details, see `make_sequence`""" return make_sequence(*args) - def params(self, *args: SfgVar) -> SfgFunctionParams: + def params(self, *args: AugExpr) -> SfgFunctionParams: """Use inside a function body to add parameters to the function.""" - return SfgFunctionParams(args) + return SfgFunctionParams([x.as_variable() for x in args]) def require(self, *includes: str | SfgHeaderInclude) -> SfgRequireIncludes: return SfgRequireIncludes( @@ -232,7 +257,7 @@ class SfgBasicComposer(SfgIComposer): ptr: bool = False, ref: bool = False, const: bool = False, - ): + ) -> PsType: if ptr and ref: raise SfgException("Create either a pointer, or a ref type, not both!") @@ -250,11 +275,11 @@ class SfgBasicComposer(SfgIComposer): else: return base_type - def var(self, name: str, dtype: UserTypeSpec) -> SfgVar: + def var(self, name: str, dtype: UserTypeSpec) -> AugExpr: """Create a variable with given name and data type.""" - return SfgVar(name, create_type(dtype)) + return AugExpr(create_type(dtype)).var(name) - def init(self, lhs: SfgVar) -> SfgInplaceInitBuilder: + def init(self, lhs: VarLike) -> SfgInplaceInitBuilder: """Create a C++ in-place initialization. Usage: @@ -270,7 +295,7 @@ class SfgBasicComposer(SfgIComposer): SomeClass obj { arg1, arg2, arg3 }; """ - return SfgInplaceInitBuilder(lhs) + return SfgInplaceInitBuilder(_asvar(lhs)) def expr(self, fmt: str, *deps, **kwdeps): return AugExpr.format(fmt, *deps, **kwdeps) @@ -329,15 +354,8 @@ class SfgBasicComposer(SfgIComposer): def make_statements(arg: ExprLike) -> SfgStatements: - match arg: - case str(): - return SfgStatements(arg, (), ()) - case SfgVar(name, _): - return SfgStatements(name, (), (arg,)) - case AugExpr(): - return SfgStatements(str(arg), (), arg.depends) - case _: - assert False + depends = _depends(arg) + return SfgStatements(str(arg), (), depends) def make_sequence(*args: SequencerArg) -> SfgSequence: @@ -392,10 +410,8 @@ def make_sequence(*args: SequencerArg) -> SfgSequence: children.append(arg.resolve()) elif isinstance(arg, SfgCallTreeNode): children.append(arg) - elif isinstance(arg, AugExpr): - children.append(SfgStatements(str(arg), (), arg.depends)) - elif isinstance(arg, str): - children.append(SfgStatements(arg, (), ())) + elif isinstance(arg, _ExprLike): + children.append(make_statements(arg)) elif isinstance(arg, tuple): # Tuples are treated as blocks subseq = make_sequence(*arg) @@ -407,25 +423,20 @@ def make_sequence(*args: SequencerArg) -> SfgSequence: class SfgInplaceInitBuilder(SfgNodeBuilder): - def __init__(self, lhs: SfgVar | AugExpr) -> None: - if isinstance(lhs, AugExpr): - lhs = lhs.as_variable() - + def __init__(self, lhs: SfgVar) -> None: self._lhs: SfgVar = lhs self._depends: set[SfgVar] = set() self._rhs: str | None = None def __call__( self, - *rhs: str | AugExpr, + *rhs: ExprLike, ) -> SfgInplaceInitBuilder: if self._rhs is not None: raise SfgException("Assignment builder used multiple times.") self._rhs = ", ".join(str(expr) for expr in rhs) - self._depends = reduce( - set.union, (obj.depends for obj in rhs if isinstance(obj, AugExpr)), set() - ) + self._depends = reduce(set.union, (_depends(obj) for obj in rhs), set()) return self def resolve(self) -> SfgCallTreeNode: @@ -538,12 +549,30 @@ def struct_from_numpy_dtype( return cls -def _depends(expr: ExprLike | Sequence[ExprLike] | None) -> set[SfgVar]: +def _asvar(var: VarLike) -> SfgVar: + match var: + case AugExpr(): + return var.as_variable() + case TypedSymbol(): + from pystencils import DynamicType + + if isinstance(var.dtype, DynamicType): + raise SfgException( + f"Unable to cast dynamically typed symbol {var} to a variable.\n" + f"{var} has dynamic type {var.dtype}, which cannot be resolved to a type outside of a kernel." + ) + + return SfgVar(var.name, var.dtype) + case _: + raise ValueError(f"Invalid variable: {var}") + + +def _depends(expr: ExprLike) -> set[SfgVar]: match expr: case None | str(): return set() - case SfgVar(): - return {expr} + case TypedSymbol(): + return {_asvar(expr)} case AugExpr(): return expr.depends case _: diff --git a/src/pystencilssfg/composer/class_composer.py b/src/pystencilssfg/composer/class_composer.py index 63588b7829b2a94122e5c9d7d38770cabce9d6f5..9b8fcfb13e5cfa84349275735cab8ee25f53771f 100644 --- a/src/pystencilssfg/composer/class_composer.py +++ b/src/pystencilssfg/composer/class_composer.py @@ -1,8 +1,10 @@ from __future__ import annotations from typing import Sequence +from pystencils import TypedSymbol from pystencils.types import PsCustomType, UserTypeSpec +from ..lang import AugExpr from ..ir import SfgCallTreeNode from ..ir.source_components import ( SfgClass, @@ -14,12 +16,18 @@ from ..ir.source_components import ( SfgClassKeyword, SfgVisibility, SfgVisibilityBlock, - SfgVar, ) from ..exceptions import SfgException from .mixin import SfgComposerMixIn -from .basic_composer import SfgNodeBuilder, make_sequence +from .basic_composer import ( + SfgNodeBuilder, + make_sequence, + _VarLike, + VarLike, + ExprLike, + _asvar, +) class SfgClassComposer(SfgComposerMixIn): @@ -46,7 +54,7 @@ class SfgClassComposer(SfgComposerMixIn): def __call__( self, *args: ( - SfgClassMember | SfgClassComposer.ConstructorBuilder | SfgVar | str + SfgClassMember | SfgClassComposer.ConstructorBuilder | VarLike | str ), ): for arg in args: @@ -63,15 +71,21 @@ class SfgClassComposer(SfgComposerMixIn): Returned by `constructor`. """ - def __init__(self, *params: SfgVar): - self._params = params + def __init__(self, *params: VarLike): + self._params = tuple(_asvar(p) for p in params) self._initializers: list[str] = [] self._body: str | None = None - def init(self, initializer: str) -> SfgClassComposer.ConstructorBuilder: + def init(self, var: VarLike): """Add an initialization expression to the constructor's initializer list.""" - self._initializers.append(initializer) - return self + + def init_sequencer(expr: ExprLike): + expr = str(expr) + initializer = f"{_asvar(var)}{{ {expr} }}" + self._initializers.append(initializer) + return self + + return init_sequencer def body(self, body: str): """Define the constructor body""" @@ -120,7 +134,7 @@ class SfgClassComposer(SfgComposerMixIn): """Create a `private` visibility block in a class or struct body""" return SfgClassComposer.VisibilityContext(SfgVisibility.PRIVATE) - def constructor(self, *params: SfgVar): + def constructor(self, *params: VarLike): """In a class or struct body or visibility block, add a constructor. Args: @@ -171,7 +185,7 @@ class SfgClassComposer(SfgComposerMixIn): SfgClassComposer.VisibilityContext | SfgClassMember | SfgClassComposer.ConstructorBuilder - | SfgVar + | VarLike | str ), ): @@ -186,9 +200,9 @@ class SfgClassComposer(SfgComposerMixIn): ( SfgClassMember, SfgClassComposer.ConstructorBuilder, - SfgVar, str, - ), + ) + + _VarLike, ): if default_ended: raise SfgException( @@ -204,13 +218,17 @@ class SfgClassComposer(SfgComposerMixIn): @staticmethod def _resolve_member( - arg: SfgClassMember | SfgClassComposer.ConstructorBuilder | SfgVar | str, - ): - if isinstance(arg, SfgVar): - return SfgMemberVariable(arg.name, arg.dtype) - elif isinstance(arg, str): - return SfgInClassDefinition(arg) - elif isinstance(arg, SfgClassComposer.ConstructorBuilder): - return arg.resolve() - else: - return arg + arg: SfgClassMember | SfgClassComposer.ConstructorBuilder | VarLike | str, + ) -> SfgClassMember: + match arg: + case AugExpr() | TypedSymbol(): + var = _asvar(arg) + return SfgMemberVariable(var.name, var.dtype) + case str(): + return SfgInClassDefinition(arg) + case SfgClassComposer.ConstructorBuilder(): + return arg.resolve() + case SfgClassMember(): + return arg + case _: + raise ValueError(f"Invalid class member: {arg}") diff --git a/src/pystencilssfg/lang/expressions.py b/src/pystencilssfg/lang/expressions.py index 579646f3ee5b96dd3a57c1ece45c6b6407c48102..ca5a78c4d0794d33a2b22827b7a5e9d5a09e47d4 100644 --- a/src/pystencilssfg/lang/expressions.py +++ b/src/pystencilssfg/lang/expressions.py @@ -48,7 +48,7 @@ class DependentExpression: def __add__(self, other: DependentExpression): return DependentExpression(self.expr + other.expr, self.depends | other.depends) - + class VarExpr(DependentExpression): def __init__(self, var: SfgVar): @@ -61,6 +61,8 @@ class VarExpr(DependentExpression): class AugExpr: + __match_args__ = ("expr", "dtype") + def __init__(self, dtype: PsType | None = None): self._dtype = dtype self._bound: DependentExpression | None = None @@ -77,6 +79,7 @@ class AugExpr: @staticmethod def format(fmt: str, *deps, **kwdeps) -> AugExpr: + """Create a new `AugExpr` by combining existing expressions.""" return AugExpr().bind(fmt, *deps, **kwdeps) def bind(self, fmt: str, *deps, **kwdeps): @@ -109,11 +112,11 @@ class AugExpr: raise SfgException("This AugExpr has no known data type.") return self._dtype - + @property def is_variable(self) -> bool: return isinstance(self._bound, VarExpr) - + def as_variable(self) -> SfgVar: if not isinstance(self._bound, VarExpr): raise SfgException("This expression is not a variable") diff --git a/tests/integration/expected/Variables.h b/tests/integration/expected/Variables.h new file mode 100644 index 0000000000000000000000000000000000000000..96c16d7b306fe7594db0caa02e9168b2f5c86fc6 --- /dev/null +++ b/tests/integration/expected/Variables.h @@ -0,0 +1,13 @@ +#pragma once + +#include <cstdint> + +#define RESTRICT __restrict__ + +class Scale { +private: + float alpha; +public: + Scale(float alpha) : alpha{ alpha } {} + void operator() (float *const _data_f, float *const _data_g); +}; diff --git a/tests/integration/scripts/SimpleJacobi.py b/tests/integration/scripts/SimpleJacobi.py index 199419c541fb4aef68d44a1baf7cfc2f28d67e86..e84c872b05c354f4e1473b2eab17781f0880f035 100644 --- a/tests/integration/scripts/SimpleJacobi.py +++ b/tests/integration/scripts/SimpleJacobi.py @@ -20,4 +20,4 @@ with SourceFileGenerator() as sfg: sfg.map_field(u_dst, mdspan_ref(u_dst)), sfg.map_field(f, mdspan_ref(f)), sfg.call(poisson_kernel) - ) \ No newline at end of file + ) diff --git a/tests/integration/scripts/Variables.py b/tests/integration/scripts/Variables.py new file mode 100644 index 0000000000000000000000000000000000000000..9fd4e0027104451738abea72e15084047cf4465e --- /dev/null +++ b/tests/integration/scripts/Variables.py @@ -0,0 +1,22 @@ +import sympy as sp +from pystencils import TypedSymbol, fields, kernel + +from pystencilssfg import SourceFileGenerator, SfgConfiguration + +with SourceFileGenerator() as sfg: + α = TypedSymbol("alpha", "float32") + f, g = fields("f, g: float32[10]") + + @kernel + def scale(): + f[0] @= α * g.center() + + khandle = sfg.kernels.create(scale) + + sfg.klass("Scale")( + sfg.private(α), + sfg.public( + sfg.constructor(α).init(α)(α.name), + sfg.method("operator()")(sfg.init(α)(f"this->{α}"), sfg.call(khandle)), + ), + ) diff --git a/tests/integration/test_generator_scripts.py b/tests/integration/test_generator_scripts.py index 33f747820b966bd7a2630e9497cbb00a7937340c..466dbf34ca6f622442090d7e7001e437a8260156 100644 --- a/tests/integration/test_generator_scripts.py +++ b/tests/integration/test_generator_scripts.py @@ -15,20 +15,45 @@ EXPECTED_DIR = path.join(THIS_DIR, "expected") @dataclass class ScriptInfo: script_name: str + """Name of the generator script, without .py-extension. + + Generator scripts must be located in the ``scripts`` folder. + """ + expected_outputs: tuple[str, ...] + """List of file extensions expected to be emitted by the generator script. + + Output files will all be placed in the ``out`` folder. + """ compilable_output: str | None = None + """File extension of the output file that can be compiled. + + If this is set, and the expected file exists, the ``compile_cmd`` will be + executed to check for error-free compilation of the output. + """ + compile_cmd: str = f"g++ --std=c++17 -I {THIS_DIR}/deps/mdspan/include" + """Command to be invoked to compile the generated source file.""" SCRIPTS = [ ScriptInfo("SimpleJacobi", ("h", "cpp"), compilable_output="cpp"), ScriptInfo("SimpleClasses", ("h", "cpp")), + ScriptInfo("Variables", ("h", "cpp"), compilable_output="cpp"), ] @pytest.mark.parametrize("script_info", SCRIPTS) def test_generator_script(script_info: ScriptInfo): + """Test a generator script defined by ``script_info``. + + The generator script will be run, with its output placed in the ``out`` folder. + If it is successful, its output files will be compared against + any files of the same name from the ``expected`` folder. + Finally, if any compilable files are specified, the test will attempt to compile them. + """ + script_name = script_info.script_name script_file = path.join(SCRIPTS_DIR, script_name + ".py")