From 02b6e240cf56af1dba2f60aac09f20993398cb31 Mon Sep 17 00:00:00 2001
From: Frederik Hennig <frederik.hennig@fau.de>
Date: Fri, 21 Feb 2025 14:07:23 +0100
Subject: [PATCH 1/6] Remove Inline output mode. Refactor output_mode to
 `header_only` switch.

---
 docs/source/api/generation.rst                |  3 -
 docs/source/usage/config_and_cli.md           |  3 +-
 docs/source/usage/project_integration.md      |  5 +-
 integration/test_sycl_buffer.py               |  8 +-
 src/pystencilssfg/__init__.py                 |  3 +-
 .../cmake/modules/PystencilsSfg.cmake         | 10 ++-
 src/pystencilssfg/config.py                   | 79 ++++---------------
 src/pystencilssfg/generator.py                | 30 +++----
 tests/generator/test_config.py                | 20 ++++-
 tests/generator_scripts/README.md             |  8 +-
 tests/generator_scripts/index.yaml            |  8 +-
 tests/generator_scripts/source/SyclKernels.py |  5 +-
 .../test_generator_scripts.py                 |  9 ++-
 .../integration/cmake_project/CMakeLists.txt  |  4 +-
 tests/integration/test_cli.py                 |  3 +-
 15 files changed, 79 insertions(+), 119 deletions(-)

diff --git a/docs/source/api/generation.rst b/docs/source/api/generation.rst
index 8e7b0b4..7a0d013 100644
--- a/docs/source/api/generation.rst
+++ b/docs/source/api/generation.rst
@@ -21,9 +21,6 @@ Categories, Parameter Types, and Special Values
 .. autoclass:: _GlobalNamespace
 .. autodata:: GLOBAL_NAMESPACE
 
-.. autoclass:: OutputMode
-    :members:
-
 .. autoclass:: FileExtensions
     :members:
 
diff --git a/docs/source/usage/config_and_cli.md b/docs/source/usage/config_and_cli.md
index 1fdc9df..80169b1 100644
--- a/docs/source/usage/config_and_cli.md
+++ b/docs/source/usage/config_and_cli.md
@@ -42,6 +42,7 @@ The file extensions of the generated files can be modified through
 {any}`cfg.extensions.header <FileExtensions.header>`
 and {any}`cfg.extensions.impl <FileExtensions.impl>`;
 and the output directory of the code generator can be set through {any}`cfg.output_directory <SfgConfig.output_directory>`.
+Header-only code generation can be enabled using {any}`cfg.header_only <SfgConfig.header_only>`.
 
 :::{danger}
 
@@ -76,7 +77,7 @@ on invocation. These include:
 - `--sfg-output-dir <path>`: Set the output directory of the generator script. This corresponds to {any}`SfgConfig.output_directory`.
 - `--sfg-file-extensions <exts>`: Set the file extensions used for the generated files;
   `exts` must be a comma-separated list not containing any spaces. Corresponds to {any}`SfgConfig.extensions`.
-- `--sfg-output-mode <mode>`: Set the output mode of the generator script. Corresponds to {any}`SfgConfig.output_mode`.
+- `[--no]--sfg-header-only`: Enable or disable header-only code generation. Corresponds to {any}`SfgConfig.header_only`.
 
 If any configuration option is set to conflicting values on the command line and in the inline configuration,
 the generator script will terminate with an error.
diff --git a/docs/source/usage/project_integration.md b/docs/source/usage/project_integration.md
index 47b1b68..0ad13e8 100644
--- a/docs/source/usage/project_integration.md
+++ b/docs/source/usage/project_integration.md
@@ -120,9 +120,9 @@ pystencilssfg_generate_target_sources( <target>
     [SCRIPT_ARGS arg1 [arg2 ...]]
     [DEPENDS dependency1.py [dependency2.py...]]
     [FILE_EXTENSIONS <header-extension> <impl-extension>]
-    [OUTPUT_MODE <standalone|inline|header-only>]
     [CONFIG_MODULE <path-to-config-module.py>]
     [OUTPUT_DIRECTORY <output-directory>]
+    [HEADER_ONLY]
 )
 ```
 
@@ -135,12 +135,13 @@ The function takes the following options:
  - `SCRIPT_ARGS`: A list of custom command line arguments passed to the generator scripts; see [](#custom_cli_args)
  - `DEPENDS`: A list of dependencies for the generator scripts
  - `FILE_EXTENSION`: The desired extensions for the generated files
- - `OUTPUT_MODE`: Sets the output mode of the code generator; see {any}`SfgConfig.output_mode`.
  - `CONFIG_MODULE`: Set the configuration module for all scripts registered with this call.
    If set, this overrides the value of `PystencilsSfg_CONFIG_MODULE`
    in the current scope (see [](#cmake_set_config_module))
  - `OUTPUT_DIRECTORY`: Custom output directory for generated files. If `OUTPUT_DIRECTORY` is a relative path,
    it will be interpreted relative to the current build directory.
+ - `HEADER_ONLY`: If this option is set, instruct the generator scripts to only generate header files
+   (see {any}`SfgConfig.header_only`).
 
 If `OUTPUT_DIRECTORY` is *not* specified, any C++ header files generated by the above call
 can be included in any files belonging to `target` via:
diff --git a/integration/test_sycl_buffer.py b/integration/test_sycl_buffer.py
index a1e52eb..a49d4e4 100644
--- a/integration/test_sycl_buffer.py
+++ b/integration/test_sycl_buffer.py
@@ -1,14 +1,14 @@
 from pystencils import Target, CreateKernelConfig, no_jit
 from lbmpy import create_lb_update_rule, LBMOptimisation
-from pystencilssfg import SourceFileGenerator, SfgConfig, OutputMode
-from pystencilssfg.lang.cpp.sycl_accessor import sycl_accessor_ref
+from pystencilssfg import SourceFileGenerator, SfgConfig
+from pystencilssfg.lang.cpp.sycl_accessor import SyclAccessor
 import pystencilssfg.extensions.sycl as sycl
 from itertools import chain
 
 sfg_config = SfgConfig(
     output_directory="out/test_sycl_buffer",
     outer_namespace="gen_code",
-    output_mode=OutputMode.INLINE,
+    header_only=True
 )
 
 with SourceFileGenerator(sfg_config) as sfg:
@@ -21,7 +21,7 @@ with SourceFileGenerator(sfg_config) as sfg:
     cgh = sfg.sycl_handler("handler")
     rang = sfg.sycl_range(update.method.dim, "range")
     mappings = [
-        sfg.map_field(field, sycl_accessor_ref(field))
+        sfg.map_field(field, SyclAccessor.from_field(field))
         for field in chain(update.free_fields, update.bound_fields)
     ]
 
diff --git a/src/pystencilssfg/__init__.py b/src/pystencilssfg/__init__.py
index fea6f8a..cea370c 100644
--- a/src/pystencilssfg/__init__.py
+++ b/src/pystencilssfg/__init__.py
@@ -1,4 +1,4 @@
-from .config import SfgConfig, GLOBAL_NAMESPACE, OutputMode
+from .config import SfgConfig, GLOBAL_NAMESPACE
 from .generator import SourceFileGenerator
 from .composer import SfgComposer
 from .context import SfgContext
@@ -8,7 +8,6 @@ from .exceptions import SfgException
 __all__ = [
     "SfgConfig",
     "GLOBAL_NAMESPACE",
-    "OutputMode",
     "SourceFileGenerator",
     "SfgComposer",
     "SfgContext",
diff --git a/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake b/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake
index 0779599..eb4c234 100644
--- a/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake
+++ b/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake
@@ -54,15 +54,17 @@ endfunction()
 
 
 function(pystencilssfg_generate_target_sources TARGET)
-    set(options)
-    set(oneValueArgs OUTPUT_MODE CONFIG_MODULE OUTPUT_DIRECTORY)
+    set(options HEADER_ONLY)
+    set(oneValueArgs CONFIG_MODULE OUTPUT_DIRECTORY)
     set(multiValueArgs SCRIPTS DEPENDS FILE_EXTENSIONS SCRIPT_ARGS)
     cmake_parse_arguments(_pssfg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
 
     set(generatorArgs)
 
-    if(DEFINED _pssfg_OUTPUT_MODE)
-        list(APPEND generatorArgs "--sfg-output-mode=${_pssfg_OUTPUT_MODE}")
+    if(_pssfg_HEADER_ONLY)
+        list(APPEND generatorArgs "--sfg-header-only")
+    else()
+        list(APPEND generatorArgs "--no-sfg-header-only")
     endif()
 
     if(DEFINED _pssfg_CONFIG_MODULE)
diff --git a/src/pystencilssfg/config.py b/src/pystencilssfg/config.py
index bbe2389..482ca8e 100644
--- a/src/pystencilssfg/config.py
+++ b/src/pystencilssfg/config.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from argparse import ArgumentParser
+from argparse import ArgumentParser, BooleanOptionalAction
 
 from types import ModuleType
 from typing import Any, Sequence, Callable
@@ -25,7 +25,7 @@ class FileExtensions(ConfigBase):
     header: BasicOption[str] = BasicOption("hpp")
     """File extension for generated header file."""
 
-    impl: BasicOption[str] = BasicOption()
+    impl: BasicOption[str] = BasicOption("cpp")
     """File extension for generated implementation file."""
 
     @header.validate
@@ -37,25 +37,6 @@ class FileExtensions(ConfigBase):
         return ext
 
 
-class OutputMode(Enum):
-    """Output mode of the source file generator."""
-
-    STANDALONE = auto()
-    """Generate a header/implementation file pair (e.g. ``.hpp/.cpp``) where the implementation file will
-    be compiled to a standalone object."""
-
-    INLINE = auto()
-    """Generate a header/inline implementation file pair (e.g. ``.hpp/.ipp``) where all implementations
-    are inlined by including the implementation file at the end of the header file."""
-
-    HEADER_ONLY = auto()
-    """Generate only a header file.
-
-    At the moment, header-only mode does not support generation of kernels and requires that all functions
-    and methods are marked ``inline``.
-    """
-
-
 @dataclass
 class CodeStyle(ConfigBase):
     """Options affecting the code style used by the source file generator."""
@@ -137,14 +118,10 @@ class SfgConfig(ConfigBase):
             FileExtensions.impl
     """
 
-    output_mode: BasicOption[OutputMode] = BasicOption(OutputMode.STANDALONE)
-    """The generator's output mode; defines which files to generate, and the set of legal file extensions.
-
-    Possible parameters:
-        .. autosummary::
-            OutputMode.STANDALONE
-            OutputMode.INLINE
-            OutputMode.HEADER_ONLY
+    header_only: BasicOption[bool] = BasicOption(False)
+    """If set to `True`, generate only a header file.
+    
+    This will cause all definitions to be generated ``inline``.
     """
 
     outer_namespace: BasicOption[str | _GlobalNamespace] = BasicOption(GLOBAL_NAMESPACE)
@@ -187,16 +164,9 @@ class SfgConfig(ConfigBase):
         header_ext = self.extensions.get_option("header")
         impl_ext = self.extensions.get_option("impl")
         output_files = [output_dir / f"{basename}.{header_ext}"]
-        output_mode = self.get_option("output_mode")
-
-        if impl_ext is None:
-            match output_mode:
-                case OutputMode.INLINE:
-                    impl_ext = "ipp"
-                case OutputMode.STANDALONE:
-                    impl_ext = "cpp"
+        header_only = self.get_option("header_only")
 
-        if output_mode != OutputMode.HEADER_ONLY:
+        if not header_only:
             assert impl_ext is not None
             output_files.append(output_dir / f"{basename}.{impl_ext}")
 
@@ -219,11 +189,10 @@ class CommandLineParameters:
             help="Comma-separated list of file extensions",
         )
         config_group.add_argument(
-            "--sfg-output-mode",
-            type=str,
-            default=None,
-            choices=("standalone", "inline", "header-only"),
-            dest="output_mode",
+            "--sfg-header-only",
+            action=BooleanOptionalAction,
+            dest="header_only",
+            help="Generate only a header file."
         )
         config_group.add_argument(
             "--sfg-config-module", type=str, default=None, dest="config_module_path"
@@ -234,21 +203,7 @@ class CommandLineParameters:
     def __init__(self, args) -> None:
         self._cl_config_module_path: str | None = args.config_module_path
 
-        if args.output_mode is not None:
-            match args.output_mode.lower():
-                case "standalone":
-                    output_mode = OutputMode.STANDALONE
-                case "inline":
-                    output_mode = OutputMode.INLINE
-                case "header-only":
-                    output_mode = OutputMode.HEADER_ONLY
-                case _:
-                    assert False, "invalid output mode"
-        else:
-            output_mode = None
-
-        self._cl_output_mode = output_mode
-
+        self._cl_header_only: bool | None = args.header_only
         self._cl_output_dir: str | None = args.output_directory
 
         if args.file_extensions is not None:
@@ -279,8 +234,8 @@ class CommandLineParameters:
         ):
             self._config_module.configure_sfg(cfg)
 
-        if self._cl_output_mode is not None:
-            cfg.output_mode = self._cl_output_mode
+        if self._cl_header_only is not None:
+            cfg.header_only = self._cl_header_only
         if self._cl_header_ext is not None:
             cfg.extensions.header = self._cl_header_ext
         if self._cl_impl_ext is not None:
@@ -292,7 +247,7 @@ class CommandLineParameters:
 
     def find_conflicts(self, cfg: SfgConfig):
         for name, mine, theirs in (
-            ("output_mode", self._cl_output_mode, cfg.output_mode),
+            ("header_only", self._cl_header_only, cfg.header_only),
             ("extensions.header", self._cl_header_ext, cfg.extensions.header),
             ("extensions.impl", self._cl_impl_ext, cfg.extensions.impl),
             ("output_directory", self._cl_output_dir, cfg.output_directory),
@@ -320,7 +275,7 @@ class CommandLineParameters:
         extensions = tuple((ext[1:] if ext[0] == "." else ext) for ext in extensions)
 
         HEADER_FILE_EXTENSIONS = {"h", "hpp", "hxx", "h++", "cuh"}
-        IMPL_FILE_EXTENSIONS = {"c", "cpp", "cxx", "c++", "cu", ".impl.h", "ipp", "hip"}
+        IMPL_FILE_EXTENSIONS = {"c", "cpp", "cxx", "c++", "cu", "hip"}
 
         for ext in extensions:
             if ext in HEADER_FILE_EXTENSIONS:
diff --git a/src/pystencilssfg/generator.py b/src/pystencilssfg/generator.py
index 91a124a..c314d67 100644
--- a/src/pystencilssfg/generator.py
+++ b/src/pystencilssfg/generator.py
@@ -4,7 +4,6 @@ from typing import Callable, Any
 from .config import (
     SfgConfig,
     CommandLineParameters,
-    OutputMode,
     _GlobalNamespace,
 )
 from .context import SfgContext
@@ -78,7 +77,7 @@ class SourceFileGenerator:
             cli_params.find_conflicts(sfg_config)
             config.override(sfg_config)
 
-        self._output_mode: OutputMode = config.get_option("output_mode")
+        self._header_only: bool = config.get_option("header_only")
         self._output_dir: Path = config.get_option("output_directory")
 
         output_files = config._get_output_files(basename)
@@ -90,20 +89,15 @@ class SourceFileGenerator:
         )
         self._impl_file: SfgSourceFile | None
 
-        match self._output_mode:
-            case OutputMode.HEADER_ONLY:
-                self._impl_file = None
-            case OutputMode.STANDALONE:
-                self._impl_file = SfgSourceFile(
-                    output_files[1].name, SfgSourceFileType.TRANSLATION_UNIT
-                )
-                self._impl_file.includes.append(
-                    HeaderFile.parse(self._header_file.name)
-                )
-            case OutputMode.INLINE:
-                self._impl_file = SfgSourceFile(
-                    output_files[1].name, SfgSourceFileType.HEADER
-                )
+        if self._header_only:
+            self._impl_file = None
+        else:
+            self._impl_file = SfgSourceFile(
+                output_files[1].name, SfgSourceFileType.TRANSLATION_UNIT
+            )
+            self._impl_file.includes.append(
+                HeaderFile.parse(self._header_file.name)
+            )
 
         #   TODO: Find a way to not hard-code the restrict qualifier in pystencils
         self._header_file.elements.append("#define RESTRICT __restrict__")
@@ -150,10 +144,6 @@ class SourceFileGenerator:
                 impl_path.unlink()
 
     def _finish_files(self) -> None:
-        if self._output_mode == OutputMode.INLINE:
-            assert self._impl_file is not None
-            self._header_file.elements.append(f'#include "{self._impl_file.name}"')
-
         from .ir import collect_includes
 
         header_includes = collect_includes(self._header_file)
diff --git a/tests/generator/test_config.py b/tests/generator/test_config.py
index 250c158..9d542cd 100644
--- a/tests/generator/test_config.py
+++ b/tests/generator/test_config.py
@@ -3,7 +3,6 @@ from pathlib import Path
 
 from pystencilssfg.config import (
     SfgConfig,
-    OutputMode,
     GLOBAL_NAMESPACE,
     CommandLineParameters,
     SfgConfigException
@@ -13,7 +12,7 @@ from pystencilssfg.config import (
 def test_defaults():
     cfg = SfgConfig()
 
-    assert cfg.get_option("output_mode") == OutputMode.STANDALONE
+    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"
@@ -90,6 +89,23 @@ def test_from_commandline(sample_config_module):
     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]
diff --git a/tests/generator_scripts/README.md b/tests/generator_scripts/README.md
index 185e270..5dfc474 100644
--- a/tests/generator_scripts/README.md
+++ b/tests/generator_scripts/README.md
@@ -85,12 +85,12 @@ The test suite parses the following (groups of) parameters:
 
 SFG-related command-line parameters passed to the generator script.
 These may be:
-- `output-mode`: Define the output mode, can be either `standalone`, `inline` or `header-only`.
-If `header-only` is specified, the set of expected output files is reduced to `{".hpp"}`.
+- `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.
+  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.
+  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.
diff --git a/tests/generator_scripts/index.yaml b/tests/generator_scripts/index.yaml
index 68352fe..5e2db9a 100644
--- a/tests/generator_scripts/index.yaml
+++ b/tests/generator_scripts/index.yaml
@@ -19,7 +19,7 @@ TestIllegalArgs:
 
 TestIncludeSorting:
   sfg-args:
-    output-mode: header-only
+    header-only: true
   expect-code:
     hpp:
       - regex: >-
@@ -32,7 +32,7 @@ TestIncludeSorting:
 
 BasicDefinitions:
   sfg-args:
-    output-mode: header-only
+    header-only: true
   expect-code:
     hpp:
       - regex: >-
@@ -45,7 +45,7 @@ BasicDefinitions:
 
 SimpleClasses:
   sfg-args:
-    output-mode: header-only
+    header-only: true
 
 ComposerFeatures:
   expect-code:
@@ -67,7 +67,7 @@ Conditionals:
 
 NestedNamespaces:
   sfg-args:
-    output-mode: header-only
+    header-only: true
 
 # Kernel Generation
 
diff --git a/tests/generator_scripts/source/SyclKernels.py b/tests/generator_scripts/source/SyclKernels.py
index 8417743..f181a3d 100644
--- a/tests/generator_scripts/source/SyclKernels.py
+++ b/tests/generator_scripts/source/SyclKernels.py
@@ -1,12 +1,11 @@
 import sympy as sp
 import pystencils as ps
 
-from pystencilssfg import SourceFileGenerator, SfgConfig, OutputMode
+from pystencilssfg import SourceFileGenerator, SfgConfig
 from pystencilssfg.extensions.sycl import SyclComposer
 
 cfg = SfgConfig()
-cfg.output_mode = OutputMode.INLINE
-cfg.extensions.impl = "ipp"
+cfg.header_only = True
 
 with SourceFileGenerator(cfg) as sfg:
     sfg = SyclComposer(sfg)
diff --git a/tests/generator_scripts/test_generator_scripts.py b/tests/generator_scripts/test_generator_scripts.py
index bba12af..6f2ff16 100644
--- a/tests/generator_scripts/test_generator_scripts.py
+++ b/tests/generator_scripts/test_generator_scripts.py
@@ -72,11 +72,12 @@ class GenScriptTest:
 
         sfg_args: dict = test_description.get("sfg-args", dict())
 
-        if (output_mode := sfg_args.get("output-mode", None)) is not None:
-            if output_mode == "header-only":
+        if (header_only := sfg_args.get("header-only", None)) is not None:
+            if header_only:
                 expected_extensions = ["hpp"]
-
-            self._script_args += ["--sfg-output-mode", output_mode]
+                self._script_args += ["--sfg-header-only"]
+            else:
+                self._script_args += ["--no--sfg-header-only"]
 
         if (file_exts := sfg_args.get("file-extensions", None)) is not None:
             expected_extensions = file_exts
diff --git a/tests/integration/cmake_project/CMakeLists.txt b/tests/integration/cmake_project/CMakeLists.txt
index ee7afae..f93b090 100644
--- a/tests/integration/cmake_project/CMakeLists.txt
+++ b/tests/integration/cmake_project/CMakeLists.txt
@@ -35,12 +35,12 @@ pystencilssfg_generate_target_sources(
     TestApp
     SCRIPTS CliTest.py
     SCRIPT_ARGS apples bananas unicorns
-    OUTPUT_MODE header-only
+    HEADER_ONLY
 )
 
 pystencilssfg_generate_target_sources(
     TestApp
     SCRIPTS CustomDirTest.py
     OUTPUT_DIRECTORY my-output
-    OUTPUT_MODE header-only
+    HEADER_ONLY
 )
diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py
index d3c2585..41cdda8 100644
--- a/tests/integration/test_cli.py
+++ b/tests/integration/test_cli.py
@@ -29,8 +29,7 @@ def test_list_files_headeronly():
         "list-files",
         "--sfg-output-dir",
         output_dir,
-        "--sfg-output-mode",
-        "header-only",
+        "--sfg-header-only",
         "genscript.py",
     ]
 
-- 
GitLab


From 3f572623ac6573702b54db2894ea13edc7d17cea Mon Sep 17 00:00:00 2001
From: Frederik Hennig <frederik.hennig@fau.de>
Date: Fri, 21 Feb 2025 14:47:58 +0100
Subject: [PATCH 2/6] adapt composer to automatically generate inline defs in
 header-only mode. Add test cases.

---
 src/pystencilssfg/composer/basic_composer.py  | 53 ++++++++++++++-----
 src/pystencilssfg/composer/class_composer.py  |  5 +-
 src/pystencilssfg/context.py                  |  4 ++
 src/pystencilssfg/emission/file_printer.py    | 16 ++++--
 src/pystencilssfg/ir/entities.py              | 14 ++++-
 tests/generator_scripts/index.yaml            | 17 ++++--
 .../source/ComposerHeaderOnly.harness.cpp     | 18 +++++++
 .../source/ComposerHeaderOnly.py              | 26 +++++++++
 8 files changed, 131 insertions(+), 22 deletions(-)
 create mode 100644 tests/generator_scripts/source/ComposerHeaderOnly.harness.cpp
 create mode 100644 tests/generator_scripts/source/ComposerHeaderOnly.py

diff --git a/src/pystencilssfg/composer/basic_composer.py b/src/pystencilssfg/composer/basic_composer.py
index 49b6c73..ff1ab2d 100644
--- a/src/pystencilssfg/composer/basic_composer.py
+++ b/src/pystencilssfg/composer/basic_composer.py
@@ -88,11 +88,16 @@ SequencerArg: TypeAlias = tuple | ExprLike | SfgCallTreeNode | SfgNodeBuilder
 class KernelsAdder:
     """Handle on a kernel namespace that permits registering kernels."""
 
-    def __init__(self, ctx: SfgContext, loc: SfgNamespaceBlock):
-        self._ctx = ctx
-        self._loc = loc
-        assert isinstance(loc.namespace, SfgKernelNamespace)
-        self._kernel_namespace = loc.namespace
+    def __init__(self, cursor: SfgCursor, knamespace: SfgKernelNamespace):
+        self._cursor = cursor
+        self._kernel_namespace = knamespace
+        self._inline: bool = False
+        self._loc: SfgNamespaceBlock | None = None
+
+    def inline(self) -> KernelsAdder:
+        """Generate kernel definitions ``inline`` in the header file."""
+        self._inline = True
+        return self
 
     def add(self, kernel: Kernel, name: str | None = None):
         """Adds an existing pystencils AST to this namespace.
@@ -111,14 +116,22 @@ class KernelsAdder:
         if name is not None:
             kernel.name = kernel_name
 
-        khandle = SfgKernelHandle(kernel_name, self._kernel_namespace, kernel)
+        khandle = SfgKernelHandle(
+            kernel_name, self._kernel_namespace, kernel, inline=self._inline
+        )
         self._kernel_namespace.add_kernel(khandle)
 
-        self._loc.elements.append(SfgEntityDef(khandle))
+        loc = self._get_loc()
+        loc.elements.append(SfgEntityDef(khandle))
 
         for header in kernel.required_headers:
-            assert self._ctx.impl_file is not None
-            self._ctx.impl_file.includes.append(HeaderFile.parse(header))
+            hfile = HeaderFile.parse(header)
+            if self._inline:
+                self._cursor.context.header_file.includes.append(hfile)
+            else:
+                impl_file = self._cursor.context.impl_file
+                assert impl_file is not None
+                impl_file.includes.append(hfile)
 
         return khandle
 
@@ -147,6 +160,18 @@ class KernelsAdder:
         kernel = create_kernel(assignments, config=config)
         return self.add(kernel)
 
+    def _get_loc(self) -> SfgNamespaceBlock:
+        if self._loc is None:
+            kns_block = SfgNamespaceBlock(self._kernel_namespace)
+
+            if self._inline:
+                self._cursor.write_header(kns_block)
+            else:
+                self._cursor.write_impl(kns_block)
+
+            self._loc = kns_block
+        return self._loc
+
 
 class SfgBasicComposer(SfgIComposer):
     """Composer for basic source components, and base class for all composer mix-ins."""
@@ -281,9 +306,10 @@ class SfgBasicComposer(SfgIComposer):
                 f"The existing entity {kns.fqname} is not a kernel namespace"
             )
 
-        kns_block = SfgNamespaceBlock(kns)
-        self._cursor.write_impl(kns_block)
-        return KernelsAdder(self._ctx, kns_block)
+        kadder = KernelsAdder(self._cursor, kns)
+        if self._ctx.impl_file is None:
+            kadder.inline()
+        return kadder
 
     def include(self, header: str | HeaderFile, private: bool = False):
         """Include a header file.
@@ -356,6 +382,9 @@ class SfgBasicComposer(SfgIComposer):
             )
             seq.returns(return_type)
 
+        if self._ctx.impl_file is None:
+            seq.inline()
+
         return seq
 
     def call(self, kernel_handle: SfgKernelHandle) -> SfgCallTreeNode:
diff --git a/src/pystencilssfg/composer/class_composer.py b/src/pystencilssfg/composer/class_composer.py
index 7787150..5dbb4c6 100644
--- a/src/pystencilssfg/composer/class_composer.py
+++ b/src/pystencilssfg/composer/class_composer.py
@@ -253,7 +253,10 @@ class SfgClassComposer(SfgComposerMixIn):
             name: The method name
         """
 
-        return SfgMethodSequencer(self._cursor, name)
+        seq = SfgMethodSequencer(self._cursor, name)
+        if self._ctx.impl_file is None:
+            seq.inline()
+        return seq
 
     #   INTERNALS
 
diff --git a/src/pystencilssfg/context.py b/src/pystencilssfg/context.py
index 199c678..1622a1e 100644
--- a/src/pystencilssfg/context.py
+++ b/src/pystencilssfg/context.py
@@ -115,6 +115,10 @@ class SfgCursor:
             else:
                 self._loc[f] = f.elements
 
+    @property
+    def context(self) -> SfgContext:
+        return self._ctx
+
     @property
     def current_namespace(self) -> SfgNamespace:
         return self._cur_namespace
diff --git a/src/pystencilssfg/emission/file_printer.py b/src/pystencilssfg/emission/file_printer.py
index 765bf70..648e419 100644
--- a/src/pystencilssfg/emission/file_printer.py
+++ b/src/pystencilssfg/emission/file_printer.py
@@ -27,9 +27,7 @@ from ..config import CodeStyle
 class SfgFilePrinter:
     def __init__(self, code_style: CodeStyle) -> None:
         self._code_style = code_style
-        self._kernel_printer = CAstPrinter(
-            indent_width=code_style.get_option("indent_width")
-        )
+        self._indent_width = code_style.get_option("indent_width")
 
     def __call__(self, file: SfgSourceFile) -> str:
         code = ""
@@ -86,7 +84,11 @@ class SfgFilePrinter:
     ) -> str:
         match declared_entity:
             case SfgKernelHandle(kernel):
-                return self._kernel_printer.print_signature(kernel) + ";"
+                kernel_printer = CAstPrinter(
+                    indent_width=self._indent_width,
+                    func_prefix="inline" if declared_entity.inline else None,
+                )
+                return kernel_printer.print_signature(kernel) + ";"
 
             case SfgFunction(name, _, params) | SfgMethod(name, _, params):
                 return self._func_signature(declared_entity, inclass) + ";"
@@ -113,7 +115,11 @@ class SfgFilePrinter:
     ) -> str:
         match defined_entity:
             case SfgKernelHandle(kernel):
-                return self._kernel_printer(kernel)
+                kernel_printer = CAstPrinter(
+                    indent_width=self._indent_width,
+                    func_prefix="inline" if defined_entity.inline else None,
+                )
+                return kernel_printer(kernel)
 
             case SfgFunction(name, tree, params) | SfgMethod(name, tree, params):
                 sig = self._func_signature(defined_entity, inclass)
diff --git a/src/pystencilssfg/ir/entities.py b/src/pystencilssfg/ir/entities.py
index 40abb14..0edde22 100644
--- a/src/pystencilssfg/ir/entities.py
+++ b/src/pystencilssfg/ir/entities.py
@@ -141,12 +141,20 @@ class SfgKernelHandle(SfgCodeEntity):
 
     __match_args__ = ("kernel", "parameters")
 
-    def __init__(self, name: str, namespace: SfgKernelNamespace, kernel: Kernel):
+    def __init__(
+        self,
+        name: str,
+        namespace: SfgKernelNamespace,
+        kernel: Kernel,
+        inline: bool = False,
+    ):
         super().__init__(name, namespace)
 
         self._kernel = kernel
         self._parameters = [SfgKernelParamVar(p) for p in kernel.parameters]
 
+        self._inline: bool = inline
+
         self._scalar_params: set[SfgVar] = set()
         self._fields: set[Field] = set()
 
@@ -176,6 +184,10 @@ class SfgKernelHandle(SfgCodeEntity):
         """Underlying pystencils kernel object"""
         return self._kernel
 
+    @property
+    def inline(self) -> bool:
+        return self._inline
+
 
 class SfgKernelNamespace(SfgNamespace):
     """A namespace grouping together a number of kernels."""
diff --git a/tests/generator_scripts/index.yaml b/tests/generator_scripts/index.yaml
index 5e2db9a..e7db347 100644
--- a/tests/generator_scripts/index.yaml
+++ b/tests/generator_scripts/index.yaml
@@ -53,6 +53,16 @@ ComposerFeatures:
       - 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\sint32_t\stwice\(
+      - regex: >-
+          inline\svoid\skernel\(
+
 Conditionals:
   expect-code:
     cpp:
@@ -84,10 +94,11 @@ MdSpanLbStreaming:
 
 SyclKernels:
   sfg-args:
-    output-mode: inline
-    file-extensions: [hpp, ipp]
+    header-only: true
   expect-code:
-    ipp:
+    hpp:
+      - regex: >-
+          inline\svoid\skernel\(
       - regex: >-
           cgh\.parallel_for\(range,\s*\[=\]\s*\(const\s+sycl::item<\s*2\s*>\s+sycl_item\s*\)\s*\{\s*kernels::kernel\(.*\);\s*\}\);
 
diff --git a/tests/generator_scripts/source/ComposerHeaderOnly.harness.cpp b/tests/generator_scripts/source/ComposerHeaderOnly.harness.cpp
new file mode 100644
index 0000000..f6c549a
--- /dev/null
+++ b/tests/generator_scripts/source/ComposerHeaderOnly.harness.cpp
@@ -0,0 +1,18 @@
+#include "ComposerHeaderOnly.hpp"
+
+#include <vector>
+
+#undef NDEBUG
+#include <cassert>
+
+int main(void) {
+    assert( twice(13) == 26 );
+
+    std::vector< int64_t > arr { 1, 2, 3, 4, 5, 6 };
+    twiceKernel(arr);
+
+    std::vector< int64_t > expected { 2, 4, 6, 8, 10, 12 };
+    assert ( arr == expected );
+
+    return 0;
+}
diff --git a/tests/generator_scripts/source/ComposerHeaderOnly.py b/tests/generator_scripts/source/ComposerHeaderOnly.py
new file mode 100644
index 0000000..0bd1a07
--- /dev/null
+++ b/tests/generator_scripts/source/ComposerHeaderOnly.py
@@ -0,0 +1,26 @@
+from pystencilssfg import SourceFileGenerator, SfgConfig
+from pystencilssfg.lang.cpp import std
+import pystencils as ps
+
+cfg = SfgConfig(header_only=True)
+
+with SourceFileGenerator(cfg) as sfg:
+    n = sfg.var("n", "int32")
+
+    #   Should be automatically marked inline
+    sfg.function("twice").returns("int32")(
+        sfg.expr("return 2 * {};", n)
+    )
+
+    #   Inline kernel
+
+    arr = ps.fields("arr: int64[1D]")
+    vec = std.vector.from_field(arr)
+
+    asm = ps.Assignment(arr(0), 2 * arr(0))
+    khandle = sfg.kernels.create(asm)
+
+    sfg.function("twiceKernel")(
+        sfg.map_field(arr, vec),
+        sfg.call(khandle)
+    )
-- 
GitLab


From f283398e82abacd34d18996d9ea13d68c6e5e39b Mon Sep 17 00:00:00 2001
From: Frederik Hennig <frederik.hennig@fau.de>
Date: Fri, 21 Feb 2025 14:48:38 +0100
Subject: [PATCH 3/6] code formatting

---
 src/pystencilssfg/config.py | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/pystencilssfg/config.py b/src/pystencilssfg/config.py
index 482ca8e..34d2f27 100644
--- a/src/pystencilssfg/config.py
+++ b/src/pystencilssfg/config.py
@@ -5,7 +5,6 @@ from argparse import ArgumentParser, BooleanOptionalAction
 from types import ModuleType
 from typing import Any, Sequence, Callable
 from dataclasses import dataclass
-from enum import Enum, auto
 from os import path
 from importlib import util as iutil
 from pathlib import Path
@@ -120,7 +119,7 @@ class SfgConfig(ConfigBase):
 
     header_only: BasicOption[bool] = BasicOption(False)
     """If set to `True`, generate only a header file.
-    
+
     This will cause all definitions to be generated ``inline``.
     """
 
@@ -192,7 +191,7 @@ class CommandLineParameters:
             "--sfg-header-only",
             action=BooleanOptionalAction,
             dest="header_only",
-            help="Generate only a header file."
+            help="Generate only a header file.",
         )
         config_group.add_argument(
             "--sfg-config-module", type=str, default=None, dest="config_module_path"
-- 
GitLab


From c0c1aebe8ea34b6c31f182a9e3afbdbf91182b59 Mon Sep 17 00:00:00 2001
From: Frederik Hennig <frederik.hennig@fau.de>
Date: Fri, 21 Feb 2025 14:52:47 +0100
Subject: [PATCH 4/6] attempt to fix regexes

---
 tests/generator_scripts/index.yaml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/tests/generator_scripts/index.yaml b/tests/generator_scripts/index.yaml
index e7db347..1c97aaf 100644
--- a/tests/generator_scripts/index.yaml
+++ b/tests/generator_scripts/index.yaml
@@ -59,9 +59,9 @@ ComposerHeaderOnly:
   expect-code:
     hpp:
       - regex: >-
-          inline\sint32_t\stwice\(
+          inline\s+int32_t\s+twice\s*\(
       - regex: >-
-          inline\svoid\skernel\(
+          inline\s+void\s+kernel\s*\(
 
 Conditionals:
   expect-code:
@@ -98,7 +98,7 @@ SyclKernels:
   expect-code:
     hpp:
       - regex: >-
-          inline\svoid\skernel\(
+          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*\}\);
 
-- 
GitLab


From ec46da109cc51b60a7fdfc501e27b01a7e32c2df Mon Sep 17 00:00:00 2001
From: Frederik Hennig <frederik.hennig@fau.de>
Date: Fri, 21 Feb 2025 15:04:34 +0100
Subject: [PATCH 5/6] Add section on header-only mode to docs

---
 docs/source/usage/config_and_cli.md | 22 +++++++++++++++++-----
 1 file changed, 17 insertions(+), 5 deletions(-)

diff --git a/docs/source/usage/config_and_cli.md b/docs/source/usage/config_and_cli.md
index 80169b1..785ff52 100644
--- a/docs/source/usage/config_and_cli.md
+++ b/docs/source/usage/config_and_cli.md
@@ -42,14 +42,15 @@ The file extensions of the generated files can be modified through
 {any}`cfg.extensions.header <FileExtensions.header>`
 and {any}`cfg.extensions.impl <FileExtensions.impl>`;
 and the output directory of the code generator can be set through {any}`cfg.output_directory <SfgConfig.output_directory>`.
-Header-only code generation can be enabled using {any}`cfg.header_only <SfgConfig.header_only>`.
+The [header-only mode](#header_only_mode) can be enabled using {any}`cfg.header_only <SfgConfig.header_only>`.
 
 :::{danger}
 
-When running generator scripts through [CMake](#cmake_integration), you should *never* set the file extensions
-and the output directory in the inline configuration.
-Both are managed by the pystencils-sfg CMake module, and setting them manually inside the script will
-lead to an error.
+When running generator scripts through [CMake](#cmake_integration), the file extensions,
+output directory, and header-only mode settings will be managed fully by the pystencils-sfg
+CMake module and the (optional) project configuration module.
+They should therefore not be set in the inline configuration,
+as this will likely lead to errors being raised during code generation.
 :::
 
 ### Outer Namespace
@@ -89,6 +90,17 @@ with the `--help` flag:
 $ python kernels.py --help
 ```
 
+(header_only_mode)=
+## Header-Only Mode
+
+When the header-only output mode is enabled,
+the code generator will emit only a header file and no separate implementation file.
+In this case, the composer will automatically place all function, method,
+and kernel definitions in the header file.
+
+Header-only code generation can be enabled by setting the `--header-only` command-line flag
+or the {any}`SfgConfig.header_only` configuration option.
+
 (custom_cli_args)=
 ## Adding Custom Command-Line Options
 
-- 
GitLab


From f91ed584cbb2dddd1ede1bf638589f2a146a427f Mon Sep 17 00:00:00 2001
From: Frederik Hennig <frederik.hennig@fau.de>
Date: Thu, 27 Feb 2025 17:46:35 +0100
Subject: [PATCH 6/6] add class method and constructor generation to
 header-only test

---
 .../source/ComposerHeaderOnly.harness.cpp     | 20 ++++++++++++++----
 .../source/ComposerHeaderOnly.py              | 21 ++++++++++++++++++-
 2 files changed, 36 insertions(+), 5 deletions(-)

diff --git a/tests/generator_scripts/source/ComposerHeaderOnly.harness.cpp b/tests/generator_scripts/source/ComposerHeaderOnly.harness.cpp
index f6c549a..76f2de4 100644
--- a/tests/generator_scripts/source/ComposerHeaderOnly.harness.cpp
+++ b/tests/generator_scripts/source/ComposerHeaderOnly.harness.cpp
@@ -8,11 +8,23 @@
 int main(void) {
     assert( twice(13) == 26 );
 
-    std::vector< int64_t > arr { 1, 2, 3, 4, 5, 6 };
-    twiceKernel(arr);
+    {
+        std::vector< int64_t > arr { 1, 2, 3, 4, 5, 6 };
+        twiceKernel(arr);
 
-    std::vector< int64_t > expected { 2, 4, 6, 8, 10, 12 };
-    assert ( arr == expected );
+        std::vector< int64_t > expected { 2, 4, 6, 8, 10, 12 };
+        assert ( arr == expected );
+    }
+    
+    {
+        std::vector< int64_t > arr { 1, 2, 3, 4, 5, 6 };
+        ScaleKernel ker { 3 };
+
+        ker( arr );
+
+        std::vector< int64_t > expected { 3, 6, 9, 12, 15, 18 };
+        assert ( arr == expected );
+    }
 
     return 0;
 }
diff --git a/tests/generator_scripts/source/ComposerHeaderOnly.py b/tests/generator_scripts/source/ComposerHeaderOnly.py
index 0bd1a07..3881457 100644
--- a/tests/generator_scripts/source/ComposerHeaderOnly.py
+++ b/tests/generator_scripts/source/ComposerHeaderOnly.py
@@ -17,10 +17,29 @@ with SourceFileGenerator(cfg) as sfg:
     arr = ps.fields("arr: int64[1D]")
     vec = std.vector.from_field(arr)
 
-    asm = ps.Assignment(arr(0), 2 * arr(0))
+    c = ps.TypedSymbol("c", "int64")
+
+    asm = ps.Assignment(arr(0), c * arr(0))
     khandle = sfg.kernels.create(asm)
 
     sfg.function("twiceKernel")(
         sfg.map_field(arr, vec),
+        sfg.set_param(c, "2"),
         sfg.call(khandle)
     )
+
+    #   Inline class members
+
+    sfg.klass("ScaleKernel")(
+        sfg.private(
+            c
+        ),
+        sfg.public(
+            sfg.constructor(c).init(c)(c),
+            sfg.method("operator()")(
+                sfg.map_field(arr, vec),
+                sfg.set_param(c, "this->c"),
+                sfg.call(khandle)
+            )
+        )
+    )
-- 
GitLab