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] 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