diff --git a/pystencilssfg/__main__.py b/pystencilssfg/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..36413da374b1450e018c231ce45230206d349a25 --- /dev/null +++ b/pystencilssfg/__main__.py @@ -0,0 +1,80 @@ +import sys +from os import path + +from argparse import ArgumentParser + +from .configuration import ( + SfgConfigException, SfgConfigSource, + add_config_args_to_parser, config_from_parser_args, merge_configurations +) + + +def main(): + parser = ArgumentParser("pystencilssfg", + description="pystencilssfg command-line utility") + + subparsers = parser.add_subparsers(required=True, title="Subcommands") + + version_parser = subparsers.add_parser( + "version", help="Print version and exit.") + version_parser.set_defaults(func=version) + + outfiles_parser = subparsers.add_parser( + "list-files", help="List files produced by given codegen script.") + + outfiles_parser.set_defaults(func=list_files) + outfiles_parser.add_argument( + "--format", type=str, choices=("human", "cmake"), default="human") + outfiles_parser.add_argument("codegen_script", type=str) + + add_config_args_to_parser(outfiles_parser) + + args = parser.parse_args() + args.func(args) + + +def version(args, argv): + from . import __version__ + print(version) + exit(0) + + +def list_files(args): + try: + project_config, cmdline_config = config_from_parser_args(args) + except SfgConfigException as exc: + abort_with_config_exception(exc) + + config = merge_configurations(project_config, cmdline_config, None) + + scriptdir, scriptname = path.split(args.codegen_script) + basename = path.splitext(scriptname)[0] + + from .emitters.cpu.basic_cpu import BasicCpuEmitter + + emitter = BasicCpuEmitter(basename, config) + + match args.format: + case "human": print(" ".join(emitter.output_files)) + case "cmake": print(";".join(emitter.output_files), end="") + + exit(0) + + +def abort_with_config_exception(exception: SfgConfigException): + def eprint(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + + match exception.config_source: + case SfgConfigSource.PROJECT: + eprint( + f"Invalid project configuration: {exception.message}\nCheck your configurator script.") + case SfgConfigSource.COMMANDLINE: + eprint( + f"Invalid configuration on command line: {exception.message}") + case _: assert False, "(Theoretically) unreachable code. Contact the developers." + + exit(1) + + +main() diff --git a/pystencilssfg/cmake/PystencilsSfgFunctions.cmake b/pystencilssfg/cmake/PystencilsSfgFunctions.cmake index 4dcefa9af148eb2b3e5d3828c7b44a69c22de58b..9cd75aaf268f992b3a07e991c6f0900362943076 100644 --- a/pystencilssfg/cmake/PystencilsSfgFunctions.cmake +++ b/pystencilssfg/cmake/PystencilsSfgFunctions.cmake @@ -1,33 +1,84 @@ + +set(PSSFG_CONFIGURATOR_SCRIPT "" CACHE FILEPATH "Configurator script for the pystencils Source File Generator" ) + set(PSSFG_GENERATED_SOURCES_DIR "${CMAKE_BINARY_DIR}/pystencils_generated_sources") file(MAKE_DIRECTORY "${PSSFG_GENERATED_SOURCES_DIR}") include_directories(${PSSFG_GENERATED_SOURCES_DIR}) -function(pystencilssfg_generate_target_sources) + +function(_pssfg_config_cmdline_args OUTVAR) + set(options HEADER_ONLY) + set(oneValueArgs CONFIGURATOR_SCRIPT OUTPUT_DIR) + set(multiValueArgs FILE_EXTENSIONS) + + cmake_parse_arguments(_pssfg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + + + set(${OUTVAR} ${cmdline} PARENT_SCOPE) +endfunction() + +function(_pssfg_add_gen_source target script) set(options) - set(oneValueArgs TARGET SCRIPT) - set(multiValueArgs DEPENDS) - cmake_parse_arguments(GENSRC "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + set(oneValueArgs) + set(multiValueArgs GENERATOR_ARGS DEPENDS) + + cmake_parse_arguments(_pssfg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - set(generatedSourcesDir ${PSSFG_GENERATED_SOURCES_DIR}/gen/${GENSRC_TARGET}) + set(generatedSourcesDir ${PSSFG_GENERATED_SOURCES_DIR}/gen/${target}) + get_filename_component(basename ${script} NAME_WLE) + cmake_path(ABSOLUTE_PATH script OUTPUT_VARIABLE scriptAbsolute) - get_filename_component(basename ${GENSRC_SCRIPT} NAME_WLE) - cmake_path(ABSOLUTE_PATH GENSRC_SCRIPT OUTPUT_VARIABLE pythonFile) + execute_process(COMMAND ${Python_EXECUTABLE} -m pystencilssfg list-files --format=cmake ${_pssfg_GENERATOR_ARGS} ${script} + OUTPUT_VARIABLE generatedSources RESULT_VARIABLE _pssfg_result + ERROR_VARIABLE _pssfg_stderr) - set(generatedSourceFiles ${basename}.h ${basename}.cpp) - set(generatedWithAbsolutePath) - foreach (filename ${generatedSourceFiles}) - list(APPEND generatedWithAbsolutePath ${generatedSourcesDir}/${filename}) + if(NOT (${_pssfg_result} EQUAL 0)) + message( FATAL_ERROR ${_pssfg_stderr} ) + endif() + + set(generatedSourcesAbsolute) + foreach (filename ${generatedSources}) + list(APPEND generatedSourcesAbsolute ${generatedSourcesDir}/${filename}) endforeach () file(MAKE_DIRECTORY "${generatedSourcesDir}") - # TODO: Get generator arguments via PYSTENCILS_GENERATOR_FLAGS, source file and target properties + add_custom_command(OUTPUT ${generatedSourcesAbsolute} + DEPENDS ${scriptAbsolute} ${_pssfg_DEPENDS} + COMMAND ${Python_EXECUTABLE} ${scriptAbsolute} ${_pssfg_GENERATOR_ARGS} + WORKING_DIRECTORY "${generatedSourcesDir}") + + target_sources(${target} PRIVATE ${generatedSourcesAbsolute}) +endfunction() + + +function(pystencilssfg_generate_target_sources TARGET) + set(options HEADER_ONLY) + set(multiValueArgs SCRIPTS DEPENDS FILE_EXTENSIONS) + cmake_parse_arguments(_pssfg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + set(generatedSourcesDir ${PSSFG_GENERATED_SOURCES_DIR}/gen/${_pssfg_TARGET}) + + set(generatorArgs) + + if(_pssfg_HEADER_ONLY) + list(APPEND generatorArgs "--sfg-header-only") + endif() + + if(NOT (PSSFG_CONFIGURATOR_SCRIPT STREQUAL "")) + list(APPEND generatorArgs "--sfg-configurator='${_pssfg_CONFIGURATOR_SCRIPT}'") + endif() + + if(DEFINED _pssfg_FILE_EXTENSIONS) + string(JOIN "," extensionsString ${_pssfg_FILE_EXTENSIONS}) - add_custom_command(OUTPUT ${generatedWithAbsolutePath} - DEPENDS ${pythonFile} ${GENSRC_DEPENDS} - COMMAND ${Python_EXECUTABLE} ${pythonFile} - WORKING_DIRECTORY "${generatedSourcesDir}") + list(APPEND generatorArgs "--sfg-file-extensions=${extensionsString}") + endif() - target_sources(${GENSRC_TARGET} PRIVATE ${generatedWithAbsolutePath}) + foreach(codegenScript ${_pssfg_SCRIPTS}) + _pssfg_add_gen_source(${TARGET} ${codegenScript} GENERATOR_ARGS ${generatorArgs} DEPENDS ${_pssfg_DEPENDS}) + endforeach() + endfunction() diff --git a/pystencilssfg/configuration.py b/pystencilssfg/configuration.py index c712cb15a18fd3d8af6d76a6f88694d8a50d315e..f2f53a692e739a32fcb6a6f21ebd061713905985 100644 --- a/pystencilssfg/configuration.py +++ b/pystencilssfg/configuration.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import List, Sequence from enum import Enum, auto -from dataclasses import dataclass, replace, asdict, fields +from dataclasses import dataclass, replace, asdict, InitVar from argparse import ArgumentParser from jinja2.filters import do_indent @@ -13,6 +13,22 @@ HEADER_FILE_EXTENSIONS = {'h', 'hpp'} SOURCE_FILE_EXTENSIONS = {'c', 'cpp'} +class SfgConfigSource(Enum): + DEFAULT = auto() + PROJECT = auto() + COMMANDLINE = auto + SCRIPT = auto() + + +class SfgConfigException(Exception): + def __init__(self, cfg_src: SfgConfigSource, message: str): + assert cfg_src != SfgConfigSource.DEFAULT, "Invalid default config. Contact a developer." + + super().__init__(cfg_src, message) + self.message = message + self.config_source = cfg_src + + @dataclass class SfgCodeStyle: indent_width: int = 2 @@ -23,6 +39,8 @@ class SfgCodeStyle: @dataclass class SfgConfiguration: + config_source: InitVar[SfgConfigSource | None] = None + header_extension: str = None """File extension for generated header files.""" @@ -42,10 +60,9 @@ class SfgConfiguration: output_directory: str = None """Directory to which the generated files should be written.""" - def __post_init__(self): + def __post_init__(self, cfg_src: SfgConfigSource = None): if self.header_only: - raise SfgException( - "Header-only code generation is not implemented yet.") + raise SfgConfigException(cfg_src, "Header-only code generation is not implemented yet.") if self.header_extension and self.header_extension[0] == '.': self.header_extension = self.header_extension[1:] @@ -60,6 +77,7 @@ class SfgConfiguration: DEFAULT_CONFIG = SfgConfiguration( + config_source=SfgConfigSource.DEFAULT, header_extension='h', source_extension='cpp', header_only=False, @@ -73,39 +91,52 @@ def run_configurator(configurator_script: str): raise NotImplementedError() -def config_from_commandline(argv: List[str]): - parser = ArgumentParser("pystencilssfg", - description="pystencils Source File Generator", - allow_abbrev=False) +def add_config_args_to_parser(parser: ArgumentParser): + config_group = parser.add_argument_group("Configuration") - parser.add_argument("-sfg-d", "--sfg-output-dir", - type=str, default='.', dest='output_directory') - parser.add_argument("-sfg-e", "--sfg-file-extensions", - type=str, default=None, nargs='*', dest='file_extensions') - parser.add_argument("--sfg-header-only", - type=str, default=None, nargs='*', dest='header_only') - parser.add_argument("--sfg-configurator", type=str, - default=None, nargs='*', dest='configurator_script') + config_group.add_argument("--sfg-output-dir", + type=str, default=None, dest='output_directory') + config_group.add_argument("--sfg-file-extensions", + type=str, default=None, dest='file_extensions', help="Comma-separated list of file extensions") + config_group.add_argument("--sfg-header-only", default=None, action='store_true', dest='header_only') + config_group.add_argument("--sfg-configurator", type=str, default=None, dest='configurator_script') + + return parser - args, script_args = parser.parse_known_args(argv) +def config_from_parser_args(args): if args.configurator_script is not None: project_config = run_configurator(args.configurator_script) else: project_config = None if args.file_extensions is not None: - h_ext, src_ext = _get_file_extensions(args.file_extensions) + file_extentions = list(args.file_extensions.split(",")) + h_ext, src_ext = _get_file_extensions(SfgConfigSource.COMMANDLINE, file_extentions) else: h_ext, src_ext = None, None cmdline_config = SfgConfiguration( + config_source=SfgConfigSource.COMMANDLINE, header_extension=h_ext, source_extension=src_ext, header_only=args.header_only, output_directory=args.output_directory ) + return project_config, cmdline_config + + +def config_from_commandline(argv: List[str]): + parser = ArgumentParser("pystencilssfg", + description="pystencils Source File Generator", + allow_abbrev=False) + + add_config_args_to_parser(parser) + + args, script_args = parser.parse_known_args(argv) + project_config, cmdline_config = config_from_parser_args(args) + return project_config, cmdline_config, script_args @@ -138,7 +169,7 @@ def merge_configurations(project_config: SfgConfiguration, return config -def _get_file_extensions(extensions: Sequence[str]): +def _get_file_extensions(cfgsrc: SfgConfigSource, extensions: Sequence[str]): h_ext = None src_ext = None @@ -147,13 +178,13 @@ def _get_file_extensions(extensions: Sequence[str]): for ext in extensions: if ext in HEADER_FILE_EXTENSIONS: if h_ext is not None: - raise ValueError("Multiple header file extensions found.") + raise SfgConfigException(cfgsrc, "Multiple header file extensions specified.") h_ext = ext elif ext in SOURCE_FILE_EXTENSIONS: if src_ext is not None: - raise ValueError("Multiple source file extensions found.") + raise SfgConfigException(cfgsrc, "Multiple source file extensions specified.") src_ext = ext else: - raise ValueError(f"Don't know how to interpret extension '.{ext}'") + raise SfgConfigException(cfgsrc, f"Don't know how to interpret file extension '.{ext}'") return h_ext, src_ext diff --git a/pystencilssfg/emitters/cpu/basic_cpu.py b/pystencilssfg/emitters/cpu/basic_cpu.py index 962acda78aa2354c69ab96e5767a1fca1d968235..fdb081cbed6471658c8f1771c5858faeeaa61f96 100644 --- a/pystencilssfg/emitters/cpu/basic_cpu.py +++ b/pystencilssfg/emitters/cpu/basic_cpu.py @@ -10,18 +10,20 @@ class BasicCpuEmitter: self._basename = basename self._output_directory = config.output_directory self._header_filename = f"{basename}.{config.header_extension}" - self._cpp_filename = f"{basename}.{config.source_extension}" + self._source_filename = f"{basename}.{config.source_extension}" @property def output_files(self) -> str: return ( path.join(self._output_directory, self._header_filename), - path.join(self._output_directory, self._cpp_filename) + path.join(self._output_directory, self._source_filename) ) def write_files(self, ctx: SfgContext): jinja_context = { 'ctx': ctx, + 'header_filename': self._header_filename, + 'source_filename': self._source_filename, 'basename': self._basename, 'root_namespace': ctx.root_namespace, 'public_includes': list(incl.get_code() for incl in ctx.includes() if not incl.private), @@ -43,5 +45,5 @@ class BasicCpuEmitter: with open(path.join(self._output_directory, self._header_filename), 'w') as headerfile: headerfile.write(header) - with open(path.join(self._output_directory, self._cpp_filename), 'w') as cppfile: + with open(path.join(self._output_directory, self._source_filename), 'w') as cppfile: cppfile.write(source) diff --git a/pystencilssfg/emitters/cpu/templates/BasicCpu.tmpl.cpp b/pystencilssfg/emitters/cpu/templates/BasicCpu.tmpl.cpp index 80cfe4de1e1ceedfcf4eb2dea68a7090fd0eebde..1f2e61414e348565b7f14c07f137f3e1fe717955 100644 --- a/pystencilssfg/emitters/cpu/templates/BasicCpu.tmpl.cpp +++ b/pystencilssfg/emitters/cpu/templates/BasicCpu.tmpl.cpp @@ -1,4 +1,4 @@ -#include "{{basename}}.h" +#include "{{header_filename}}" {% for incl in private_includes -%} {{incl}} diff --git a/tests/cmake_integration/CMakeLists.txt b/tests/cmake_integration/CMakeLists.txt index f67ed7b857c69cb78636c8ec4ccb80a884c55e0f..d1075415e7766240e71ebe1afaf429e0c3bdb933 100644 --- a/tests/cmake_integration/CMakeLists.txt +++ b/tests/cmake_integration/CMakeLists.txt @@ -6,5 +6,7 @@ set( CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${pssfg_cmake_integration_test_SOURC find_package( PystencilsSfg REQUIRED ) +# set( PSSFG_CONFIGURATOR_SCRIPT "codegen_config.py" ) + add_library( genlib ) -pystencilssfg_generate_target_sources( TARGET genlib SCRIPT kernels.py ) +pystencilssfg_generate_target_sources( genlib SCRIPTS kernels.py more_kernels.py FILE_EXTENSIONS .hpp .cpp ) diff --git a/tests/cmake_integration/more_kernels.py b/tests/cmake_integration/more_kernels.py new file mode 100644 index 0000000000000000000000000000000000000000..2d97a94c0f99427e345447ef7650728637242928 --- /dev/null +++ b/tests/cmake_integration/more_kernels.py @@ -0,0 +1,23 @@ +import sympy as sp +import numpy as np + +from pystencils.session import * + +from pystencilssfg import SourceFileGenerator +from pystencilssfg.source_concepts.cpp import std_mdspan + + +with SourceFileGenerator() as sfg: + src = ps.fields("src: double[2D]") + + h = sp.Symbol('h') + + @ps.kernel + def poisson_gs(): + src[0,0] @= (src[1, 0] + src[-1, 0] + src[0, 1] + src[0, -1]) / 4 + + poisson_kernel = sfg.kernels.create(poisson_gs) + + sfg.function("gs_smooth")( + sfg.call(poisson_kernel) + )