fprime-cpp-codegen 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,371 @@
1
+ """The C++ document IR: one ``.hpp`` file plus one or more ``.cpp`` files.
2
+
3
+ The layer the writers consume: a tree of frozen dataclasses.
4
+ :mod:`fprime_cpp_codegen.builder` assembles it for you.
5
+
6
+ A single :class:`CppDoc` describes a header and any number of source files. Every
7
+ definition with a body, and every block of raw lines, can name the ``.cpp`` file it
8
+ belongs to via ``cpp_file``; those naming nothing land in the document's default
9
+ ``.cpp``. The header always gets everything, so one class can be split across
10
+ translation units without duplicating its declaration.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass, field
16
+ from enum import Enum
17
+ from typing import Protocol, runtime_checkable
18
+
19
+ from .lines import Line
20
+
21
+ __all__ = [
22
+ "VOID",
23
+ "Class",
24
+ "ClassMember",
25
+ "Constructor",
26
+ "CppDoc",
27
+ "DefaultFileBanner",
28
+ "Definition",
29
+ "Destructor",
30
+ "FileBanner",
31
+ "Function",
32
+ "HppFile",
33
+ "Lines",
34
+ "Member",
35
+ "Namespace",
36
+ "Output",
37
+ "Param",
38
+ "SVQualifier",
39
+ "Type",
40
+ "Variable",
41
+ "as_type",
42
+ ]
43
+
44
+
45
+ class Output(Enum):
46
+ """Which of a document's files a block of raw lines is emitted into."""
47
+
48
+ HPP = "hpp"
49
+ """Header only."""
50
+
51
+ CPP = "cpp"
52
+ """Source only."""
53
+
54
+ BOTH = "both"
55
+ """Both the header and the source."""
56
+
57
+
58
+ class SVQualifier(Enum):
59
+ """A function's static/virtual specifier, mutually exclusive by construction.
60
+
61
+ ``OVERRIDE`` and ``FINAL`` render as trailing specifiers; ``STATIC``, ``VIRTUAL``
62
+ and ``PURE_VIRTUAL`` as leading ones. ``PURE_VIRTUAL`` also terminates the
63
+ declaration with ``= 0``.
64
+ """
65
+
66
+ NONE = "none"
67
+ STATIC = "static"
68
+ VIRTUAL = "virtual"
69
+ PURE_VIRTUAL = "pure-virtual"
70
+ OVERRIDE = "override"
71
+ FINAL = "final"
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class Type:
76
+ """A C++ type.
77
+
78
+ The source file may need a different spelling from the header, typically because
79
+ the header sits inside the namespace qualifying the name. ``cpp_type`` supplies
80
+ it; when absent, both files use ``hpp_type``.
81
+ """
82
+
83
+ hpp_type: str
84
+ cpp_type: str | None = None
85
+
86
+ @property
87
+ def cpp(self) -> str:
88
+ """The spelling to use in a ``.cpp`` file."""
89
+ return self.cpp_type if self.cpp_type is not None else self.hpp_type
90
+
91
+ @property
92
+ def hpp(self) -> str:
93
+ """The spelling to use in the ``.hpp`` file."""
94
+ return self.hpp_type
95
+
96
+
97
+ #: The ``void`` type, and the default return type of a :class:`Function`.
98
+ VOID = Type("void")
99
+
100
+
101
+ def as_type(t: Type | str | tuple[str, str]) -> Type:
102
+ """Coerce a type specification to a :class:`Type`.
103
+
104
+ A single string is used in both files. A ``(header, source)`` pair supplies the
105
+ two spellings a nested type needs: ``Status`` inside the class,
106
+ ``MyClass::Status`` in the source file where the return type precedes
107
+ ``MyClass::``.
108
+ """
109
+ if isinstance(t, Type):
110
+ return t
111
+ if isinstance(t, tuple):
112
+ hpp, cpp = t
113
+ return Type(hpp, cpp)
114
+ return Type(t)
115
+
116
+
117
+ @dataclass(frozen=True)
118
+ class Param:
119
+ """A formal parameter of a function, constructor, or destructor."""
120
+
121
+ type: Type
122
+ name: str
123
+ comment: str | None = None
124
+ """A doxygen post-comment, rendered after the parameter in the header."""
125
+
126
+ default: str | None = None
127
+ """A default argument, rendered in the header declaration only."""
128
+
129
+
130
+ @dataclass(frozen=True)
131
+ class Lines:
132
+ """A block of raw, already-rendered C++ lines.
133
+
134
+ Access tags, banner comments, ``#include`` directives, enums and structs are all
135
+ lines. ``output`` decides which files see them.
136
+ """
137
+
138
+ content: list[Line] = field(default_factory=list)
139
+ output: Output = Output.HPP
140
+ cpp_file: str | None = None
141
+ """Restrict source-file output to this ``.cpp`` base name. Header output is
142
+ unaffected."""
143
+
144
+
145
+ @dataclass(frozen=True, kw_only=True)
146
+ class Definition:
147
+ """Fields shared by everything that has a body.
148
+
149
+ Keyword-only, so subclasses can take their own defining field as the first
150
+ positional argument.
151
+
152
+ ``deleted`` and ``defaulted`` replace the body with ``= delete`` or
153
+ ``= default``; ``inline_body`` and ``template`` move the definition into the
154
+ header, since a template's definition must be visible at every use. In all four
155
+ cases the source file gets nothing.
156
+ """
157
+
158
+ body: list[Line] = field(default_factory=list)
159
+ comment: str | None = None
160
+ cpp_file: str | None = None
161
+ """Which ``.cpp`` file the definition goes in. ``None`` means the default."""
162
+
163
+ noexcept: bool = False
164
+ deleted: bool = False
165
+ defaulted: bool = False
166
+ inline_body: bool = False
167
+
168
+ template: str | None = None
169
+ """A template parameter list without the keyword, e.g. ``"typename T"``.
170
+ Implies :attr:`inline_body`."""
171
+
172
+ @property
173
+ def defined_in_header(self) -> bool:
174
+ return self.inline_body or self.template is not None
175
+
176
+ @property
177
+ def has_definition(self) -> bool:
178
+ """Whether there is a body to write at all."""
179
+ return not (self.deleted or self.defaulted)
180
+
181
+
182
+ @dataclass(frozen=True)
183
+ class Function(Definition):
184
+ """A C++ function, either free-standing or a class member."""
185
+
186
+ name: str
187
+ params: list[Param] = field(default_factory=list)
188
+ ret_type: Type = VOID
189
+ """``Type("")`` emits no return type, as a conversion operator needs."""
190
+
191
+ sv: SVQualifier = SVQualifier.NONE
192
+ const: bool = False
193
+ constexpr: bool = False
194
+ inline: bool = False
195
+
196
+ @property
197
+ def defined_in_header(self) -> bool:
198
+ """``constexpr`` and ``inline`` force the definition into the header: both
199
+ require it visible in every translation unit that uses the function."""
200
+ return super().defined_in_header or self.constexpr or self.inline
201
+
202
+
203
+ @dataclass(frozen=True)
204
+ class Constructor(Definition):
205
+ """A C++ constructor. Its name is taken from the enclosing class."""
206
+
207
+ params: list[Param] = field(default_factory=list)
208
+ initializers: list[str] = field(default_factory=list)
209
+ """Member-initializer-list entries, e.g. ``"m_size(size)"``, rendered wherever the
210
+ definition goes."""
211
+
212
+ explicit: bool = False
213
+ constexpr: bool = False
214
+
215
+ @property
216
+ def defined_in_header(self) -> bool:
217
+ return super().defined_in_header or self.constexpr
218
+
219
+
220
+ @dataclass(frozen=True)
221
+ class Destructor(Definition):
222
+ """A C++ destructor. Its name is taken from the enclosing class."""
223
+
224
+ virtual: bool = False
225
+ override: bool = False
226
+
227
+
228
+ @dataclass(frozen=True)
229
+ class Class:
230
+ """A C++ class, possibly nested inside another class."""
231
+
232
+ name: str
233
+ superclass_decls: str | None = None
234
+ """Everything after the colon, verbatim, e.g. ``"public Fw::Serializable"``.
235
+ Multiple bases go in one comma-separated string."""
236
+
237
+ members: list[ClassMember] = field(default_factory=list)
238
+ comment: str | None = None
239
+ final: bool = False
240
+ template: str | None = None
241
+ """A template parameter list without the keyword, e.g. ``"typename T"``. A
242
+ templated class defines all its members in the header, so its source file output
243
+ is empty."""
244
+
245
+ struct: bool = False
246
+ """Emit ``struct``, making members public by default."""
247
+
248
+
249
+ @dataclass(frozen=True)
250
+ class Variable:
251
+ """A variable: a class data member, or a constant or global at namespace scope.
252
+
253
+ Where the initialiser goes depends on the kind of variable:
254
+
255
+ * A non-static data member takes its initialiser in the class body.
256
+ * A static data member is only declared in the class; the definition, with the
257
+ initialiser, goes in a source file. A ``constexpr`` static is initialised in
258
+ the class and needs no out-of-line definition unless something takes its
259
+ address -- see ``out_of_line_definition``.
260
+ * At namespace scope, ``extern`` splits declaration from definition the same way.
261
+ Without it the variable is defined where it is declared, as a ``constexpr`` or
262
+ ``const`` constant in a header should be.
263
+ """
264
+
265
+ name: str
266
+ type: Type
267
+ init: str | None = None
268
+ array: str | None = None
269
+ """An array extent, without brackets, e.g. ``"SIZE"`` for ``m_data[SIZE]``."""
270
+
271
+ comment: str | None = None
272
+ static: bool = False
273
+ const: bool = False
274
+ constexpr: bool = False
275
+ mutable: bool = False
276
+ extern: bool = False
277
+ out_of_line_definition: bool = False
278
+ """Also emit a source-file definition for a ``constexpr`` static member."""
279
+
280
+ cpp_file: str | None = None
281
+
282
+ @property
283
+ def declarator(self) -> str:
284
+ """The name plus any array extent, e.g. ``"m_data[SIZE]"``."""
285
+ return f"{self.name}[{self.array}]" if self.array is not None else self.name
286
+
287
+
288
+ @dataclass(frozen=True)
289
+ class Namespace:
290
+ """A C++ namespace. Nest instances to nest namespaces."""
291
+
292
+ name: str
293
+ members: list[Member] = field(default_factory=list)
294
+
295
+
296
+ #: What may appear at document or namespace scope.
297
+ Member = Class | Lines | Function | Namespace | Variable
298
+
299
+ #: What may appear at class scope. Namespaces may not; constructors and
300
+ #: destructors may.
301
+ ClassMember = Class | Lines | Function | Constructor | Destructor | Variable
302
+
303
+
304
+ @runtime_checkable
305
+ class FileBanner(Protocol):
306
+ """Supplies the ``\\title``/``\\author``/``\\brief`` lines atop each file."""
307
+
308
+ def title(self, file_name: str) -> str:
309
+ """The ``\\title`` text for ``file_name``."""
310
+ ...
311
+
312
+ def author(self, file_name: str) -> str:
313
+ """The ``\\author`` text for ``file_name``."""
314
+ ...
315
+
316
+ def description(self, file_name: str, generic_description: str) -> str:
317
+ """The ``\\brief`` text. ``generic_description`` is the writer's own phrasing,
318
+ e.g. ``"hpp file for my component"``."""
319
+ ...
320
+
321
+
322
+ @dataclass(frozen=True)
323
+ class DefaultFileBanner:
324
+ """The banner used when a document does not supply one."""
325
+
326
+ tool_name: str | None = None
327
+
328
+ def title(self, file_name: str) -> str:
329
+ return file_name
330
+
331
+ def author(self, file_name: str) -> str:
332
+ return f"Generated by {self.tool_name or 'fpp tools'}"
333
+
334
+ def description(self, file_name: str, generic_description: str) -> str:
335
+ return generic_description
336
+
337
+
338
+ @dataclass(frozen=True)
339
+ class HppFile:
340
+ """The header file of a document."""
341
+
342
+ name: str
343
+ """The file name including extension, e.g. ``"MyClass.hpp"``."""
344
+
345
+ include_guard: str
346
+ """The include-guard macro, e.g. ``"Fw_MyClass_HPP"``."""
347
+
348
+
349
+ @dataclass(frozen=True)
350
+ class CppDoc:
351
+ """A C++ document: one header, one default source file, and the members."""
352
+
353
+ description: str
354
+ """Used in the file banners, as ``"hpp file for <description>"``."""
355
+
356
+ hpp_file: HppFile
357
+ cpp_file_name: str
358
+ """The default source file name including extension, e.g. ``"MyClass.cpp"``."""
359
+
360
+ members: list[Member] = field(default_factory=list)
361
+ tool_name: str | None = None
362
+ banner: FileBanner | None = None
363
+
364
+ @property
365
+ def file_banner(self) -> FileBanner:
366
+ """The document's banner, falling back to :class:`DefaultFileBanner`."""
367
+ return (
368
+ self.banner
369
+ if self.banner is not None
370
+ else DefaultFileBanner(self.tool_name)
371
+ )
@@ -0,0 +1,25 @@
1
+ """The exception hierarchy.
2
+
3
+ Everything raised here derives from :class:`CppCodegenError`, so a generator can
4
+ catch one type.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ __all__ = ["CppCodegenError", "ScopeError", "ValidationError"]
10
+
11
+
12
+ class CppCodegenError(Exception):
13
+ """Base class for every error raised here."""
14
+
15
+
16
+ class ScopeError(CppCodegenError):
17
+ """A builder scope was used in a way its structure does not allow.
18
+
19
+ ``else`` with no preceding ``if``, a ``case`` outside a ``switch``, building a body
20
+ with a scope still open.
21
+ """
22
+
23
+
24
+ class ValidationError(CppCodegenError):
25
+ """A document or declaration is malformed and could not produce valid C++."""
@@ -0,0 +1,89 @@
1
+ """Passing generated files through an external formatter.
2
+
3
+ ``clang-format`` is lexical: it needs no compilation database, include paths, or
4
+ working compiler, so generated text with missing includes and unknown types
5
+ formats fine. It cannot tell a type name from a variable, so ``a * b;`` becomes
6
+ ``a *b;``.
7
+
8
+ A formatter takes the file text and its name. ``clang-format`` locates the
9
+ governing ``.clang-format`` by searching upward from the file's path, so the name
10
+ determines which project style applies.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import shutil
16
+ import subprocess
17
+ from dataclasses import dataclass
18
+ from typing import Protocol, runtime_checkable
19
+
20
+ from .errors import CppCodegenError
21
+
22
+ __all__ = ["ClangFormat", "Formatter"]
23
+
24
+
25
+ @runtime_checkable
26
+ class Formatter(Protocol):
27
+ """Post-processes one generated file's text."""
28
+
29
+ def __call__(self, text: str, file_name: str) -> str:
30
+ """Return ``text`` reformatted. ``file_name`` is the name it is written as."""
31
+ ...
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class ClangFormat:
36
+ """Runs ``clang-format`` over generated text, through stdin and stdout::
37
+
38
+ doc.write("build-artifacts", formatter=ClangFormat())
39
+
40
+ With no ``style``, ``clang-format``'s default applies, which searches for a
41
+ ``.clang-format`` upward from the generated file's directory. ``style`` may be
42
+ a named style such as ``"LLVM"`` or inline YAML like
43
+ ``"{BasedOnStyle: LLVM, ColumnLimit: 120}"``.
44
+
45
+ This reformats everything: a ``Ret Class ::`` line and its indented signature
46
+ collapse onto one, and namespace bodies lose their indentation.
47
+ """
48
+
49
+ executable: str = "clang-format"
50
+ style: str | None = None
51
+ timeout: float = 30.0
52
+
53
+ def available(self) -> bool:
54
+ """Whether the executable can be found on ``PATH``."""
55
+ return shutil.which(self.executable) is not None
56
+
57
+ def version(self) -> str:
58
+ """The formatter's version string."""
59
+ return self._run(["--version"], text=None).strip()
60
+
61
+ def __call__(self, text: str, file_name: str) -> str:
62
+ """Return ``text`` as ``clang-format`` writes it for ``file_name``."""
63
+ args = [f"--assume-filename={file_name}"]
64
+ if self.style is not None:
65
+ args.append(f"--style={self.style}")
66
+ return self._run(args, text=text)
67
+
68
+ def _run(self, args: list[str], *, text: str | None) -> str:
69
+ try:
70
+ result = subprocess.run(
71
+ [self.executable, *args],
72
+ input=text,
73
+ capture_output=True,
74
+ text=True,
75
+ timeout=self.timeout,
76
+ )
77
+ except FileNotFoundError as exc:
78
+ raise CppCodegenError(
79
+ f"{self.executable!r} was not found on PATH; install clang-format or "
80
+ "pass a different executable to ClangFormat"
81
+ ) from exc
82
+ except subprocess.TimeoutExpired as exc:
83
+ raise CppCodegenError(
84
+ f"{self.executable!r} did not finish within {self.timeout}s"
85
+ ) from exc
86
+ if result.returncode != 0:
87
+ detail = result.stderr.strip() or f"exit status {result.returncode}"
88
+ raise CppCodegenError(f"{self.executable!r} failed: {detail}")
89
+ return result.stdout
@@ -0,0 +1,149 @@
1
+ """F Prime idioms, kept separate from the general-purpose layers.
2
+
3
+ The conventions that show up in every F Prime autocoded file, collected so a
4
+ generator does not have to retype them. All strings and lines; no knowledge of the
5
+ FPP model.
6
+
7
+ Import it explicitly; the core API stays framework-neutral.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Sequence
13
+ from typing import Any
14
+
15
+ from .doc import ClassMember, Lines, Member, Output
16
+ from .lines import Line, blank, lines, wrap_in_scope
17
+
18
+ __all__ = [
19
+ "BUILD_UT",
20
+ "FW_ENABLE_TEXT_LOGGING",
21
+ "STANDARD_SYSTEM_CPP_HEADERS",
22
+ "STANDARD_SYSTEM_HPP_HEADERS",
23
+ "STANDARD_USER_CPP_HEADERS",
24
+ "STANDARD_USER_HPP_HEADERS",
25
+ "buffer_name",
26
+ "external_string_decl",
27
+ "guard_class_members_for_text_log",
28
+ "guard_class_members_for_unit_test",
29
+ "guard_members_for_text_log",
30
+ "guard_members_for_unit_test",
31
+ "write_assert",
32
+ "write_ostream_operator",
33
+ ]
34
+
35
+ #: The preprocessor condition guarding text-log support.
36
+ FW_ENABLE_TEXT_LOGGING = "#if FW_ENABLE_TEXT_LOGGING"
37
+
38
+ #: The preprocessor condition guarding unit-test-only code.
39
+ BUILD_UT = "#ifdef BUILD_UT"
40
+
41
+ #: Project headers an autocoded F Prime header normally needs.
42
+ STANDARD_USER_HPP_HEADERS = [
43
+ f'#include "{path}"'
44
+ for path in (
45
+ "Fw/FPrimeBasicTypes.hpp",
46
+ "Fw/Types/ExternalString.hpp",
47
+ "Fw/Types/Serializable.hpp",
48
+ "Fw/Types/String.hpp",
49
+ )
50
+ ]
51
+
52
+ #: System headers an autocoded F Prime header normally needs. Empty, but kept so a
53
+ #: generator can splice it in unconditionally.
54
+ STANDARD_SYSTEM_HPP_HEADERS: list[str] = []
55
+
56
+ #: Project headers an autocoded F Prime source file normally needs.
57
+ STANDARD_USER_CPP_HEADERS = ['#include "Fw/Types/Assert.hpp"']
58
+
59
+ #: System headers an autocoded F Prime source file normally needs.
60
+ STANDARD_SYSTEM_CPP_HEADERS: list[str] = []
61
+
62
+
63
+ def write_assert(condition: str, *args: str) -> list[Line]:
64
+ """Render an ``FW_ASSERT``.
65
+
66
+ Extra arguments become the assert's reported values, making a flight-side
67
+ assertion diagnosable after the fact. Each must already be spelled as a C++
68
+ expression, casts included.
69
+ """
70
+ joined = ", ".join([condition, *args])
71
+ return lines(f"FW_ASSERT({joined});")
72
+
73
+
74
+ def buffer_name(name: str) -> str:
75
+ """The backing-array name for a generated ``Fw::ExternalString``."""
76
+ return f"__fprime_ac_{name}_buffer"
77
+
78
+
79
+ def external_string_decl(name: str, size: str) -> list[Line]:
80
+ """Declare an ``Fw::ExternalString`` over a stack buffer of ``size`` characters.
81
+
82
+ F Prime avoids dynamic memory, so a string-typed local is a fixed char array plus
83
+ a view onto it, not an owning string object.
84
+ """
85
+ buf = buffer_name(name)
86
+ return lines(f"""|char {buf}[Fw::StringBase::BUFFER_SIZE({size})];
87
+ |Fw::ExternalString {name}({buf}, sizeof {buf});""")
88
+
89
+
90
+ def _guard_members(directive: str, members: Sequence[Any], output: Output) -> list[Any]:
91
+ """Bracket a non-empty run of members with ``directive`` and ``#endif``."""
92
+ if not members:
93
+ return []
94
+ return [
95
+ Lines(lines(f"\n{directive}"), output),
96
+ *members,
97
+ Lines([blank(), *lines("#endif")], output),
98
+ ]
99
+
100
+
101
+ def guard_class_members_for_text_log(
102
+ members: Sequence[ClassMember], output: Output = Output.BOTH
103
+ ) -> list[ClassMember]:
104
+ """Bracket class members with ``#if FW_ENABLE_TEXT_LOGGING``."""
105
+ return _guard_members(FW_ENABLE_TEXT_LOGGING, members, output)
106
+
107
+
108
+ def guard_members_for_text_log(
109
+ members: Sequence[Member], output: Output = Output.BOTH
110
+ ) -> list[Member]:
111
+ """Bracket document members with ``#if FW_ENABLE_TEXT_LOGGING``."""
112
+ return _guard_members(FW_ENABLE_TEXT_LOGGING, members, output)
113
+
114
+
115
+ def guard_class_members_for_unit_test(
116
+ members: Sequence[ClassMember], output: Output = Output.BOTH
117
+ ) -> list[ClassMember]:
118
+ """Bracket class members with ``#ifdef BUILD_UT``."""
119
+ return _guard_members(BUILD_UT, members, output)
120
+
121
+
122
+ def guard_members_for_unit_test(
123
+ members: Sequence[Member], output: Output = Output.BOTH
124
+ ) -> list[Member]:
125
+ """Bracket document members with ``#ifdef BUILD_UT``."""
126
+ return _guard_members(BUILD_UT, members, output)
127
+
128
+
129
+ def write_ostream_operator(name: str, body: Sequence[Line]) -> list[ClassMember]:
130
+ """Declare and define a friend ``operator<<`` for ``name``, unit-test only.
131
+
132
+ Returns the header declaration and the source-file definition, both inside a
133
+ ``BUILD_UT`` guard, ready to splice into a class's member list.
134
+ """
135
+ declaration = Lines(lines(f"""|
136
+ |//! Ostream operator
137
+ |friend std::ostream& operator<<(
138
+ | std::ostream& os, //!< The ostream
139
+ | const {name}& obj //!< The object
140
+ |);"""))
141
+ definition = Lines(
142
+ wrap_in_scope(
143
+ f"\nstd::ostream& operator<<(std::ostream& os, const {name}& obj) {{",
144
+ body,
145
+ "}",
146
+ ),
147
+ Output.CPP,
148
+ )
149
+ return guard_class_members_for_unit_test([declaration, definition])