fprime-cpp-codegen 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/PKG-INFO +73 -1
  2. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/README.md +72 -0
  3. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/__init__.py +8 -0
  4. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/body.py +16 -8
  5. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/builder/coercion.py +11 -0
  6. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/builder/decoration.py +3 -2
  7. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/builder/definitions.py +31 -9
  8. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/builder/document.py +93 -8
  9. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/builder/scopes.py +307 -61
  10. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/comments.py +77 -14
  11. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/doc.py +43 -8
  12. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/lines.py +12 -2
  13. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/output.py +48 -16
  14. fprime_cpp_codegen-0.2.0/src/fprime_cpp_codegen/validation.py +256 -0
  15. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/writer.py +64 -16
  16. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen.egg-info/PKG-INFO +73 -1
  17. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen.egg-info/SOURCES.txt +3 -0
  18. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen.egg-info/scm_file_list.json +3 -0
  19. fprime_cpp_codegen-0.2.0/src/fprime_cpp_codegen.egg-info/scm_version.json +8 -0
  20. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/test_body.py +29 -0
  21. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/test_builder.py +162 -4
  22. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/test_comments.py +79 -1
  23. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/test_lines.py +27 -0
  24. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/test_output.py +98 -2
  25. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/test_readme.py +59 -0
  26. fprime_cpp_codegen-0.2.0/tests/test_signatures.py +130 -0
  27. fprime_cpp_codegen-0.2.0/tests/test_validation.py +414 -0
  28. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/test_writer_constructs.py +179 -2
  29. fprime_cpp_codegen-0.1.0/src/fprime_cpp_codegen.egg-info/scm_version.json +0 -8
  30. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/.github/workflows/ci.yml +0 -0
  31. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/.github/workflows/format.yml +0 -0
  32. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/.github/workflows/publish.yml +0 -0
  33. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/.gitignore +0 -0
  34. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/LICENSE +0 -0
  35. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/examples/fpp_constants.py +0 -0
  36. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/examples/fpp_enum.py +0 -0
  37. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/examples/ring_buffer.py +0 -0
  38. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/pyproject.toml +0 -0
  39. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/setup.cfg +0 -0
  40. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/builder/__init__.py +0 -0
  41. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/builder/base.py +0 -0
  42. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/errors.py +0 -0
  43. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/formatting.py +0 -0
  44. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/fprime.py +0 -0
  45. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen/py.typed +0 -0
  46. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen.egg-info/dependency_links.txt +0 -0
  47. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen.egg-info/requires.txt +0 -0
  48. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/src/fprime_cpp_codegen.egg-info/top_level.txt +0 -0
  49. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/__init__.py +0 -0
  50. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/conftest.py +0 -0
  51. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/goldens/C.cpp +0 -0
  52. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/goldens/C.hpp +0 -0
  53. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/goldens/Other.cpp +0 -0
  54. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/goldens/fpp/EEnumAc.ref.cpp +0 -0
  55. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/goldens/fpp/EEnumAc.ref.hpp +0 -0
  56. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/goldens/fpp/FppConstantsAc.ref.cpp +0 -0
  57. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/goldens/fpp/FppConstantsAc.ref.hpp +0 -0
  58. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/test_examples.py +0 -0
  59. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/test_formatting.py +0 -0
  60. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/test_fpp_examples.py +0 -0
  61. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/test_fprime.py +0 -0
  62. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/test_golden_builder.py +0 -0
  63. {fprime_cpp_codegen-0.1.0 → fprime_cpp_codegen-0.2.0}/tests/test_golden_ir.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fprime-cpp-codegen
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A Python package for generating C++ code for F Prime
5
5
  Author: Andrei Tumbar
6
6
  License: Apache License
@@ -310,6 +310,78 @@ create it, so you can keep filling it in afterwards. It is worth using on access
310
310
  sections and preprocessor guards, which delete themselves when nothing lands
311
311
  inside them.
312
312
 
313
+ ## Text that comes from your model
314
+
315
+ Multi-line strings are margin-stripped: on each line, leading whitespace followed by
316
+ a `|` is dropped, so a block of C++ can be indented to match the Python around it.
317
+ That is convenient for literals written by hand and a hazard for text a generator
318
+ derives from its input — an FPP annotation used as a doc comment, a C++ expression
319
+ whose continuation line starts with `|`, would silently lose that character.
320
+
321
+ Three escapes, in order of preference:
322
+
323
+ ```python
324
+ doc.lines(expression, margin=None) # no stripping at all
325
+ body.line(expression) # one line, never stripped
326
+ cls.function("f", comment=lines(text, margin=None)) # comments take Lines, too
327
+ ```
328
+
329
+ Every `comment=` argument accepts either a string, which is stripped, or a
330
+ `Sequence[Line]`, which is not. `Body.line()` and `Body.raw()` never strip;
331
+ `Body.lines()` and the scope-level `lines()` take `margin=None`. Doubling the marker
332
+ (`||x`) also works, since only the first one per line is removed.
333
+
334
+ Separately, a line whose text begins with a recognised preprocessor directive —
335
+ `#include`, `#if`, `#pragma` and the rest — is forced to column zero, wherever in the
336
+ document it sits. Content that merely starts with `#`, such as a Markdown heading
337
+ inside a C++ string literal, keeps its indentation. A real directive that has to keep
338
+ its column carries the spaces in the text (`line(" #include <x>")`) rather than in
339
+ the line's indent.
340
+
341
+ ## Extension points
342
+
343
+ Declarations take attributes, which is what a shared object's exported symbols need.
344
+ On a class they land between the keyword and the name, leaving the name — and so the
345
+ constructor, the destructor and every `MyComp ::` qualifier — untouched:
346
+
347
+ ```python
348
+ cls = ns.class_("MyComp", extends="public MyCompComponentBase",
349
+ attributes='__attribute__((visibility("default")))')
350
+ cls.function("get", ret="U32", attributes="[[nodiscard]]", body="return m_v;")
351
+ ```
352
+
353
+ A document does not have to be two files. `emit_hpp=False` gives a standalone
354
+ translation unit — a `PYBIND11_MODULE` block with no header to declare — and
355
+ `emit_cpp=False` a header-only one. Either way, anything the dropped file was the
356
+ only home for raises a `ValidationError` naming it, rather than disappearing:
357
+
358
+ ```python
359
+ doc = CppDocBuilder("MyCompModule", emit_hpp=False, strict=True)
360
+ ```
361
+
362
+ `strict=True` additionally rejects a definition that needs a body and has none, which
363
+ otherwise renders as an empty out-of-line definition — valid C++, and so easy for a
364
+ generator to emit by accident. Say `declaration_only=True` for a header-only
365
+ declaration, or `body=""` for a definition that is deliberately empty.
366
+ `fprime_cpp_codegen.validation` reports both classes of problem without raising.
367
+
368
+ To change how something renders, subclass `HppWriter` or `CppWriter` and pass the
369
+ instance in. Every output path takes one, so overriding a single method costs you
370
+ nothing else — `write()` keeps its directory creation, its unchanged-file mtime
371
+ handling and its formatter:
372
+
373
+ ```python
374
+ class GuardlessHpp(HppWriter):
375
+ def open_include_guard(self, guard: str) -> list[Line]:
376
+ return lines("#pragma once")
377
+
378
+ def close_include_guard(self) -> list[Line]:
379
+ return []
380
+
381
+ doc = CppDocBuilder("MyComp", hpp_writer=GuardlessHpp())
382
+ doc.write("build-artifacts") # or pass writer= / hpp_writer= per call
383
+ ```
384
+
313
385
  ## Examples
314
386
 
315
387
  Run any of these to print the C++ they generate, or pass a directory to write it:
@@ -77,6 +77,78 @@ create it, so you can keep filling it in afterwards. It is worth using on access
77
77
  sections and preprocessor guards, which delete themselves when nothing lands
78
78
  inside them.
79
79
 
80
+ ## Text that comes from your model
81
+
82
+ Multi-line strings are margin-stripped: on each line, leading whitespace followed by
83
+ a `|` is dropped, so a block of C++ can be indented to match the Python around it.
84
+ That is convenient for literals written by hand and a hazard for text a generator
85
+ derives from its input — an FPP annotation used as a doc comment, a C++ expression
86
+ whose continuation line starts with `|`, would silently lose that character.
87
+
88
+ Three escapes, in order of preference:
89
+
90
+ ```python
91
+ doc.lines(expression, margin=None) # no stripping at all
92
+ body.line(expression) # one line, never stripped
93
+ cls.function("f", comment=lines(text, margin=None)) # comments take Lines, too
94
+ ```
95
+
96
+ Every `comment=` argument accepts either a string, which is stripped, or a
97
+ `Sequence[Line]`, which is not. `Body.line()` and `Body.raw()` never strip;
98
+ `Body.lines()` and the scope-level `lines()` take `margin=None`. Doubling the marker
99
+ (`||x`) also works, since only the first one per line is removed.
100
+
101
+ Separately, a line whose text begins with a recognised preprocessor directive —
102
+ `#include`, `#if`, `#pragma` and the rest — is forced to column zero, wherever in the
103
+ document it sits. Content that merely starts with `#`, such as a Markdown heading
104
+ inside a C++ string literal, keeps its indentation. A real directive that has to keep
105
+ its column carries the spaces in the text (`line(" #include <x>")`) rather than in
106
+ the line's indent.
107
+
108
+ ## Extension points
109
+
110
+ Declarations take attributes, which is what a shared object's exported symbols need.
111
+ On a class they land between the keyword and the name, leaving the name — and so the
112
+ constructor, the destructor and every `MyComp ::` qualifier — untouched:
113
+
114
+ ```python
115
+ cls = ns.class_("MyComp", extends="public MyCompComponentBase",
116
+ attributes='__attribute__((visibility("default")))')
117
+ cls.function("get", ret="U32", attributes="[[nodiscard]]", body="return m_v;")
118
+ ```
119
+
120
+ A document does not have to be two files. `emit_hpp=False` gives a standalone
121
+ translation unit — a `PYBIND11_MODULE` block with no header to declare — and
122
+ `emit_cpp=False` a header-only one. Either way, anything the dropped file was the
123
+ only home for raises a `ValidationError` naming it, rather than disappearing:
124
+
125
+ ```python
126
+ doc = CppDocBuilder("MyCompModule", emit_hpp=False, strict=True)
127
+ ```
128
+
129
+ `strict=True` additionally rejects a definition that needs a body and has none, which
130
+ otherwise renders as an empty out-of-line definition — valid C++, and so easy for a
131
+ generator to emit by accident. Say `declaration_only=True` for a header-only
132
+ declaration, or `body=""` for a definition that is deliberately empty.
133
+ `fprime_cpp_codegen.validation` reports both classes of problem without raising.
134
+
135
+ To change how something renders, subclass `HppWriter` or `CppWriter` and pass the
136
+ instance in. Every output path takes one, so overriding a single method costs you
137
+ nothing else — `write()` keeps its directory creation, its unchanged-file mtime
138
+ handling and its formatter:
139
+
140
+ ```python
141
+ class GuardlessHpp(HppWriter):
142
+ def open_include_guard(self, guard: str) -> list[Line]:
143
+ return lines("#pragma once")
144
+
145
+ def close_include_guard(self) -> list[Line]:
146
+ return []
147
+
148
+ doc = CppDocBuilder("MyComp", hpp_writer=GuardlessHpp())
149
+ doc.write("build-artifacts") # or pass writer= / hpp_writer= per call
150
+ ```
151
+
80
152
  ## Examples
81
153
 
82
154
  Run any of these to print the C++ they generate, or pass a directory to write it:
@@ -10,6 +10,7 @@ The package is layered, and you can enter at whichever level suits the job:
10
10
  * :mod:`~fprime_cpp_codegen.comments` -- comment and banner formatting.
11
11
  * :mod:`~fprime_cpp_codegen.lines` -- the line model everything is built on.
12
12
  * :mod:`~fprime_cpp_codegen.output` -- rendering to text and to disk.
13
+ * :mod:`~fprime_cpp_codegen.validation` -- whole-document checks.
13
14
  * :mod:`~fprime_cpp_codegen.formatting` -- optional post-processing through
14
15
  ``clang-format``.
15
16
  * :mod:`~fprime_cpp_codegen.fprime` -- F Prime conventions. Import it explicitly;
@@ -52,6 +53,7 @@ from .doc import (
52
53
  VOID,
53
54
  Class,
54
55
  ClassMember,
56
+ Comment,
55
57
  Constructor,
56
58
  CppDoc,
57
59
  DefaultFileBanner,
@@ -83,6 +85,7 @@ from .lines import (
83
85
  wrap_in_scope,
84
86
  )
85
87
  from .output import WriteResult, collect_cpp_files, doc_files, write_doc
88
+ from .validation import check_document, orphaned_members, unfilled_definitions
86
89
  from .writer import (
87
90
  Context,
88
91
  CppWriter,
@@ -108,6 +111,7 @@ __all__ = [
108
111
  "VOID",
109
112
  "Class",
110
113
  "ClassMember",
114
+ "Comment",
111
115
  "Constructor",
112
116
  "CppDoc",
113
117
  "DefaultFileBanner",
@@ -155,6 +159,10 @@ __all__ = [
155
159
  "collect_cpp_files",
156
160
  "doc_files",
157
161
  "write_doc",
162
+ # Validation
163
+ "check_document",
164
+ "orphaned_members",
165
+ "unfilled_definitions",
158
166
  # Errors
159
167
  "CppCodegenError",
160
168
  "ScopeError",
@@ -34,6 +34,7 @@ from .comments import (
34
34
  write_comment_body,
35
35
  write_doxygen_comment,
36
36
  )
37
+ from .doc import Comment
37
38
  from .errors import ScopeError, ValidationError
38
39
  from .lines import Line, blank, indent_lines
39
40
  from .lines import line as _line
@@ -44,7 +45,9 @@ __all__ = ["Body", "Code", "Switch", "stmts"]
44
45
 
45
46
  #: Anything usable as a run of C++ statements. ``None`` contributes nothing, so
46
47
  #: ``b.add(frag if condition else None)`` needs no branch. A ``str`` is
47
- #: margin-stripped and taken verbatim, with no punctuation added.
48
+ #: margin-stripped and taken verbatim, with no punctuation added; pass a
49
+ #: :class:`~fprime_cpp_codegen.lines.Line` instead, or use :meth:`Body.line`, for text
50
+ #: that must survive a leading ``|``.
48
51
  Code: TypeAlias = "None | str | Line | Body | Sequence[Code]"
49
52
 
50
53
 
@@ -216,9 +219,14 @@ class Body:
216
219
  """Append one line verbatim."""
217
220
  return self._emit([_line(text)])
218
221
 
219
- def lines(self, text: str) -> Body:
220
- """Append a margin-stripped, possibly multi-line block of C++."""
221
- return self._emit(_lines(text))
222
+ def lines(self, text: str, *, margin: str | None = "|") -> Body:
223
+ """Append a margin-stripped, possibly multi-line block of C++.
224
+
225
+ ``margin=None`` turns the stripping off, for text taken from a generator's
226
+ input that may legitimately begin with the margin character. :meth:`line`
227
+ never strips.
228
+ """
229
+ return self._emit(_lines(text, margin=margin))
222
230
 
223
231
  def raw(self, ll: Iterable[Line]) -> Body:
224
232
  """Append already-rendered lines."""
@@ -241,19 +249,19 @@ class Body:
241
249
  # Comments
242
250
  # ------------------------------------------------------------------
243
251
 
244
- def comment(self, text: str) -> Body:
252
+ def comment(self, text: Comment) -> Body:
245
253
  """Append a ``//`` comment with no leading blank line."""
246
254
  return self._emit(write_comment_body(text))
247
255
 
248
- def spaced_comment(self, text: str) -> Body:
256
+ def spaced_comment(self, text: Comment) -> Body:
249
257
  """Append a ``//`` comment preceded by a blank line."""
250
258
  return self._emit(write_comment(text))
251
259
 
252
- def doc_comment(self, text: str) -> Body:
260
+ def doc_comment(self, text: Comment) -> Body:
253
261
  """Append a ``//!`` doxygen comment."""
254
262
  return self._emit(write_doxygen_comment(text))
255
263
 
256
- def banner(self, text: str) -> Body:
264
+ def banner(self, text: Comment) -> Body:
257
265
  """Append a ruled banner comment."""
258
266
  return self._emit(write_banner_comment(text))
259
267
 
@@ -95,3 +95,14 @@ def _extends(extends: str | Sequence[str] | None) -> str | None:
95
95
  return extends
96
96
  joined = ", ".join(extends)
97
97
  return joined or None
98
+
99
+
100
+ def _as_attributes(attributes: str | Sequence[str]) -> tuple[str, ...]:
101
+ """Normalise a declaration-attribute specification into a tuple.
102
+
103
+ A single string is one attribute, not a sequence of characters, since one is the
104
+ common case: ``attributes='__attribute__((visibility("default")))'``.
105
+ """
106
+ if isinstance(attributes, str):
107
+ return (attributes,)
108
+ return tuple(attributes)
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any
12
12
  from ..comments import write_access_tag, write_banner_comment
13
13
  from ..doc import (
14
14
  Class,
15
+ Comment,
15
16
  Constructor,
16
17
  Destructor,
17
18
  Function,
@@ -90,7 +91,7 @@ class _SectionBanner(_Builder[Lines]):
90
91
  definitions went.
91
92
  """
92
93
 
93
- def __init__(self, comment: str) -> None:
94
+ def __init__(self, comment: Comment) -> None:
94
95
  self.comment = comment
95
96
  self.members: list[object] = []
96
97
 
@@ -179,7 +180,7 @@ class AccessSection:
179
180
  its label and banner back out again.
180
181
  """
181
182
 
182
- def __init__(self, scope: ClassBuilder, tag: str, comment: str | None) -> None:
183
+ def __init__(self, scope: ClassBuilder, tag: str, comment: Comment | None) -> None:
183
184
  self._scope = scope
184
185
  self._count = 1
185
186
  scope._add(Lines(write_access_tag(tag), Output.HPP))
@@ -11,12 +11,22 @@ from ..comments import (
11
11
  write_doxygen_comment,
12
12
  write_doxygen_comment_opt,
13
13
  )
14
- from ..doc import Constructor, Destructor, Function, Lines, Output, Param, Type, as_type
14
+ from ..doc import (
15
+ Comment,
16
+ Constructor,
17
+ Destructor,
18
+ Function,
19
+ Lines,
20
+ Output,
21
+ Param,
22
+ Type,
23
+ as_type,
24
+ )
15
25
  from ..errors import ValidationError
16
26
  from ..lines import Line, wrap_in_scope
17
27
  from ..lines import line as _line
18
28
  from .base import _Builder
19
- from .coercion import _as_body_lines, _as_params, _sv_qualifier
29
+ from .coercion import _as_attributes, _as_body_lines, _as_params, _sv_qualifier
20
30
 
21
31
 
22
32
  class Radix(Enum):
@@ -53,7 +63,7 @@ class FunctionBuilder(_Builder[Function]):
53
63
  *,
54
64
  ret: Type | str = "void",
55
65
  params: Iterable[Param | Sequence[str]] = (),
56
- comment: str | None = None,
66
+ comment: Comment | None = None,
57
67
  body: Code = None,
58
68
  const: bool = False,
59
69
  static: bool = False,
@@ -66,6 +76,8 @@ class FunctionBuilder(_Builder[Function]):
66
76
  noexcept: bool = False,
67
77
  deleted: bool = False,
68
78
  defaulted: bool = False,
79
+ declaration_only: bool = False,
80
+ attributes: str | Sequence[str] = (),
69
81
  template: str | None = None,
70
82
  inline_body: bool = False,
71
83
  cpp_file: str | None = None,
@@ -81,6 +93,8 @@ class FunctionBuilder(_Builder[Function]):
81
93
  self.noexcept = noexcept
82
94
  self.deleted = deleted
83
95
  self.defaulted = defaulted
96
+ self.declaration_only = declaration_only
97
+ self.attributes = _as_attributes(attributes)
84
98
  self.template = template
85
99
  self.inline_body = inline_body
86
100
  self.cpp_file = cpp_file
@@ -99,7 +113,7 @@ class FunctionBuilder(_Builder[Function]):
99
113
  type_name: Type | str,
100
114
  name: str,
101
115
  *,
102
- comment: str | None = None,
116
+ comment: Comment | None = None,
103
117
  default: str | None = None,
104
118
  ) -> FunctionBuilder:
105
119
  """Append one formal parameter. Returns self, so calls can be chained."""
@@ -125,6 +139,8 @@ class FunctionBuilder(_Builder[Function]):
125
139
  noexcept=self.noexcept,
126
140
  deleted=self.deleted,
127
141
  defaulted=self.defaulted,
142
+ declaration_only=self.declaration_only,
143
+ attributes=self.attributes,
128
144
  template=self.template,
129
145
  inline_body=self.inline_body,
130
146
  cpp_file=self.cpp_file,
@@ -145,13 +161,14 @@ class ConstructorBuilder(_Builder[Constructor]):
145
161
  *,
146
162
  params: Iterable[Param | Sequence[str]] = (),
147
163
  initializers: Iterable[str] = (),
148
- comment: str | None = None,
164
+ comment: Comment | None = None,
149
165
  body: Code = None,
150
166
  explicit: bool = False,
151
167
  constexpr: bool = False,
152
168
  noexcept: bool = False,
153
169
  deleted: bool = False,
154
170
  defaulted: bool = False,
171
+ declaration_only: bool = False,
155
172
  template: str | None = None,
156
173
  inline_body: bool = False,
157
174
  cpp_file: str | None = None,
@@ -162,6 +179,7 @@ class ConstructorBuilder(_Builder[Constructor]):
162
179
  self.noexcept = noexcept
163
180
  self.deleted = deleted
164
181
  self.defaulted = defaulted
182
+ self.declaration_only = declaration_only
165
183
  self.template = template
166
184
  self.inline_body = inline_body
167
185
  self.cpp_file = cpp_file
@@ -174,7 +192,7 @@ class ConstructorBuilder(_Builder[Constructor]):
174
192
  type_name: Type | str,
175
193
  name: str,
176
194
  *,
177
- comment: str | None = None,
195
+ comment: Comment | None = None,
178
196
  default: str | None = None,
179
197
  ) -> ConstructorBuilder:
180
198
  """Append one formal parameter."""
@@ -202,6 +220,7 @@ class ConstructorBuilder(_Builder[Constructor]):
202
220
  noexcept=self.noexcept,
203
221
  deleted=self.deleted,
204
222
  defaulted=self.defaulted,
223
+ declaration_only=self.declaration_only,
205
224
  template=self.template,
206
225
  inline_body=self.inline_body,
207
226
  cpp_file=self.cpp_file,
@@ -220,13 +239,14 @@ class DestructorBuilder(_Builder[Destructor]):
220
239
  def __init__(
221
240
  self,
222
241
  *,
223
- comment: str | None = None,
242
+ comment: Comment | None = None,
224
243
  body: Code = None,
225
244
  virtual: bool = False,
226
245
  override: bool = False,
227
246
  noexcept: bool = False,
228
247
  deleted: bool = False,
229
248
  defaulted: bool = False,
249
+ declaration_only: bool = False,
230
250
  inline_body: bool = False,
231
251
  cpp_file: str | None = None,
232
252
  ) -> None:
@@ -236,6 +256,7 @@ class DestructorBuilder(_Builder[Destructor]):
236
256
  self.noexcept = noexcept
237
257
  self.deleted = deleted
238
258
  self.defaulted = defaulted
259
+ self.declaration_only = declaration_only
239
260
  self.inline_body = inline_body
240
261
  self.cpp_file = cpp_file
241
262
  self.body = Body(_as_body_lines(body))
@@ -249,6 +270,7 @@ class DestructorBuilder(_Builder[Destructor]):
249
270
  noexcept=self.noexcept,
250
271
  deleted=self.deleted,
251
272
  defaulted=self.defaulted,
273
+ declaration_only=self.declaration_only,
252
274
  inline_body=self.inline_body,
253
275
  cpp_file=self.cpp_file,
254
276
  )
@@ -269,7 +291,7 @@ class EnumBuilder(_Builder[Lines]):
269
291
  *,
270
292
  underlying: str | None = None,
271
293
  scoped: bool = False,
272
- comment: str | None = None,
294
+ comment: Comment | None = None,
273
295
  output: Output = Output.HPP,
274
296
  cpp_file: str | None = None,
275
297
  radix: Radix = Radix.DECIMAL,
@@ -323,7 +345,7 @@ class EnumBuilder(_Builder[Lines]):
323
345
  name: str,
324
346
  value: int | str | None = None,
325
347
  *,
326
- comment: str | None = None,
348
+ comment: Comment | None = None,
327
349
  radix: Radix | None = None,
328
350
  ) -> EnumBuilder:
329
351
  """Append one enumerator.
@@ -10,7 +10,8 @@ from ..doc import CppDoc, FileBanner, HppFile
10
10
  from ..errors import ValidationError
11
11
  from ..formatting import Formatter
12
12
  from ..output import WriteResult, doc_files, write_doc
13
- from ..writer import render_cpp, render_hpp
13
+ from ..validation import check_document
14
+ from ..writer import CppWriter, HppWriter, render_cpp, render_hpp
14
15
  from .base import _DocContext
15
16
  from .scopes import _MemberScope
16
17
 
@@ -28,8 +29,13 @@ class CppDocBuilder(_MemberScope[CppDoc]):
28
29
  tool_name: str | None = None,
29
30
  file_banner: FileBanner | None = None,
30
31
  formatter: Formatter | None = None,
32
+ hpp_writer: HppWriter | None = None,
33
+ cpp_writer: CppWriter | None = None,
31
34
  hpp_extension: str = "hpp",
32
35
  cpp_extension: str = "cpp",
36
+ emit_hpp: bool = True,
37
+ emit_cpp: bool = True,
38
+ strict: bool = False,
33
39
  ) -> None:
34
40
  """Start a document whose files are named after ``file_base``.
35
41
 
@@ -39,7 +45,20 @@ class CppDocBuilder(_MemberScope[CppDoc]):
39
45
 
40
46
  ``formatter`` post-processes every file this document renders; see
41
47
  :mod:`fprime_cpp_codegen.formatting`. Any render or write call can override
42
- it, but cannot switch it off.
48
+ it, but cannot switch it off. ``hpp_writer`` and ``cpp_writer`` work the same
49
+ way, substituting a :class:`~fprime_cpp_codegen.writer.DocWriter` subclass for
50
+ the default rendering.
51
+
52
+ ``emit_hpp=False`` or ``emit_cpp=False`` makes this a one-file document, which
53
+ a translation unit holding only a module-initialisation block wants. Members
54
+ that would then have nowhere to go raise :class:`ValidationError` when the
55
+ document is built, rather than disappearing.
56
+
57
+ ``strict=True`` additionally rejects any definition that needs a body and has
58
+ none, which would otherwise render as an empty out-of-line definition -- valid
59
+ C++, and so easy for a generator to emit by accident. Say
60
+ ``declaration_only=True`` to declare without defining, or ``body=""`` for a
61
+ definition that is deliberately empty.
43
62
  """
44
63
  super().__init__(_DocContext())
45
64
  if not file_base:
@@ -52,6 +71,23 @@ class CppDocBuilder(_MemberScope[CppDoc]):
52
71
  self.formatter = formatter
53
72
  """Applied to every file this document renders, unless a call overrides it."""
54
73
 
74
+ self.hpp_writer = hpp_writer
75
+ """Renders the header, unless a call overrides it. ``None`` uses
76
+ :class:`~fprime_cpp_codegen.writer.HppWriter`."""
77
+
78
+ self.cpp_writer = cpp_writer
79
+ """Renders the source files, unless a call overrides it. ``None`` uses
80
+ :class:`~fprime_cpp_codegen.writer.CppWriter`."""
81
+
82
+ self.emit_hpp = emit_hpp
83
+ """Whether this document produces a header at all."""
84
+
85
+ self.emit_cpp = emit_cpp
86
+ """Whether this document produces any source file at all."""
87
+
88
+ self.strict = strict
89
+ """Whether :meth:`build` rejects a definition that needs a body and has none."""
90
+
55
91
  self.file_banner = file_banner
56
92
  """Overrides the ``\\title``/``\\author``/``\\brief`` block atop each file.
57
93
  Distinct from :meth:`banner`, which emits a section comment."""
@@ -73,7 +109,8 @@ class CppDocBuilder(_MemberScope[CppDoc]):
73
109
  return f"{self.file_base}.{self.cpp_extension}"
74
110
 
75
111
  def build(self) -> CppDoc:
76
- return CppDoc(
112
+ """Produce the IR, checking it against ``emit_hpp``/``emit_cpp``/``strict``."""
113
+ doc = CppDoc(
77
114
  description=self.description,
78
115
  hpp_file=HppFile(self.hpp_name, self.include_guard),
79
116
  cpp_file_name=self.cpp_name,
@@ -81,23 +118,55 @@ class CppDocBuilder(_MemberScope[CppDoc]):
81
118
  tool_name=self.tool_name,
82
119
  banner=self.file_banner,
83
120
  )
121
+ check_document(
122
+ doc,
123
+ emit_hpp=self.emit_hpp,
124
+ emit_cpp=self.emit_cpp,
125
+ strict=self.strict,
126
+ )
127
+ return doc
84
128
 
85
129
  # -- output -------------------------------------------------------
86
130
 
87
131
  def _formatter(self, override: Formatter | None) -> Formatter | None:
88
132
  return override if override is not None else self.formatter
89
133
 
90
- def render_hpp(self, *, formatter: Formatter | None = None) -> str:
134
+ def _hpp_writer(self, override: HppWriter | None) -> HppWriter | None:
135
+ return override if override is not None else self.hpp_writer
136
+
137
+ def _cpp_writer(self, override: CppWriter | None) -> CppWriter | None:
138
+ return override if override is not None else self.cpp_writer
139
+
140
+ def _require_emitted(self, which: str) -> None:
141
+ """Reject rendering a file this document was told not to produce."""
142
+ if not (self.emit_hpp if which == "hpp" else self.emit_cpp):
143
+ raise ValidationError(
144
+ f"this document was built with emit_{which}=False, so it has no "
145
+ f"{which} file to render"
146
+ )
147
+
148
+ def render_hpp(
149
+ self,
150
+ *,
151
+ formatter: Formatter | None = None,
152
+ writer: HppWriter | None = None,
153
+ ) -> str:
91
154
  """Render the header as text."""
92
- text = render_hpp(self.build())
155
+ self._require_emitted("hpp")
156
+ text = render_hpp(self.build(), writer=self._hpp_writer(writer))
93
157
  chosen = self._formatter(formatter)
94
158
  return chosen(text, self.hpp_name) if chosen else text
95
159
 
96
160
  def render_cpp(
97
- self, cpp_file: str | None = None, *, formatter: Formatter | None = None
161
+ self,
162
+ cpp_file: str | None = None,
163
+ *,
164
+ formatter: Formatter | None = None,
165
+ writer: CppWriter | None = None,
98
166
  ) -> str:
99
167
  """Render one source file as text. ``None`` selects the default one."""
100
- text = render_cpp(self.build(), cpp_file)
168
+ self._require_emitted("cpp")
169
+ text = render_cpp(self.build(), cpp_file, writer=self._cpp_writer(writer))
101
170
  chosen = self._formatter(formatter)
102
171
  if not chosen:
103
172
  return text
@@ -109,9 +178,19 @@ class CppDocBuilder(_MemberScope[CppDoc]):
109
178
  cpp_files: Sequence[str] | None = None,
110
179
  *,
111
180
  formatter: Formatter | None = None,
181
+ hpp_writer: HppWriter | None = None,
182
+ cpp_writer: CppWriter | None = None,
112
183
  ) -> dict[str, str]:
113
184
  """Render every file this document owns, as a name-to-text mapping."""
114
- return doc_files(self.build(), cpp_files, formatter=self._formatter(formatter))
185
+ return doc_files(
186
+ self.build(),
187
+ cpp_files,
188
+ formatter=self._formatter(formatter),
189
+ hpp_writer=self._hpp_writer(hpp_writer),
190
+ cpp_writer=self._cpp_writer(cpp_writer),
191
+ emit_hpp=self.emit_hpp,
192
+ emit_cpp=self.emit_cpp,
193
+ )
115
194
 
116
195
  def write(
117
196
  self,
@@ -119,6 +198,8 @@ class CppDocBuilder(_MemberScope[CppDoc]):
119
198
  cpp_files: Sequence[str] | None = None,
120
199
  *,
121
200
  formatter: Formatter | None = None,
201
+ hpp_writer: HppWriter | None = None,
202
+ cpp_writer: CppWriter | None = None,
122
203
  skip_unchanged: bool = True,
123
204
  encoding: str = "utf-8",
124
205
  ) -> WriteResult:
@@ -131,6 +212,10 @@ class CppDocBuilder(_MemberScope[CppDoc]):
131
212
  directory,
132
213
  cpp_files,
133
214
  formatter=self._formatter(formatter),
215
+ hpp_writer=self._hpp_writer(hpp_writer),
216
+ cpp_writer=self._cpp_writer(cpp_writer),
217
+ emit_hpp=self.emit_hpp,
218
+ emit_cpp=self.emit_cpp,
134
219
  skip_unchanged=skip_unchanged,
135
220
  encoding=encoding,
136
221
  )