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.
- fprime_cpp_codegen/__init__.py +162 -0
- fprime_cpp_codegen/body.py +448 -0
- fprime_cpp_codegen/builder/__init__.py +75 -0
- fprime_cpp_codegen/builder/base.py +55 -0
- fprime_cpp_codegen/builder/coercion.py +97 -0
- fprime_cpp_codegen/builder/decoration.py +201 -0
- fprime_cpp_codegen/builder/definitions.py +381 -0
- fprime_cpp_codegen/builder/document.py +155 -0
- fprime_cpp_codegen/builder/scopes.py +466 -0
- fprime_cpp_codegen/comments.py +144 -0
- fprime_cpp_codegen/doc.py +371 -0
- fprime_cpp_codegen/errors.py +25 -0
- fprime_cpp_codegen/formatting.py +89 -0
- fprime_cpp_codegen/fprime.py +149 -0
- fprime_cpp_codegen/lines.py +323 -0
- fprime_cpp_codegen/output.py +121 -0
- fprime_cpp_codegen/py.typed +0 -0
- fprime_cpp_codegen/writer.py +787 -0
- fprime_cpp_codegen-0.1.0.dist-info/METADATA +347 -0
- fprime_cpp_codegen-0.1.0.dist-info/RECORD +23 -0
- fprime_cpp_codegen-0.1.0.dist-info/WHEEL +5 -0
- fprime_cpp_codegen-0.1.0.dist-info/licenses/LICENSE +201 -0
- fprime_cpp_codegen-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Generate C++ header and source files from Python.
|
|
2
|
+
|
|
3
|
+
The package is layered, and you can enter at whichever level suits the job:
|
|
4
|
+
|
|
5
|
+
* :mod:`~fprime_cpp_codegen.builder` -- the builder API. Start with
|
|
6
|
+
:class:`CppDocBuilder`.
|
|
7
|
+
* :mod:`~fprime_cpp_codegen.body` -- :class:`Body`, for function bodies.
|
|
8
|
+
* :mod:`~fprime_cpp_codegen.doc` -- the document IR, to build the tree directly.
|
|
9
|
+
* :mod:`~fprime_cpp_codegen.writer` -- the visitors rendering the IR to lines.
|
|
10
|
+
* :mod:`~fprime_cpp_codegen.comments` -- comment and banner formatting.
|
|
11
|
+
* :mod:`~fprime_cpp_codegen.lines` -- the line model everything is built on.
|
|
12
|
+
* :mod:`~fprime_cpp_codegen.output` -- rendering to text and to disk.
|
|
13
|
+
* :mod:`~fprime_cpp_codegen.formatting` -- optional post-processing through
|
|
14
|
+
``clang-format``.
|
|
15
|
+
* :mod:`~fprime_cpp_codegen.fprime` -- F Prime conventions. Import it explicitly;
|
|
16
|
+
nothing else in the package depends on it.
|
|
17
|
+
|
|
18
|
+
Nothing here knows about the FPP model.
|
|
19
|
+
|
|
20
|
+
A minimal example::
|
|
21
|
+
|
|
22
|
+
from fprime_cpp_codegen import CppDocBuilder
|
|
23
|
+
|
|
24
|
+
doc = CppDocBuilder("Greeter", description="a greeter", namespaces=["Demo"])
|
|
25
|
+
doc.include("Fw/FPrimeBasicTypes.hpp")
|
|
26
|
+
|
|
27
|
+
with doc.namespace("Demo") as ns:
|
|
28
|
+
with ns.class_("Greeter") as cls:
|
|
29
|
+
with cls.public():
|
|
30
|
+
fn = cls.function("greet", params=[("const char*", "name")])
|
|
31
|
+
fn.body.line('printf("hello %s\\n", name);')
|
|
32
|
+
|
|
33
|
+
print(doc.render_hpp())
|
|
34
|
+
print(doc.render_cpp())
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
from .body import Body, Code, Switch, stmts
|
|
40
|
+
from .builder import (
|
|
41
|
+
AccessSection,
|
|
42
|
+
ClassBuilder,
|
|
43
|
+
ConstructorBuilder,
|
|
44
|
+
CppDocBuilder,
|
|
45
|
+
DestructorBuilder,
|
|
46
|
+
EnumBuilder,
|
|
47
|
+
FunctionBuilder,
|
|
48
|
+
NamespaceBuilder,
|
|
49
|
+
Radix,
|
|
50
|
+
)
|
|
51
|
+
from .doc import (
|
|
52
|
+
VOID,
|
|
53
|
+
Class,
|
|
54
|
+
ClassMember,
|
|
55
|
+
Constructor,
|
|
56
|
+
CppDoc,
|
|
57
|
+
DefaultFileBanner,
|
|
58
|
+
Definition,
|
|
59
|
+
Destructor,
|
|
60
|
+
FileBanner,
|
|
61
|
+
Function,
|
|
62
|
+
HppFile,
|
|
63
|
+
Lines,
|
|
64
|
+
Member,
|
|
65
|
+
Namespace,
|
|
66
|
+
Output,
|
|
67
|
+
Param,
|
|
68
|
+
SVQualifier,
|
|
69
|
+
Type,
|
|
70
|
+
Variable,
|
|
71
|
+
as_type,
|
|
72
|
+
)
|
|
73
|
+
from .errors import CppCodegenError, ScopeError, ValidationError
|
|
74
|
+
from .formatting import ClangFormat, Formatter
|
|
75
|
+
from .lines import (
|
|
76
|
+
INDENT_INCREMENT,
|
|
77
|
+
IndentMode,
|
|
78
|
+
Line,
|
|
79
|
+
blank,
|
|
80
|
+
line,
|
|
81
|
+
lines,
|
|
82
|
+
render,
|
|
83
|
+
wrap_in_scope,
|
|
84
|
+
)
|
|
85
|
+
from .output import WriteResult, collect_cpp_files, doc_files, write_doc
|
|
86
|
+
from .writer import (
|
|
87
|
+
Context,
|
|
88
|
+
CppWriter,
|
|
89
|
+
DocWriter,
|
|
90
|
+
HppWriter,
|
|
91
|
+
cpp_lines,
|
|
92
|
+
hpp_lines,
|
|
93
|
+
render_cpp,
|
|
94
|
+
render_hpp,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
__all__ = [
|
|
98
|
+
# Line model
|
|
99
|
+
"INDENT_INCREMENT",
|
|
100
|
+
"IndentMode",
|
|
101
|
+
"Line",
|
|
102
|
+
"blank",
|
|
103
|
+
"line",
|
|
104
|
+
"lines",
|
|
105
|
+
"render",
|
|
106
|
+
"wrap_in_scope",
|
|
107
|
+
# Document IR
|
|
108
|
+
"VOID",
|
|
109
|
+
"Class",
|
|
110
|
+
"ClassMember",
|
|
111
|
+
"Constructor",
|
|
112
|
+
"CppDoc",
|
|
113
|
+
"DefaultFileBanner",
|
|
114
|
+
"Definition",
|
|
115
|
+
"Destructor",
|
|
116
|
+
"FileBanner",
|
|
117
|
+
"Function",
|
|
118
|
+
"HppFile",
|
|
119
|
+
"Lines",
|
|
120
|
+
"Member",
|
|
121
|
+
"Namespace",
|
|
122
|
+
"Output",
|
|
123
|
+
"Param",
|
|
124
|
+
"Radix",
|
|
125
|
+
"SVQualifier",
|
|
126
|
+
"Type",
|
|
127
|
+
"Variable",
|
|
128
|
+
"as_type",
|
|
129
|
+
# Builders
|
|
130
|
+
"AccessSection",
|
|
131
|
+
"Body",
|
|
132
|
+
"ClassBuilder",
|
|
133
|
+
"Code",
|
|
134
|
+
"ConstructorBuilder",
|
|
135
|
+
"CppDocBuilder",
|
|
136
|
+
"DestructorBuilder",
|
|
137
|
+
"EnumBuilder",
|
|
138
|
+
"FunctionBuilder",
|
|
139
|
+
"NamespaceBuilder",
|
|
140
|
+
"Switch",
|
|
141
|
+
"stmts",
|
|
142
|
+
# Writers
|
|
143
|
+
"Context",
|
|
144
|
+
"CppWriter",
|
|
145
|
+
"DocWriter",
|
|
146
|
+
"HppWriter",
|
|
147
|
+
"cpp_lines",
|
|
148
|
+
"hpp_lines",
|
|
149
|
+
"render_cpp",
|
|
150
|
+
"render_hpp",
|
|
151
|
+
# Output
|
|
152
|
+
"ClangFormat",
|
|
153
|
+
"Formatter",
|
|
154
|
+
"WriteResult",
|
|
155
|
+
"collect_cpp_files",
|
|
156
|
+
"doc_files",
|
|
157
|
+
"write_doc",
|
|
158
|
+
# Errors
|
|
159
|
+
"CppCodegenError",
|
|
160
|
+
"ScopeError",
|
|
161
|
+
"ValidationError",
|
|
162
|
+
]
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""Building function bodies out of C++ statements.
|
|
2
|
+
|
|
3
|
+
A :class:`Body` accumulates lines. Statements append to it; control-flow scopes
|
|
4
|
+
are context managers that indent everything written inside them::
|
|
5
|
+
|
|
6
|
+
body = Body()
|
|
7
|
+
body.line("U32 total = 0;")
|
|
8
|
+
with body.for_("U32 i = 0", "i < n", "i++"):
|
|
9
|
+
body.line("total += m_data[i];")
|
|
10
|
+
body.line("return total;")
|
|
11
|
+
|
|
12
|
+
A ``Body`` is a value: build one anywhere, return it from a helper, and splice it
|
|
13
|
+
in with :meth:`Body.extend`.
|
|
14
|
+
|
|
15
|
+
This class covers structure -- scopes, nesting, control flow. Individual
|
|
16
|
+
statements go through :meth:`Body.line`. :meth:`Body.raw` takes lines from
|
|
17
|
+
elsewhere, such as :func:`fprime_cpp_codegen.fprime.write_assert`.
|
|
18
|
+
|
|
19
|
+
Scopes emit even when their body turns out empty, since a vanishing ``if`` would
|
|
20
|
+
re-point the ``else`` that follows it. Pass ``omit_if_empty=True`` to let the scope
|
|
21
|
+
disappear.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from collections.abc import Generator, Iterable, Iterator, Sequence
|
|
27
|
+
from contextlib import AbstractContextManager, contextmanager
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from typing import TypeAlias
|
|
30
|
+
|
|
31
|
+
from .comments import (
|
|
32
|
+
write_banner_comment,
|
|
33
|
+
write_comment,
|
|
34
|
+
write_comment_body,
|
|
35
|
+
write_doxygen_comment,
|
|
36
|
+
)
|
|
37
|
+
from .errors import ScopeError, ValidationError
|
|
38
|
+
from .lines import Line, blank, indent_lines
|
|
39
|
+
from .lines import line as _line
|
|
40
|
+
from .lines import lines as _lines
|
|
41
|
+
from .lines import render as _render
|
|
42
|
+
|
|
43
|
+
__all__ = ["Body", "Code", "Switch", "stmts"]
|
|
44
|
+
|
|
45
|
+
#: Anything usable as a run of C++ statements. ``None`` contributes nothing, so
|
|
46
|
+
#: ``b.add(frag if condition else None)`` needs no branch. A ``str`` is
|
|
47
|
+
#: margin-stripped and taken verbatim, with no punctuation added.
|
|
48
|
+
Code: TypeAlias = "None | str | Line | Body | Sequence[Code]"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def stmts(*code: Code) -> list[Line]:
|
|
52
|
+
"""Coerce statement-shaped values to lines, flattening nested sequences."""
|
|
53
|
+
out: list[Line] = []
|
|
54
|
+
for item in code:
|
|
55
|
+
if item is None:
|
|
56
|
+
continue
|
|
57
|
+
if isinstance(item, Body):
|
|
58
|
+
out.extend(item.build())
|
|
59
|
+
elif isinstance(item, Line):
|
|
60
|
+
out.append(item)
|
|
61
|
+
elif isinstance(item, str):
|
|
62
|
+
out.extend(_lines(item))
|
|
63
|
+
elif isinstance(item, Sequence):
|
|
64
|
+
out.extend(stmts(*item))
|
|
65
|
+
else:
|
|
66
|
+
raise ValidationError(f"not usable as C++ statements: {item!r}")
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
#: Statements after which control does not fall through.
|
|
71
|
+
_TERMINATING_KEYWORDS = ("return", "throw", "goto")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _terminates(ll: Sequence[Line]) -> bool:
|
|
75
|
+
"""Whether ``ll`` ends in a statement that unconditionally transfers control.
|
|
76
|
+
|
|
77
|
+
Conservative: a miss costs an unreachable ``break;``, while a false positive
|
|
78
|
+
would drop a ``break`` and turn a switch arm into a fallthrough. Only shapes
|
|
79
|
+
that cannot be anything else match -- ``return`` and ``throw`` are keywords, so
|
|
80
|
+
a following space or semicolon is unambiguous.
|
|
81
|
+
"""
|
|
82
|
+
if not ll:
|
|
83
|
+
return False
|
|
84
|
+
last = ll[-1].string.strip()
|
|
85
|
+
if last in ("break;", "continue;"):
|
|
86
|
+
return True
|
|
87
|
+
return any(
|
|
88
|
+
last == f"{kw};" or last.startswith(f"{kw} ") for kw in _TERMINATING_KEYWORDS
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class _Frame:
|
|
94
|
+
"""One level of the body under construction."""
|
|
95
|
+
|
|
96
|
+
lines: list[Line] = field(default_factory=list)
|
|
97
|
+
kind: str = "body"
|
|
98
|
+
open_chain: bool = False
|
|
99
|
+
"""Whether the last thing written was an ``if``/``else if``, so an ``else``
|
|
100
|
+
may still attach to it."""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Body:
|
|
104
|
+
"""A function body under construction."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, initial: Iterable[Line] | None = None) -> None:
|
|
107
|
+
self._frames: list[_Frame] = [_Frame(list(initial or []))]
|
|
108
|
+
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
# Result
|
|
111
|
+
# ------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def terminated(self) -> bool:
|
|
115
|
+
"""Whether the last statement written unconditionally transfers control.
|
|
116
|
+
|
|
117
|
+
A switch arm reads this to skip an unreachable ``break;``. A ``return``
|
|
118
|
+
nested inside an ``if`` does not count: the last line at this level is then
|
|
119
|
+
the ``if``'s closing brace.
|
|
120
|
+
"""
|
|
121
|
+
return _terminates(self._current.lines)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def depth(self) -> int:
|
|
125
|
+
"""How many scopes are currently open. Zero at the top level."""
|
|
126
|
+
return len(self._frames) - 1
|
|
127
|
+
|
|
128
|
+
def build(self) -> list[Line]:
|
|
129
|
+
"""Return the accumulated lines.
|
|
130
|
+
|
|
131
|
+
Raises if a scope is still open, which means a ``with`` block was skipped
|
|
132
|
+
or exited by something other than falling off the end.
|
|
133
|
+
"""
|
|
134
|
+
if len(self._frames) > 1:
|
|
135
|
+
raise ScopeError(
|
|
136
|
+
f"{len(self._frames) - 1} body scope(s) are still open; finish every "
|
|
137
|
+
"'with' block before building"
|
|
138
|
+
)
|
|
139
|
+
return list(self._frames[0].lines)
|
|
140
|
+
|
|
141
|
+
def __bool__(self) -> bool:
|
|
142
|
+
return any(f.lines for f in self._frames)
|
|
143
|
+
|
|
144
|
+
def __iter__(self) -> Iterator[Line]:
|
|
145
|
+
return iter(self.build())
|
|
146
|
+
|
|
147
|
+
def __str__(self) -> str:
|
|
148
|
+
return _render(self.build())
|
|
149
|
+
|
|
150
|
+
def __enter__(self) -> Body:
|
|
151
|
+
"""Support ``with fn.body as b:`` as a way to shorten the name."""
|
|
152
|
+
return self
|
|
153
|
+
|
|
154
|
+
def __exit__(self, *exc: object) -> None:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
# ------------------------------------------------------------------
|
|
158
|
+
# Internals
|
|
159
|
+
# ------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def _current(self) -> _Frame:
|
|
163
|
+
return self._frames[-1]
|
|
164
|
+
|
|
165
|
+
def _emit(self, ll: Sequence[Line], *, chain: bool = False) -> Body:
|
|
166
|
+
"""Append lines and record whether an ``else`` may follow."""
|
|
167
|
+
frame = self._current
|
|
168
|
+
frame.lines.extend(ll)
|
|
169
|
+
frame.open_chain = chain
|
|
170
|
+
return self
|
|
171
|
+
|
|
172
|
+
@contextmanager
|
|
173
|
+
def _scope(
|
|
174
|
+
self,
|
|
175
|
+
opening: str,
|
|
176
|
+
closing: str,
|
|
177
|
+
*,
|
|
178
|
+
kind: str = "body",
|
|
179
|
+
omit_if_empty: bool = False,
|
|
180
|
+
chain: bool = False,
|
|
181
|
+
indent: bool = True,
|
|
182
|
+
) -> Generator[Body]:
|
|
183
|
+
"""Open a nested scope, indenting whatever is written inside it.
|
|
184
|
+
|
|
185
|
+
If the block raises, the scope is discarded and the body is left as it was
|
|
186
|
+
before the ``with``.
|
|
187
|
+
"""
|
|
188
|
+
frame = _Frame(kind=kind)
|
|
189
|
+
self._frames.append(frame)
|
|
190
|
+
try:
|
|
191
|
+
yield self
|
|
192
|
+
except BaseException:
|
|
193
|
+
self._frames.pop()
|
|
194
|
+
raise
|
|
195
|
+
self._frames.pop()
|
|
196
|
+
if not frame.lines and omit_if_empty:
|
|
197
|
+
return
|
|
198
|
+
body = indent_lines(frame.lines) if indent else list(frame.lines)
|
|
199
|
+
self._emit(
|
|
200
|
+
[*_lines(opening), *body, *_lines(closing)],
|
|
201
|
+
chain=chain,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def _require_chain(self, keyword: str) -> None:
|
|
205
|
+
if not self._current.open_chain:
|
|
206
|
+
raise ScopeError(
|
|
207
|
+
f"{keyword!r} has no 'if' to attach to; it must directly follow an "
|
|
208
|
+
"if_() or elif_() scope in the same body"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
# Raw output
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
def line(self, text: str) -> Body:
|
|
216
|
+
"""Append one line verbatim."""
|
|
217
|
+
return self._emit([_line(text)])
|
|
218
|
+
|
|
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
|
+
|
|
223
|
+
def raw(self, ll: Iterable[Line]) -> Body:
|
|
224
|
+
"""Append already-rendered lines."""
|
|
225
|
+
return self._emit(list(ll))
|
|
226
|
+
|
|
227
|
+
def add(self, *code: Code) -> Body:
|
|
228
|
+
"""Append anything statement-shaped: text, lines, another body, or nested
|
|
229
|
+
sequences of those. ``None`` contributes nothing."""
|
|
230
|
+
return self._emit(stmts(*code))
|
|
231
|
+
|
|
232
|
+
def extend(self, other: Code) -> Body:
|
|
233
|
+
"""Splice in another body or block of lines. Alias of :meth:`add`."""
|
|
234
|
+
return self.add(other)
|
|
235
|
+
|
|
236
|
+
def blank(self) -> Body:
|
|
237
|
+
"""Append a blank line."""
|
|
238
|
+
return self._emit([blank()])
|
|
239
|
+
|
|
240
|
+
# ------------------------------------------------------------------
|
|
241
|
+
# Comments
|
|
242
|
+
# ------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
def comment(self, text: str) -> Body:
|
|
245
|
+
"""Append a ``//`` comment with no leading blank line."""
|
|
246
|
+
return self._emit(write_comment_body(text))
|
|
247
|
+
|
|
248
|
+
def spaced_comment(self, text: str) -> Body:
|
|
249
|
+
"""Append a ``//`` comment preceded by a blank line."""
|
|
250
|
+
return self._emit(write_comment(text))
|
|
251
|
+
|
|
252
|
+
def doc_comment(self, text: str) -> Body:
|
|
253
|
+
"""Append a ``//!`` doxygen comment."""
|
|
254
|
+
return self._emit(write_doxygen_comment(text))
|
|
255
|
+
|
|
256
|
+
def banner(self, text: str) -> Body:
|
|
257
|
+
"""Append a ruled banner comment."""
|
|
258
|
+
return self._emit(write_banner_comment(text))
|
|
259
|
+
|
|
260
|
+
# ------------------------------------------------------------------
|
|
261
|
+
# Control flow
|
|
262
|
+
# ------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
def block(self, *, omit_if_empty: bool = False) -> AbstractContextManager[Body]:
|
|
265
|
+
"""A bare braced block, for scoping a local."""
|
|
266
|
+
return self._scope("{", "}", omit_if_empty=omit_if_empty)
|
|
267
|
+
|
|
268
|
+
def if_(
|
|
269
|
+
self, condition: str, *, omit_if_empty: bool = False
|
|
270
|
+
) -> AbstractContextManager[Body]:
|
|
271
|
+
"""``if (condition) { ... }``, which an :meth:`elif_` or :meth:`else_` may follow."""
|
|
272
|
+
return self._scope(
|
|
273
|
+
f"if ({condition}) {{", "}", omit_if_empty=omit_if_empty, chain=True
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def elif_(self, condition: str) -> AbstractContextManager[Body]:
|
|
277
|
+
"""``else if (condition) { ... }``. Must follow an ``if`` or another ``else if``."""
|
|
278
|
+
self._require_chain("elif_")
|
|
279
|
+
return self._scope(f"else if ({condition}) {{", "}", chain=True)
|
|
280
|
+
|
|
281
|
+
def else_(self) -> AbstractContextManager[Body]:
|
|
282
|
+
"""``else { ... }``. Must follow an ``if`` or an ``else if``."""
|
|
283
|
+
self._require_chain("else_")
|
|
284
|
+
return self._scope("else {", "}")
|
|
285
|
+
|
|
286
|
+
def branch(self, condition: str) -> AbstractContextManager[Body]:
|
|
287
|
+
"""``if`` the first time, ``else if`` while a chain is still open.
|
|
288
|
+
|
|
289
|
+
Lets a chain of arbitrary length come out of a plain loop::
|
|
290
|
+
|
|
291
|
+
for condition, code in dispatch:
|
|
292
|
+
with b.branch(condition):
|
|
293
|
+
b.add(code)
|
|
294
|
+
with b.else_():
|
|
295
|
+
b.raw(fprime.write_assert("0"))
|
|
296
|
+
"""
|
|
297
|
+
return (
|
|
298
|
+
self.elif_(condition) if self._current.open_chain else self.if_(condition)
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def while_(
|
|
302
|
+
self, condition: str, *, omit_if_empty: bool = False
|
|
303
|
+
) -> AbstractContextManager[Body]:
|
|
304
|
+
"""``while (condition) { ... }``"""
|
|
305
|
+
return self._scope(f"while ({condition}) {{", "}", omit_if_empty=omit_if_empty)
|
|
306
|
+
|
|
307
|
+
def do_while(self, condition: str) -> AbstractContextManager[Body]:
|
|
308
|
+
"""``do { ... } while (condition);``"""
|
|
309
|
+
return self._scope("do {", f"}} while ({condition});")
|
|
310
|
+
|
|
311
|
+
def for_(
|
|
312
|
+
self,
|
|
313
|
+
init: str,
|
|
314
|
+
condition: str,
|
|
315
|
+
step: str,
|
|
316
|
+
*,
|
|
317
|
+
omit_if_empty: bool = False,
|
|
318
|
+
staggered: bool = False,
|
|
319
|
+
) -> AbstractContextManager[Body]:
|
|
320
|
+
"""``for (init; condition; step) { ... }``.
|
|
321
|
+
|
|
322
|
+
``staggered=True`` splits the three clauses across lines.
|
|
323
|
+
"""
|
|
324
|
+
opening = (
|
|
325
|
+
f"""|for (
|
|
326
|
+
| {init};
|
|
327
|
+
| {condition};
|
|
328
|
+
| {step}
|
|
329
|
+
|) {{
|
|
330
|
+
|"""
|
|
331
|
+
if staggered
|
|
332
|
+
else f"for ({init}; {condition}; {step}) {{"
|
|
333
|
+
)
|
|
334
|
+
return self._scope(opening, "}", omit_if_empty=omit_if_empty)
|
|
335
|
+
|
|
336
|
+
def for_range(
|
|
337
|
+
self, declaration: str, container: str, *, omit_if_empty: bool = False
|
|
338
|
+
) -> AbstractContextManager[Body]:
|
|
339
|
+
"""``for (declaration : container) { ... }``, e.g. ``for_range("auto& e", "m_list")``."""
|
|
340
|
+
return self._scope(
|
|
341
|
+
f"for ({declaration} : {container}) {{", "}", omit_if_empty=omit_if_empty
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def scope(
|
|
345
|
+
self, opening: str, closing: str, *, omit_if_empty: bool = False
|
|
346
|
+
) -> AbstractContextManager[Body]:
|
|
347
|
+
"""An arbitrary scope, for shapes this module does not cover."""
|
|
348
|
+
return self._scope(opening, closing, omit_if_empty=omit_if_empty)
|
|
349
|
+
|
|
350
|
+
def if_directive(
|
|
351
|
+
self,
|
|
352
|
+
directive: str,
|
|
353
|
+
*,
|
|
354
|
+
omit_if_empty: bool = True,
|
|
355
|
+
spaced: bool = True,
|
|
356
|
+
) -> AbstractContextManager[Body]:
|
|
357
|
+
"""Bracket the block with a preprocessor ``directive`` and ``#endif``.
|
|
358
|
+
|
|
359
|
+
``directive`` is written verbatim and must include its ``#``. The guarded
|
|
360
|
+
code is not indented relative to the guard, since the directives sit at
|
|
361
|
+
column zero. ``spaced=False`` drops the blank lines around them.
|
|
362
|
+
"""
|
|
363
|
+
gap = "\n" if spaced else ""
|
|
364
|
+
return self._scope(
|
|
365
|
+
f"{gap}{directive}",
|
|
366
|
+
f"{gap}#endif",
|
|
367
|
+
omit_if_empty=omit_if_empty,
|
|
368
|
+
indent=False,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
@contextmanager
|
|
372
|
+
def switch(
|
|
373
|
+
self, selector: str, *, omit_if_empty: bool = False
|
|
374
|
+
) -> Generator[Switch]:
|
|
375
|
+
"""``switch (selector) { ... }``. Yields a :class:`Switch` for its cases."""
|
|
376
|
+
frame = _Frame(kind="switch")
|
|
377
|
+
self._frames.append(frame)
|
|
378
|
+
switch = Switch(self, frame)
|
|
379
|
+
try:
|
|
380
|
+
yield switch
|
|
381
|
+
except BaseException:
|
|
382
|
+
self._frames.pop()
|
|
383
|
+
raise
|
|
384
|
+
self._frames.pop()
|
|
385
|
+
if not frame.lines and omit_if_empty:
|
|
386
|
+
return
|
|
387
|
+
self._emit(
|
|
388
|
+
[
|
|
389
|
+
*_lines(f"switch ({selector}) {{"),
|
|
390
|
+
*indent_lines(frame.lines),
|
|
391
|
+
*_lines("}"),
|
|
392
|
+
]
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class Switch:
|
|
397
|
+
"""The cases of an open ``switch``. Obtained from :meth:`Body.switch`."""
|
|
398
|
+
|
|
399
|
+
def __init__(self, body: Body, frame: _Frame) -> None:
|
|
400
|
+
self._body = body
|
|
401
|
+
self._frame = frame
|
|
402
|
+
|
|
403
|
+
def _check_open(self) -> None:
|
|
404
|
+
if self._body._current is not self._frame:
|
|
405
|
+
raise ScopeError(
|
|
406
|
+
"this switch is not the innermost open scope; close any nested "
|
|
407
|
+
"scope before adding another case"
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
def case(
|
|
411
|
+
self, *labels: str, fallthrough: bool = False, braces: bool = True
|
|
412
|
+
) -> AbstractContextManager[Body]:
|
|
413
|
+
"""One or more ``case`` labels sharing a body.
|
|
414
|
+
|
|
415
|
+
A ``break;`` is appended unless ``fallthrough=True`` or the body already
|
|
416
|
+
ends in a statement that transfers control. Braces let an arm declare a
|
|
417
|
+
local, which a bare label cannot; ``braces=False`` emits the bare label.
|
|
418
|
+
"""
|
|
419
|
+
self._check_open()
|
|
420
|
+
if not labels:
|
|
421
|
+
raise ScopeError("case() needs at least one label")
|
|
422
|
+
last = f"case {labels[-1]}: {{" if braces else f"case {labels[-1]}:"
|
|
423
|
+
opening = "\n".join([*(f"case {l}:" for l in labels[:-1]), last])
|
|
424
|
+
return self._scoped(opening, fallthrough, braces)
|
|
425
|
+
|
|
426
|
+
def default(
|
|
427
|
+
self, *, fallthrough: bool = False, braces: bool = True
|
|
428
|
+
) -> AbstractContextManager[Body]:
|
|
429
|
+
"""The ``default`` label. See :meth:`case` for the arguments."""
|
|
430
|
+
self._check_open()
|
|
431
|
+
return self._scoped("default: {" if braces else "default:", fallthrough, braces)
|
|
432
|
+
|
|
433
|
+
@contextmanager
|
|
434
|
+
def _scoped(self, opening: str, fallthrough: bool, braces: bool) -> Generator[Body]:
|
|
435
|
+
body = self._body
|
|
436
|
+
frame = _Frame(kind="case")
|
|
437
|
+
body._frames.append(frame)
|
|
438
|
+
try:
|
|
439
|
+
yield body
|
|
440
|
+
except BaseException:
|
|
441
|
+
body._frames.pop()
|
|
442
|
+
raise
|
|
443
|
+
body._frames.pop()
|
|
444
|
+
inner = list(frame.lines)
|
|
445
|
+
if not fallthrough and not _terminates(frame.lines):
|
|
446
|
+
inner.append(_line("break;"))
|
|
447
|
+
closing = _lines("}") if braces else []
|
|
448
|
+
body._emit([*_lines(opening), *indent_lines(inner), *closing])
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""The builder API: the comfortable way to assemble a C++ document.
|
|
2
|
+
|
|
3
|
+
Nesting in the generated C++ follows nesting in the Python::
|
|
4
|
+
|
|
5
|
+
doc = CppDocBuilder("Counter", description="a counter")
|
|
6
|
+
doc.include("Fw/FPrimeBasicTypes.hpp")
|
|
7
|
+
|
|
8
|
+
with doc.namespace("Fw") as ns:
|
|
9
|
+
with ns.class_("Counter", final=True) as cls:
|
|
10
|
+
with cls.public("Constructors and destructors"):
|
|
11
|
+
cls.constructor(initializers=["m_count(0)"])
|
|
12
|
+
cls.destructor()
|
|
13
|
+
with cls.public("Public member functions"):
|
|
14
|
+
with cls.function("bump", ret="U32") as fn:
|
|
15
|
+
fn.body.line("m_count++;")
|
|
16
|
+
fn.body.line("return m_count;")
|
|
17
|
+
with cls.private("Member variables"):
|
|
18
|
+
cls.var("U32", "m_count", comment="How many bumps so far")
|
|
19
|
+
|
|
20
|
+
doc.write("build-artifacts")
|
|
21
|
+
|
|
22
|
+
``with`` is optional almost everywhere. A builder attaches to its parent when you
|
|
23
|
+
create it, fixing its position in the output, so you can keep filling it in
|
|
24
|
+
afterwards::
|
|
25
|
+
|
|
26
|
+
fn = cls.function("bump", ret="U32")
|
|
27
|
+
fn.param("U32", "by", default="1")
|
|
28
|
+
fn.body.line("return m_count + by;")
|
|
29
|
+
|
|
30
|
+
On access sections and preprocessor guards, ``with`` additionally makes an empty
|
|
31
|
+
section disappear instead of leaving a stray ``public:`` or an empty ``#if``.
|
|
32
|
+
|
|
33
|
+
A helper can build a fragment and return it for the caller to splice in with
|
|
34
|
+
:meth:`ClassBuilder.member`::
|
|
35
|
+
|
|
36
|
+
def accessor(cls, name, type_name):
|
|
37
|
+
fn = cls.function(f"get{name}", ret=type_name, const=True)
|
|
38
|
+
fn.body.line(f"return m_{name};")
|
|
39
|
+
|
|
40
|
+
for name, type_name in model.fields:
|
|
41
|
+
accessor(cls, name, type_name)
|
|
42
|
+
|
|
43
|
+
The implementation is split across submodules: :mod:`~.base` (the builder protocol
|
|
44
|
+
and per-document state), :mod:`~.coercion` (argument shapes), :mod:`~.definitions`
|
|
45
|
+
(functions, constructors, enums), :mod:`~.decoration` (banners, guards, access
|
|
46
|
+
sections), :mod:`~.scopes` (classes and namespaces) and :mod:`~.document` (the
|
|
47
|
+
document itself). Import from :mod:`fprime_cpp_codegen` or from here.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
from .base import BodyLike
|
|
53
|
+
from .decoration import AccessSection
|
|
54
|
+
from .definitions import (
|
|
55
|
+
ConstructorBuilder,
|
|
56
|
+
DestructorBuilder,
|
|
57
|
+
EnumBuilder,
|
|
58
|
+
FunctionBuilder,
|
|
59
|
+
Radix,
|
|
60
|
+
)
|
|
61
|
+
from .document import CppDocBuilder
|
|
62
|
+
from .scopes import ClassBuilder, NamespaceBuilder
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
"AccessSection",
|
|
66
|
+
"BodyLike",
|
|
67
|
+
"ClassBuilder",
|
|
68
|
+
"ConstructorBuilder",
|
|
69
|
+
"CppDocBuilder",
|
|
70
|
+
"DestructorBuilder",
|
|
71
|
+
"EnumBuilder",
|
|
72
|
+
"FunctionBuilder",
|
|
73
|
+
"NamespaceBuilder",
|
|
74
|
+
"Radix",
|
|
75
|
+
]
|