Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • fhennig/devel
  • master
  • rangersbach/c-interfacing
  • v0.1a1
  • v0.1a2
  • v0.1a3
  • v0.1a4
7 results

Target

Select target project
  • ob28imeq/pystencils-sfg
  • brendan-waters/pystencils-sfg
  • pycodegen/pystencils-sfg
3 results
Select Git revision
  • frontend-cleanup
  • lbwelding-features
  • master
  • refactor-indexing-params
  • unit_tests
  • v0.1a1
  • v0.1a2
  • v0.1a3
  • v0.1a4
9 results
Show changes
Showing
with 660 additions and 985 deletions
from __future__ import annotations
from typing import TYPE_CHECKING, Union, TypeAlias
from abc import ABC, abstractmethod
from pystencils import TypedSymbol, Field
from pystencils.typing import FieldPointerSymbol, FieldStrideSymbol, FieldShapeSymbol
from ..types import SrcType
if TYPE_CHECKING:
from ..source_components import SfgHeaderInclude
from ..tree import SfgStatements, SfgSequence
class SrcObject:
"""C/C++ object of nonprimitive type.
Two objects are identical if they have the same identifier and type string."""
def __init__(self, identifier: str, src_type: SrcType):
self._identifier = identifier
self._src_type = src_type
@property
def identifier(self):
return self._identifier
@property
def name(self) -> str:
"""For interface compatibility with ps.TypedSymbol"""
return self._identifier
@property
def dtype(self):
return self._src_type
@property
def required_includes(self) -> set[SfgHeaderInclude]:
return set()
def __hash__(self) -> int:
return hash((self._identifier, self._src_type))
def __eq__(self, other: object) -> bool:
return (
isinstance(other, SrcObject)
and self._identifier == other._identifier
and self._src_type == other._src_type
)
def __str__(self) -> str:
return self.name
TypedSymbolOrObject: TypeAlias = TypedSymbol | SrcObject
class SrcField(SrcObject, ABC):
"""Represents a C++ data structure that can be mapped to a *pystencils* field.
Subclasses of `SrcField` are meant to be used in [SfgComposer.map_field][pystencilssfg.SfgComposer.map_field]
to produce the necessary mapping code from a high-level C++ field data structure to a pystencils field.
Subclasses of `SrcField` must implement `extract_ptr`, `extract_size` and `extract_stride`
to emit code extracting field pointers and indexing information from the high-level concept.
Currently, *pystencils-sfg* provides an implementation for the C++ `std::vector` and `std::mdspan` classes via
[StdVector][pystencilssfg.source_concepts.cpp.StdVector] and
[StdMdspan][pystencilssfg.source_concepts.cpp.StdMdspan].
"""
def __init__(self, identifier: str, src_type: SrcType):
super().__init__(identifier, src_type)
@abstractmethod
def extract_ptr(self, ptr_symbol: FieldPointerSymbol) -> SfgStatements:
pass
@abstractmethod
def extract_size(
self, coordinate: int, size: Union[int, FieldShapeSymbol]
) -> SfgStatements:
pass
@abstractmethod
def extract_stride(
self, coordinate: int, stride: Union[int, FieldStrideSymbol]
) -> SfgStatements:
pass
def extract_parameters(self, field: Field) -> SfgSequence:
ptr = FieldPointerSymbol(field.name, field.dtype, False)
from ..composer import make_sequence
return make_sequence(
self.extract_ptr(ptr),
*(self.extract_size(c, s) for c, s in enumerate(field.shape)),
*(self.extract_stride(c, s) for c, s in enumerate(field.strides)),
)
class SrcVector(SrcObject, ABC):
"""Represents a C++ abstraction of a mathematical vector that can be mapped to a vector of symbols.
Subclasses of `SrcVector` are meant to be used in [SfgComposer.map_vector][pystencilssfg.SfgComposer.map_vector]
to produce the necessary mapping code from a high-level C++ vector data structure to a vector of symbols.
Subclasses of `SrcVector` must implement `extract_component` to emit code extracting scalar values
from the high-level vector.
Currently, *pystencils-sfg* provides an implementation for the C++ `std::vector` via
[StdVector][pystencilssfg.source_concepts.cpp.StdVector].
"""
@abstractmethod
def extract_component(
self, destination: TypedSymbolOrObject, coordinate: int
) -> SfgStatements:
pass
from .basic_nodes import (
SfgCallTreeNode,
SfgCallTreeLeaf,
SfgEmptyNode,
SfgKernelCallNode,
SfgBlock,
SfgSequence,
SfgStatements,
SfgFunctionParams,
SfgRequireIncludes
)
from .conditional import SfgBranch, SfgCondition, IntEven, IntOdd
__all__ = [
"SfgCallTreeNode",
"SfgCallTreeLeaf",
"SfgEmptyNode",
"SfgKernelCallNode",
"SfgSequence",
"SfgBlock",
"SfgStatements",
"SfgFunctionParams",
"SfgRequireIncludes",
"SfgCondition",
"SfgBranch",
"IntEven",
"IntOdd",
]
from __future__ import annotations
from typing import TYPE_CHECKING, Sequence
from abc import ABC, abstractmethod
from itertools import chain
from ..source_components import SfgHeaderInclude, SfgKernelHandle
from ..source_concepts.source_objects import SrcObject, TypedSymbolOrObject
if TYPE_CHECKING:
from ..context import SfgContext
class SfgCallTreeNode(ABC):
"""Base class for all nodes comprising SFG call trees.
## Code Printing
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, *children: SfgCallTreeNode):
self._children = list(children)
@property
def children(self) -> tuple[SfgCallTreeNode, ...]:
"""This node's children"""
return tuple(self._children)
@children.setter
def children(self, cs: Sequence[SfgCallTreeNode]) -> None:
"""Replaces this node's children. By default, the number of child nodes must not change."""
if len(cs) != len(self._children):
raise ValueError("The number of child nodes must remain the same!")
self._children = list(cs)
def child(self, idx: int) -> SfgCallTreeNode:
"""Gets the child at index idx."""
return self._children[idx]
def set_child(self, idx: int, c: SfgCallTreeNode):
"""Replaces the child at index idx."""
self._children[idx] = c
def __getitem__(self, idx: int) -> SfgCallTreeNode:
return self.child(idx)
def __setitem__(self, idx: int, c: SfgCallTreeNode) -> None:
self.set_child(idx, c)
@abstractmethod
def get_code(self, ctx: SfgContext) -> str:
"""Returns the code of this node.
By convention, the code block emitted by this function should not contain a trailing newline.
"""
@property
def required_includes(self) -> set[SfgHeaderInclude]:
"""Return a set of header includes required by this node"""
return set()
class SfgCallTreeLeaf(SfgCallTreeNode, ABC):
"""A leaf node of the call tree.
Leaf nodes must implement `required_parameters` for automatic parameter collection.
"""
@property
@abstractmethod
def required_parameters(self) -> set[TypedSymbolOrObject]:
...
class SfgEmptyNode(SfgCallTreeLeaf):
"""A leaf node that does not emit any code.
Empty nodes must still implement `required_parameters`.
"""
def __init__(self):
super().__init__()
def get_code(self, ctx: SfgContext) -> str:
return ""
class SfgStatements(SfgCallTreeLeaf):
"""Represents (a sequence of) statements in the source language.
This class groups together arbitrary code strings
(e.g. sequences of C++ statements, cf. https://en.cppreference.com/w/cpp/language/statements),
and annotates them with the set of symbols read and written by these statements.
It is the user's responsibility to ensure that the code string is valid code in the output language,
and that the lists of required and defined objects are correct and complete.
Args:
code_string: Code to be printed out.
defined_params: Objects (as `SrcObject` or `TypedSymbol`) that will be newly defined and visible to
code in sequence after these statements.
required_params: Objects (as `SrcObject` or `TypedSymbol`) that are required as input to these statements.
"""
def __init__(
self,
code_string: str,
defined_params: Sequence[TypedSymbolOrObject],
required_params: Sequence[TypedSymbolOrObject],
):
super().__init__()
self._code_string = code_string
self._defined_params = set(defined_params)
self._required_params = set(required_params)
self._required_includes = set()
for obj in chain(required_params, defined_params):
if isinstance(obj, SrcObject):
self._required_includes |= obj.required_includes
@property
def required_parameters(self) -> set[TypedSymbolOrObject]:
return self._required_params
@property
def defined_parameters(self) -> set[TypedSymbolOrObject]:
return self._defined_params
@property
def required_includes(self) -> set[SfgHeaderInclude]:
return self._required_includes
def get_code(self, ctx: SfgContext) -> str:
return self._code_string
class SfgFunctionParams(SfgEmptyNode):
def __init__(self, parameters: Sequence[TypedSymbolOrObject]):
super().__init__()
self._params = set(parameters)
self._required_includes = set()
for obj in parameters:
if isinstance(obj, SrcObject):
self._required_includes |= obj.required_includes
@property
def required_parameters(self) -> set[TypedSymbolOrObject]:
return self._params
@property
def required_includes(self) -> set[SfgHeaderInclude]:
return self._required_includes
class SfgRequireIncludes(SfgEmptyNode):
def __init__(self, includes: Sequence[SfgHeaderInclude]):
super().__init__()
self._required_includes = set(includes)
@property
def required_parameters(self) -> set[TypedSymbolOrObject]:
return set()
@property
def required_includes(self) -> set[SfgHeaderInclude]:
return self._required_includes
class SfgSequence(SfgCallTreeNode):
def __init__(self, children: Sequence[SfgCallTreeNode]):
super().__init__(*children)
def get_code(self, ctx: SfgContext) -> str:
return "\n".join(c.get_code(ctx) for c in self._children)
class SfgBlock(SfgCallTreeNode):
def __init__(self, subtree: SfgCallTreeNode):
super().__init__(subtree)
@property
def subtree(self) -> SfgCallTreeNode:
return self._children[0]
def get_code(self, ctx: SfgContext) -> str:
subtree_code = ctx.codestyle.indent(self.subtree.get_code(ctx))
return "{\n" + subtree_code + "\n}"
# class SfgForLoop(SfgCallTreeNode):
# def __init__(self, control_line: SfgStatements, body: SfgCallTreeNode):
# super().__init__(control_line, body)
# @property
# def body(self) -> SfgStatements:
# return cast(SfgStatements)
class SfgKernelCallNode(SfgCallTreeLeaf):
def __init__(self, kernel_handle: SfgKernelHandle):
super().__init__()
self._kernel_handle = kernel_handle
@property
def required_parameters(self) -> set[TypedSymbolOrObject]:
return set(p.symbol for p in self._kernel_handle.parameters)
def get_code(self, ctx: SfgContext) -> str:
ast_params = self._kernel_handle.parameters
fnc_name = self._kernel_handle.fully_qualified_name
call_parameters = ", ".join([p.symbol.name for p in ast_params])
return f"{fnc_name}({call_parameters});"
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, cast, Generator, Sequence, NewType
from pystencils.typing import TypedSymbol, BasicType
from .basic_nodes import SfgCallTreeNode, SfgCallTreeLeaf
from ..source_concepts.source_objects import TypedSymbolOrObject
if TYPE_CHECKING:
from ..context import SfgContext
class SfgCondition(SfgCallTreeLeaf):
pass
class SfgCustomCondition(SfgCondition):
def __init__(self, cond_text: str):
super().__init__()
self._cond_text = cond_text
@property
def required_parameters(self) -> set[TypedSymbolOrObject]:
return set()
def get_code(self, ctx: SfgContext) -> str:
return self._cond_text
class IntEven(SfgCondition):
def __init__(self, symbol: TypedSymbol):
super().__init__()
if not isinstance(symbol.dtype, BasicType) or not symbol.dtype.is_int():
raise ValueError(f"Symbol {symbol} does not have integer type.")
self._symbol = symbol
@property
def required_parameters(self) -> set[TypedSymbolOrObject]:
return {self._symbol}
def get_code(self, ctx: SfgContext) -> str:
return f"(({self._symbol.name} & 1) ^ 1)"
class IntOdd(SfgCondition):
def __init__(self, symbol: TypedSymbol):
super().__init__()
if not isinstance(symbol.dtype, BasicType) or not symbol.dtype.is_int():
raise ValueError(f"Symbol {symbol} does not have integer type.")
self._symbol = symbol
@property
def required_parameters(self) -> set[TypedSymbolOrObject]:
return {self._symbol}
def get_code(self, ctx: SfgContext) -> str:
return f"({self._symbol.name} & 1)"
class SfgBranch(SfgCallTreeNode):
def __init__(
self,
cond: SfgCondition,
branch_true: SfgCallTreeNode,
branch_false: Optional[SfgCallTreeNode] = None,
):
super().__init__(cond, branch_true, *((branch_false,) if branch_false else ()))
@property
def condition(self) -> SfgCondition:
return cast(SfgCondition, self._children[0])
@property
def branch_true(self) -> SfgCallTreeNode:
return self._children[1]
@property
def branch_false(self) -> SfgCallTreeNode:
return self._children[2]
def get_code(self, ctx: SfgContext) -> str:
code = f"if({self.condition.get_code(ctx)}) {{\n"
code += ctx.codestyle.indent(self.branch_true.get_code(ctx))
code += "\n}"
if self.branch_false is not None:
code += "else {\n"
code += ctx.codestyle.indent(self.branch_false.get_code(ctx))
code += "\n}"
return code
class SfgSwitchCase(SfgCallTreeNode):
DefaultCaseType = NewType("DefaultCaseType", object)
Default = DefaultCaseType(object())
def __init__(self, label: str | DefaultCaseType, body: SfgCallTreeNode):
self._label = label
super().__init__(body)
@property
def label(self) -> str | DefaultCaseType:
return self._label
@property
def body(self) -> SfgCallTreeNode:
return self._children[0]
@property
def is_default(self) -> bool:
return self._label == SfgSwitchCase.Default
def get_code(self, ctx: SfgContext) -> str:
code = ""
if self._label == SfgSwitchCase.Default:
code += "default: {\n"
else:
code += f"case {self._label}: {{\n"
code += ctx.codestyle.indent(self.body.get_code(ctx))
code += "\nbreak;\n}"
return code
class SfgSwitch(SfgCallTreeNode):
def __init__(
self,
switch_arg: str | TypedSymbolOrObject,
cases_dict: dict[str, SfgCallTreeNode],
default: SfgCallTreeNode | None = None,
):
children = [SfgSwitchCase(label, body) for label, body in cases_dict.items()]
if default is not None:
# invariant: the default case is always the last child
children += [SfgSwitchCase(SfgSwitchCase.Default, default)]
self._switch_arg = switch_arg
self._default = default
super().__init__(*children)
@property
def switch_arg(self) -> str | TypedSymbolOrObject:
return self._switch_arg
def cases(self) -> Generator[SfgCallTreeNode, None, None]:
if self._default is not None:
yield from self._children[:-1]
else:
yield from self._children
@property
def default(self) -> SfgCallTreeNode | None:
return self._default
@property
def children(self) -> tuple[SfgCallTreeNode, ...]:
return tuple(self._children)
@children.setter
def children(self, cs: Sequence[SfgCallTreeNode]) -> None:
if len(cs) != len(self._children):
raise ValueError("The number of child nodes must remain the same!")
self._default = None
for i, c in enumerate(cs):
if not isinstance(c, SfgSwitchCase):
raise ValueError(
"An SfgSwitch node can only have SfgSwitchCases as children."
)
if c.is_default:
if i != len(cs) - 1:
raise ValueError("Default case must be listed last.")
else:
self._default = c
self._children = list(cs)
def set_child(self, idx: int, c: SfgCallTreeNode):
if not isinstance(c, SfgSwitchCase):
raise ValueError(
"An SfgSwitch node can only have SfgSwitchCases as children."
)
if c.is_default:
if idx != len(self._children) - 1:
raise ValueError("Default case must be the last child.")
elif self._default is None:
raise ValueError("Cannot replace normal case with default case.")
else:
self._default = c
self._children[-1] = c
else:
self._children[idx] = c
def get_code(self, ctx: SfgContext) -> str:
code = f"switch({self._switch_arg}) {{\n"
code += "\n".join(c.get_code(ctx) for c in self.children)
code += "}"
return code
from __future__ import annotations
from typing import TYPE_CHECKING
from abc import ABC, abstractmethod
from pystencils import Field
from pystencils.typing import FieldPointerSymbol, FieldShapeSymbol, FieldStrideSymbol
from ..exceptions import SfgException
from .basic_nodes import SfgCallTreeNode, SfgSequence
from ..source_concepts import SrcField
from ..source_concepts.source_objects import TypedSymbolOrObject
if TYPE_CHECKING:
from ..context import SfgContext
class SfgDeferredNode(SfgCallTreeNode, ABC):
"""Nodes of this type are inserted as placeholders into the kernel call tree
and need to be expanded at a later time.
Subclasses of SfgDeferredNode correspond to nodes that cannot be created yet
because information required for their construction is not yet known.
"""
class InvalidAccess:
def __get__(self):
raise SfgException("Invalid access into deferred node; deferred nodes must be expanded first.")
def __init__(self):
self._children = SfgDeferredNode.InvalidAccess
def get_code(self, ctx: SfgContext) -> str:
raise SfgException("Invalid access into deferred node; deferred nodes must be expanded first.")
class SfgParamCollectionDeferredNode(SfgDeferredNode, ABC):
@abstractmethod
def expand(self, visible_params: set[TypedSymbolOrObject]) -> SfgCallTreeNode:
...
class SfgDeferredFieldMapping(SfgParamCollectionDeferredNode):
def __init__(self, field: Field, src_field: SrcField):
self._field = field
self._src_field = src_field
def expand(self, visible_params: set[TypedSymbolOrObject]) -> SfgCallTreeNode:
# Find field pointer
ptr = None
for param in visible_params:
if isinstance(param, FieldPointerSymbol) and param.field_name == self._field.name:
if param.dtype.base_type != self._field.dtype:
raise SfgException("Data type mismatch between field and encountered pointer symbol")
ptr = param
# Find required sizes
shape = []
for c, s in enumerate(self._field.shape):
if isinstance(s, FieldShapeSymbol) and s not in visible_params:
continue
else:
shape.append((c, s))
# Find required strides
strides = []
for c, s in enumerate(self._field.strides):
if isinstance(s, FieldStrideSymbol) and s not in visible_params:
continue
else:
strides.append((c, s))
nodes = []
if ptr is not None:
nodes += [self._src_field.extract_ptr(ptr)]
nodes += [self._src_field.extract_size(c, s) for c, s in shape]
nodes += [self._src_field.extract_stride(c, s) for c, s in strides]
return SfgSequence(nodes)
from typing import Union, TypeAlias, NewType
import numpy as np
from pystencils.typing import AbstractType, numpy_name_to_c
PsType: TypeAlias = Union[type, np.dtype, AbstractType]
"""Types used in interacting with pystencils.
PsType represents various ways of specifying types within pystencils.
In particular, it encompasses most ways to construct an instance of `AbstractType`,
for example via `create_type`.
(Note that, while `create_type` does accept strings, they are excluded here for
reasons of safety. It is discouraged to use strings for type specifications when working
with pystencils!)
PsType is a temporary solution and will be removed in the future
in favor of the consolidated pystencils backend typing system.
"""
SrcType = NewType('SrcType', str)
"""C/C++-Types occuring during source file generation.
When necessary, the SFG package checks equality of types by their name strings; it does
not care about typedefs, aliases, namespaces, etc!
SrcType is a temporary solution and will be removed in the future
in favor of the consolidated pystencils backend typing system.
"""
def cpp_typename(type_obj: Union[str, SrcType, PsType]):
"""Accepts type specifications in various ways and returns a valid typename to be used in code."""
# if isinstance(type_obj, str):
# return type_obj
if isinstance(type_obj, str):
return type_obj
elif isinstance(type_obj, AbstractType):
return str(type_obj)
elif isinstance(type_obj, np.dtype) or isinstance(type_obj, type):
return numpy_name_to_c(np.dtype(type_obj).name)
else:
raise ValueError(f"Don't know how to interpret type object {type_obj}.")
from .dispatcher import visitor
from .collectors import CollectIncludes
from .tree_visitors import FlattenSequences, ExpandingParameterCollector
__all__ = [
"visitor",
"CollectIncludes",
"FlattenSequences",
"ExpandingParameterCollector",
]
from __future__ import annotations
from typing import TYPE_CHECKING
from functools import reduce
from .dispatcher import visitor
from ..exceptions import SfgException
from ..tree import SfgCallTreeNode
from ..source_components import (
SfgFunction,
SfgClass,
SfgConstructor,
SfgMemberVariable,
SfgInClassDefinition,
)
from ..context import SfgContext
if TYPE_CHECKING:
from ..source_components import SfgHeaderInclude
class CollectIncludes:
@visitor
def visit(self, obj: object) -> set[SfgHeaderInclude]:
raise SfgException(f"Can't collect includes from object of type {type(obj)}")
@visit.case(SfgContext)
def context(self, ctx: SfgContext) -> set[SfgHeaderInclude]:
includes = set()
for func in ctx.functions():
includes |= self.visit(func)
for cls in ctx.classes():
includes |= self.visit(cls)
return includes
@visit.case(SfgCallTreeNode)
def tree_node(self, node: SfgCallTreeNode) -> set[SfgHeaderInclude]:
return reduce(
lambda accu, child: accu | self.visit(child),
node.children,
node.required_includes,
)
@visit.case(SfgFunction)
def sfg_function(self, func: SfgFunction) -> set[SfgHeaderInclude]:
return self.visit(func.tree)
@visit.case(SfgClass)
def sfg_class(self, cls: SfgClass) -> set[SfgHeaderInclude]:
return reduce(
lambda accu, member: accu | (self.visit(member)), cls.members(), set()
)
@visit.case(SfgConstructor)
def sfg_constructor(self, constr: SfgConstructor) -> set[SfgHeaderInclude]:
return reduce(
lambda accu, obj: accu | obj.required_includes, constr.parameters, set()
)
@visit.case(SfgMemberVariable)
def sfg_member_var(self, var: SfgMemberVariable) -> set[SfgHeaderInclude]:
return var.required_includes
@visit.case(SfgInClassDefinition)
def sfg_cls_def(self, _: SfgInClassDefinition) -> set[SfgHeaderInclude]:
return set()
from __future__ import annotations
from typing import Callable, TypeVar, Generic
from types import MethodType
from functools import wraps
V = TypeVar("V")
R = TypeVar("R")
class VisitorDispatcher(Generic[V, R]):
def __init__(self, wrapped_method: Callable[..., R]):
self._dispatch_dict: dict[type, Callable[..., R]] = {}
self._wrapped_method: Callable[..., R] = wrapped_method
def case(self, node_type: type):
"""Decorator for visitor's case handlers."""
def decorate(handler: Callable[..., R]):
if node_type in self._dispatch_dict:
raise ValueError(f"Duplicate visitor case {node_type}")
self._dispatch_dict[node_type] = handler
return handler
return decorate
def __call__(self, instance: V, node: object, *args, **kwargs) -> R:
for cls in node.__class__.mro():
if cls in self._dispatch_dict:
return self._dispatch_dict[cls](instance, node, *args, **kwargs)
return self._wrapped_method(instance, node, *args, **kwargs)
def __get__(self, obj: V, objtype=None) -> Callable[..., R]:
if obj is None:
return self
return MethodType(self, obj)
def visitor(method):
"""Decorator to create a visitor using type-based dispatch.
Use this decorator to convert a method into a visitor, like shown below.
After declaring a method (e.g. `my_method`) a visitor,
its case handlers can be declared using the `my_method.case` decorator, like this:
```Python
class DemoVisitor:
@visitor
def visit(self, obj: object):
# fallback case
...
@visit.case(str)
def visit_str(self, obj: str):
# code for handling a str
```
When `visit` is later called with some object `x`, the case handler to be executed is
determined according to the method resolution order of `x` (i.e. along its type's inheritance hierarchy).
If no case matches, the fallback code in the original visitor method is executed.
In this example, if `visit` is called with an object of type `str`, the call is dispatched to `visit_str`.
This visitor dispatch method is primarily designed for traversing abstract syntax tree structures.
The primary visitor method (`visit` in above example) should define the common parent type of all object
types the visitor can handle, with cases declared for all required subtypes.
However, this type relationship is not enforced at runtime.
"""
return wraps(method)(VisitorDispatcher(method))
from __future__ import annotations
# from typing import TYPE_CHECKING
from functools import reduce
from ..tree.basic_nodes import (
SfgCallTreeNode,
SfgCallTreeLeaf,
SfgSequence,
SfgStatements,
)
from ..tree.deferred_nodes import SfgParamCollectionDeferredNode
from .dispatcher import visitor
from ..source_concepts.source_objects import TypedSymbolOrObject
class FlattenSequences:
"""Flattens any nested sequences occuring in a kernel call tree."""
@visitor
def visit(self, node: SfgCallTreeNode) -> None:
for c in node.children:
self.visit(c)
@visit.case(SfgSequence)
def sequence(self, sequence: SfgSequence) -> None:
children_flattened: list[SfgCallTreeNode] = []
def flatten(seq: SfgSequence):
for c in seq.children:
if isinstance(c, SfgSequence):
flatten(c)
else:
children_flattened.append(c)
flatten(sequence)
for c in children_flattened:
self.visit(c)
sequence._children = children_flattened
class ExpandingParameterCollector:
"""Collects all parameters required but not defined in a kernel call tree.
Expands any deferred nodes of type `SfgParamCollectionDeferredNode` found within sequences on the way.
"""
def __init__(self) -> None:
self._flattener = FlattenSequences()
@visitor
def visit(self, node: SfgCallTreeNode) -> set[TypedSymbolOrObject]:
return self.branching_node(node)
@visit.case(SfgCallTreeLeaf)
def leaf(self, leaf: SfgCallTreeLeaf) -> set[TypedSymbolOrObject]:
return leaf.required_parameters
@visit.case(SfgSequence)
def sequence(self, sequence: SfgSequence) -> set[TypedSymbolOrObject]:
"""
Only in a sequence may parameters be defined and visible to subsequent nodes.
"""
params: set[TypedSymbolOrObject] = set()
def iter_nested_sequences(
seq: SfgSequence, visible_params: set[TypedSymbolOrObject]
):
for i in range(len(seq.children) - 1, -1, -1):
c = seq.children[i]
if isinstance(c, SfgParamCollectionDeferredNode):
c = c.expand(visible_params=visible_params)
seq[i] = c
if isinstance(c, SfgSequence):
iter_nested_sequences(c, visible_params)
else:
if isinstance(c, SfgStatements):
visible_params -= c.defined_parameters
visible_params |= self.visit(c)
iter_nested_sequences(sequence, params)
return params
def branching_node(self, node: SfgCallTreeNode) -> set[TypedSymbolOrObject]:
"""
Each interior node that is not a sequence simply requires the union of all parameters
required by its children.
"""
return reduce(lambda x, y: x | y, (self.visit(c) for c in node.children), set())
class ParameterCollector:
"""Collects all parameters required but not defined in a kernel call tree.
Requires that all sequences in the tree are flattened.
"""
@visitor
def visit(self, node: SfgCallTreeNode) -> set[TypedSymbolOrObject]:
return self.branching_node(node)
@visit.case(SfgCallTreeLeaf)
def leaf(self, leaf: SfgCallTreeLeaf) -> set[TypedSymbolOrObject]:
return leaf.required_parameters
@visit.case(SfgSequence)
def sequence(self, sequence: SfgSequence) -> set[TypedSymbolOrObject]:
"""
Only in a sequence may parameters be defined and visible to subsequent nodes.
"""
params: set[TypedSymbolOrObject] = set()
for c in sequence.children[::-1]:
if isinstance(c, SfgStatements):
params -= c.defined_parameters
assert not isinstance(c, SfgSequence), "Sequence not flattened."
params |= self.visit(c)
return params
def branching_node(self, node: SfgCallTreeNode) -> set[TypedSymbolOrObject]:
"""
Each interior node that is not a sequence simply requires the union of all parameters
required by its children.
"""
return reduce(lambda x, y: x | y, (self.visit(c) for c in node.children), set())
cmake_minimum_required( VERSION 3.24 )
project(PystencilsSfg_Standalone)
if (DEFINED CACHE{PystencilsSfg_PYTHON_INTERPRETER})
set( _use_venv_init OFF)
elseif(DEFINED PystencilsSfg_PYTHON_PATH)
set( _use_venv_init OFF)
else()
set( _use_venv_init ON )
endif()
set(CODEGEN_PRIVATE_VENV ${_use_venv_init}
CACHE BOOL
"Create a private virtual Python environment inside the build tree for code generation"
)
function(codegen_venv_install)
if(NOT CODEGEN_PRIVATE_VENV)
return()
endif()
if(NOT _sfg_private_venv_done)
message( FATAL_ERROR "The private virtual environment for code generation was not initialized yet" )
endif()
execute_process(
COMMAND ${PystencilsSfg_PYTHON_INTERPRETER} -m pip install ${ARGV}
)
endfunction()
if (CODEGEN_PRIVATE_VENV)
set(CODEGEN_VENV_PATH ${CMAKE_CURRENT_BINARY_DIR}/codegen-venv CACHE PATH "Location of the virtual environment used for code generation")
set(_venv_python_exe ${CODEGEN_VENV_PATH}/bin/python)
set(CODEGEN_VENV_PYTHON ${_venv_python_exe})
find_package( Python COMPONENTS Interpreter REQUIRED )
if(NOT _sfg_private_venv_done)
message( STATUS "Setting up Python virtual environment at ${CODEGEN_VENV_PATH}" )
# Create the venv and register its interpreter with pystencils-sfg
if(NOT EXISTS ${CODEGEN_VENV_PATH})
execute_process( COMMAND ${Python_EXECUTABLE} -m venv ${CODEGEN_VENV_PATH})
endif()
set(CODEGEN_VENV_REQUIREMENTS ${PROJECT_SOURCE_DIR}/requirements.txt CACHE FILEPATH "Location of the requirements installed in the virtual environment used for code generation")
if (EXISTS ${CODEGEN_VENV_REQUIREMENTS})
message( STATUS "Installing required Python packages from ${CODEGEN_VENV_REQUIREMENTS}" )
execute_process( COMMAND ${_venv_python_exe} -m pip install -r ${CODEGEN_VENV_REQUIREMENTS} OUTPUT_QUIET)
else()
message( WARNING "Could not find ${CODEGEN_VENV_REQUIREMENTS}" )
endif()
set( _sfg_private_venv_done TRUE CACHE BOOL "" )
mark_as_advanced(_sfg_private_venv_done)
endif()
set(_sfg_cache_python_init ${_venv_python_exe})
set(PystencilsSfg_PYTHON_INTERPRETER ${_sfg_cache_python_init} CACHE PATH "Path to the Python executable used to run pystencils-sfg")
endif()
# get the find pystencils-sfg file
execute_process(
COMMAND ${PystencilsSfg_PYTHON_INTERPRETER} -m pystencilssfg cmake make-find-module
WORKING_DIRECTORY ${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}
)
# renaming it
file(RENAME ${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/FindPystencilsSfg.cmake ${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/pystencilssfg-config.cmake)
# Find it
find_package( PystencilsSfg REQUIRED )
py-cpuinfo
# pystencils 2.0 Development Branch
git+https://i10git.cs.fau.de/pycodegen/pystencils.git@4876f0b774d38fe7644f2b64f80f1291ffa0fa08
# pystencils-sfg
git+https://i10git.cs.fau.de/pycodegen/pystencils-sfg.git@dcb77258670abe5bc87f2bd80761594eb304cb90
from pystencilssfg import SfgConfig
def configure_sfg(cfg: SfgConfig):
cfg.codestyle.indent_width = 3
cfg.clang_format.code_style = "llvm"
cfg.clang_format.skip = True
cfg.output_directory = "generated_sources"
cfg.outer_namespace = "myproject"
cfg.extensions.header = "hpp"
magic_string = "Spam and eggs"
magic_number = 0xcafe
def project_info():
return {
"use_openmp": True,
"use_cuda": True,
"float_format": "float32"
}
import pytest
import pystencilssfg.extensions.sycl as sycl
import pystencils as ps
def test_parallel_for_1_kernels(sfg):
sfg = sycl.SyclComposer(sfg)
data_type = "double"
dim = 2
f, g, h, i = ps.fields(f"f,g,h,i:{data_type}[{dim}D]")
assignement_1 = ps.Assignment(f.center(), g.center())
assignement_2 = ps.Assignment(h.center(), i.center())
config = ps.CreateKernelConfig(target=ps.Target.SYCL)
kernel_1 = sfg.kernels.create(assignement_1, "kernel_1", config)
kernel_2 = sfg.kernels.create(assignement_2, "kernel_2", config)
cgh = sfg.sycl_handler("handler")
rang = sfg.sycl_range(dim, "range")
cgh.parallel_for(rang)(
sfg.call(kernel_1),
sfg.call(kernel_2),
)
def test_parallel_for_2_kernels(sfg):
sfg = sycl.SyclComposer(sfg)
data_type = "double"
dim = 2
f, g, h, i = ps.fields(f"f,g,h,i:{data_type}[{dim}D]")
assignement_1 = ps.Assignment(f.center(), g.center())
assignement_2 = ps.Assignment(h.center(), i.center())
config = ps.CreateKernelConfig(target=ps.Target.SYCL)
kernel_1 = sfg.kernels.create(assignement_1, "kernel_1", config)
kernel_2 = sfg.kernels.create(assignement_2, "kernel_2", config)
cgh = sfg.sycl_handler("handler")
rang = sfg.sycl_range(dim, "range")
cgh.parallel_for(rang)(
sfg.call(kernel_1),
sfg.call(kernel_2),
)
def test_parallel_for_2_kernels_fail(sfg):
sfg = sycl.SyclComposer(sfg)
data_type = "double"
dim = 2
f, g = ps.fields(f"f,g:{data_type}[{dim}D]")
h, i = ps.fields(f"h,i:{data_type}[{dim-1}D]")
assignement_1 = ps.Assignment(f.center(), g.center())
assignement_2 = ps.Assignment(h.center(), i.center())
config = ps.CreateKernelConfig(target=ps.Target.SYCL)
kernel_1 = sfg.kernels.create(assignement_1, "kernel_1", config)
kernel_2 = sfg.kernels.create(assignement_2, "kernel_2", config)
cgh = sfg.sycl_handler("handler")
rang = sfg.sycl_range(dim, "range")
with pytest.raises(ValueError):
cgh.parallel_for(rang)(
sfg.call(kernel_1),
sfg.call(kernel_2),
)
import pytest
from pathlib import Path
from pystencilssfg.config import (
SfgConfig,
GLOBAL_NAMESPACE,
CommandLineParameters,
SfgConfigException
)
def test_defaults():
cfg = SfgConfig()
assert cfg.get_option("header_only") is False
assert cfg.extensions.get_option("header") == "hpp"
assert cfg.codestyle.get_option("indent_width") == 2
assert cfg.clang_format.get_option("binary") == "clang-format"
assert cfg.clang_format.get_option("code_style") == "file"
assert cfg.get_option("outer_namespace") is GLOBAL_NAMESPACE
cfg.extensions.impl = ".cu"
assert cfg.extensions.get_option("impl") == "cu"
# Check that section subobjects of different config objects are independent
# -> must use default_factory to construct them, because they are mutable!
cfg.clang_format.binary = "bogus"
cfg2 = SfgConfig()
assert cfg2.clang_format.binary is None
def test_override():
cfg1 = SfgConfig()
cfg1.outer_namespace = "test"
cfg1.extensions.header = "h"
cfg1.extensions.impl = "c"
cfg1.clang_format.force = True
cfg2 = SfgConfig()
cfg2.outer_namespace = GLOBAL_NAMESPACE
cfg2.extensions.header = "hpp"
cfg2.extensions.impl = "cpp"
cfg2.clang_format.binary = "bogus"
cfg1.override(cfg2)
assert cfg1.outer_namespace is GLOBAL_NAMESPACE
assert cfg1.extensions.header == "hpp"
assert cfg1.extensions.impl == "cpp"
assert cfg1.codestyle.indent_width is None
assert cfg1.clang_format.force is True
assert cfg1.clang_format.code_style is None
assert cfg1.clang_format.binary == "bogus"
def test_sanitation():
cfg = SfgConfig()
cfg.extensions.header = ".hxx"
assert cfg.extensions.header == "hxx"
cfg.extensions.header = ".cxx"
assert cfg.extensions.header == "cxx"
cfg.clang_format.force = True
with pytest.raises(SfgConfigException):
cfg.clang_format.skip = True
cfg.clang_format.force = False
cfg.clang_format.skip = True
with pytest.raises(SfgConfigException):
cfg.clang_format.force = True
def test_from_commandline(sample_config_module):
from argparse import ArgumentParser
parser = ArgumentParser()
CommandLineParameters.add_args_to_parser(parser)
args = parser.parse_args(
["--sfg-output-dir", ".out", "--sfg-file-extensions", ".h++,c++"]
)
cli_args = CommandLineParameters(args)
cfg = cli_args.get_config()
assert cfg.output_directory == Path(".out")
assert cfg.extensions.header == "h++"
assert cfg.extensions.impl == "c++"
assert cfg.header_only is None
args = parser.parse_args(
["--sfg-header-only"]
)
cli_args = CommandLineParameters(args)
cfg = cli_args.get_config()
assert cfg.header_only is True
args = parser.parse_args(
["--no-sfg-header-only"]
)
cli_args = CommandLineParameters(args)
cfg = cli_args.get_config()
assert cfg.header_only is False
args = parser.parse_args(
["--sfg-output-dir", "gen_sources", "--sfg-config-module", sample_config_module]
)
cli_args = CommandLineParameters(args)
cfg = cli_args.get_config()
assert cfg.codestyle.indent_width == 3
assert cfg.clang_format.code_style == "llvm"
assert cfg.clang_format.skip is True
assert (
cfg.output_directory == Path("gen_sources")
) # value from config module overridden by commandline
assert cfg.outer_namespace == "myproject"
assert cfg.extensions.header == "hpp"
assert cli_args.configuration_module is not None
assert cli_args.configuration_module.magic_string == "Spam and eggs"
assert cli_args.configuration_module.magic_number == 0xCAFE
deps/mdspan-*
*.lock
\ No newline at end of file
# Generator Script Test Suite
This directory contains the generator script test suite of pystencils-sfg.
Here, the code generation pipeline of the SFG is tested in full by running
and evaluating the output of generator scripts.
This has proven much more effective than trying to construct fine-grained
unit tests for the `composer`, `ir` and `emission` modules.
## Structure
The `pystencils-sfg/tests/generator-scripts` directory contains these subfolders and files:
- `deps`: Dependencies of the test suite, e.g. std::mdspan
- `source`: Generator scripts, their configuration and test harnesses
- `index.yaml`: Test suite index
- `test_generator_scripts.py`: Actual test suite code, run by pytest
## Registering Tests
A generator script test comprises at least a generator script `<name>.py` in the `source` directory,
and an associated entry in `index.yaml`.
That entry may define various options for the testing run of that script.
A test may optionally define a C++ harness program called `<name>.harness.cpp` in the `source` directory,
against which the generated code will be tested.
### Creating a New Test
At its top level, the test index file `index.yaml` is a dictionary mapping test names to their parameters.
After creating the `<name>.py` generator script, we register it by adding an empty entry:
```yaml
# somewhere in index.yaml
<name>:
# params (might be empty)
```
This will allow the test suite to discover `<name>.py` and add an associated test to its `pytest` test set.
The test can be parametrized using the parameters listed in the parameter reference below.
### Test Execution Flow
The above empty test definition already leads to the following execution flow:
- The generator script is executed and its output placed in a temporary directory.
If the script fails, so does the test.
- The set of output files is checked against the expected file set.
By default, scripts are expected to emit one `.hpp` file and one `.cpp` file,
but the expected files can be affected with test parameters as explained below.
- If any generated files are detected as 'compilable' from their extensions (candidates are `.cpp`, `.cxx`, and`.c++`).
the test suite will attempt to compile them using default settings
(currently `g++ -std=c++20`, with the `<experimental/mdspan>` header in scope).
If compilation fails, the test fails.
- If a test harness (`<name>.harness.cpp`) is found in the `source` folder, it will be compiled and linked as an executable
against the generated files.
The harness executable is then executed.
If compilation fails or execution yields a return code other than `0`, the test fails.
If all steps run without errors, the test succeeds.
### Writing a Test Harness
The most important requirement placed on our code generator is that it produces
functionally correct code that adheres exactly to its input specification.
For one, all generated code must be compilable using an appropriate compiler,
so compiling it (with strict treatment of warnings) as part of the test is a sure way of
checking its syntactical correctness.
Its semantical correctness can be further ensured by providing a C++ test harness.
This test harness can check the semantics of the generated code both statically
(using compile-time assertions, combined with concepts or type traits)
and dynamically (by executing the generated code and checking its output).
Each generator script registered at the test suite can have one test harness named `<name>.harness.cpp`
in the `source` folder. That test harness should `#include` any generated header files
(the test suite ensures the generated files are on the compiler's include path).
Since it will be compiled to an executable, the test harness must also define a `main` function
which should call any dynamic functional tests of the generated code.
If any dynamic test fails, the harness application must terminate with a nonzero error code.
## Test Index (`index.yaml`) Parameter Reference
Each entry in `index.yaml` must be a dictionary.
The test suite parses the following (groups of) parameters:
#### `sfg-args`
SFG-related command-line parameters passed to the generator script.
These may be:
- `header-only` (`true` or `false`): Enable or disable header-only code generation.
If `true`, the set of expected output files is reduced to `{".hpp"}`.
- `file-extensions`: List of file extensions for the output files of the generator script.
If specified, these are taken as the expected output files by the test suite.
- `config-module`: Path to a config module, relative to `source/`.
The Python file referred to by this option will be passed as a configuration module to the generator script.
#### `extra-args`
List of additional command line parameters passed to the script.
#### `expected-output`
List of file extensions that are expected to be produced by the generator script.
Overrides any other source of expected file extensions;
use this if file extensions are determined by inline configuration or the configuration module.
#### `expect-failure`
Boolean indicating whether the script is expected to fail.
If set to `True`, the test fails if the script runs successfully.
#### `expect-code`
Dictionary mapping file extensions to a list of string patterns
that are expected to be generated in the respective files.
These patterns may be:
- A plain string: In this case, that string must be contained verbatim in the generated code
- A dictionary defining at least the `regex` key containing a regular expressions,
and some options affecting its matching.
**Example: Plain String**
This example requires that the generated `hpp` file contains the inclusion of `iostream` verbatim:
```yaml
MyTest:
expect-code:
hpp:
- "#include <iostream>"
```
**Example: Regular Expression**
This example requires a type alias for an `std::mdspan` instance be defined in the header file,
but does not care about the exact extents, or number of spaces used inside the template parameter list:
```yaml
MyTest:
expect-code:
hpp:
- regex: using\sfield_t\s=\sstd::mdspan<\s*float,\s*std::extents<.*>\s*>
```
In the regex example, the pattern is a dictionary with the single key `regex`.
Regex matching can be configured by adding additional keys:
- `count`: How often the regex should match; default is `1`
- `strip-whitespace`: Set to `true` to have the test suite remove any whitespace from the regex string.
Use this if you want to break your long regular expression across several lines. Default is `false`.
**Example: Multiline Regex**
This example is the same as above, but using folded block style (see [yaml-multiline.info](https://yaml-multiline.info/))
to line-break the regex:
```yaml
MyTest:
expect-code:
hpp:
- regex: >-
using\sfield_t\s=
\sstd::mdspan<\s*
float,\s*
std::extents<.*>
\s*>
- strip-whitespace: true
```
#### `compile`
Options affecting compilation of the generated files and the test harness.
Possible options are:
- `cxx`: Executable of the C++ compiler; default is `g++`
- `cxx-flags`: List of arguments to the C++ compiler; default is `["-std=c++20", "-Wall", "-Werror"]`
- `link-flags`: List of arguments for the linker; default is `[]`
- `skip-if-not-found`: If set to `true` and the compiler specified in `cxx` cannot be found,
skip compilation and harness execution. Otherwise, fail the test.
## Dependencies
The `deps` folder includes any vendored dependencies required by generated code.
At the moment, this includes the reference implementation of `std::mdspan`
provided by the Kokkos group [here](https://github.com/kokkos/mdspan).
# This file acts as an index for the generator script test suite.
# For information about its structure and valid parameters, refer to the Readme.md in this folder.
# Configuration
TestConfigModule:
sfg-args:
file-extensions: [h++, c++]
config-module: "config/TestConfigModule_cfg.py"
TestExtraCommandLineArgs:
sfg-args:
file-extensions: [h++, c++]
extra-args: [--precision, float32, test1, test2]
TestIllegalArgs:
extra-args: [--sfg-file-extensionss, ".c++,.h++"]
expect-failure: true
TestIncludeSorting:
sfg-args:
header-only: true
expect-code:
hpp:
- regex: >-
#include\s\<memory>\s*
#include\s<vector>\s*
#include\s<array>
strip-whitespace: true
# Basic Composer Functionality
BasicDefinitions:
sfg-args:
header-only: true
expect-code:
hpp:
- regex: >-
#include\s\"config\.h\"(\s|.)*
namespace\s+awesome\s+{\s+.+\s+
#define\sPI\s3\.1415\s+
using\snamespace\sstd\;\s+
}\s\/\/\s+namespace\sawesome
strip-whitespace: true
SimpleClasses:
sfg-args:
header-only: true
ComposerFeatures:
expect-code:
hpp:
- regex: >-
\[\[nodiscard\]\]\s*static\s*double\s*geometric\(\s*double\s*q,\s*uint64_t\s*k\)
ComposerHeaderOnly:
sfg-args:
header-only: true
expect-code:
hpp:
- regex: >-
inline\s+int32_t\s+twice\s*\(
- regex: >-
inline\s+void\s+kernel\s*\(
Conditionals:
expect-code:
cpp:
- regex: switch\s*\(\s*noodle\s*\)\s*\{\s*
count: 2
- regex: case\s+Noodles::[A-Z]+:\s*\{\s*.*\s*break;\s*\}
count: 2
- regex: case\s+Noodles::[A-Z]+:\s*\{\s*return\s[0-9]+;\s*\}
count: 4
- regex: if\s*\(\s*noodle\s==\sNoodles::RIGATONI\s\|\|\snoodle\s==\sNoodles::SPAGHETTI\s*\)
count: 1
NestedNamespaces:
sfg-args:
header-only: true
# Kernel Generation
ScaleKernel:
JacobiMdspan:
StlContainers1D:
VectorExtraction:
# std::mdspan
MdSpanFixedShapeLayouts:
MdSpanLbStreaming:
# CUDA
CudaKernels:
sfg-args:
file-extensions: ["hpp", "cu"]
compile:
cxx: nvcc
cxx-flags:
- -std=c++20
- -Werror
- all-warnings
- --expt-relaxed-constexpr
skip-if-not-found: true
# HIP
HipKernels:
sfg-args:
file-extensions: ["hpp", "hip"]
compile:
cxx: hipcc
cxx-flags:
- -std=c++20
- -Wall
- -Werror
skip-if-not-found: true
# SYCL
SyclKernels:
sfg-args:
header-only: true
expect-code:
hpp:
- regex: >-
inline\s+void\s+kernel\s*\(
- regex: >-
cgh\.parallel_for\(range,\s*\[=\]\s*\(const\s+sycl::item<\s*2\s*>\s+sycl_item\s*\)\s*\{\s*kernels::kernel\(.*\);\s*\}\);
SyclBuffers:
compile:
cxx: icpx
cxx-flags:
- -fsycl
- -std=c++20
- -Wall
- -Werror
link-flags:
- -fsycl
skip-if-not-found: true
from pystencilssfg import SourceFileGenerator, SfgConfig
# Do not use clang-format, since it reorders headers
cfg = SfgConfig()
cfg.clang_format.skip = True
with SourceFileGenerator(cfg) as sfg:
sfg.namespace("awesome")
sfg.prelude("Expect the unexpected, and you shall never be surprised.")
sfg.include("<iostream>")
sfg.include("config.h")
sfg.code("#define PI 3.1415")
sfg.code("using namespace std;")
#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 );
}