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,787 @@
|
|
|
1
|
+
"""Rendering a :class:`~fprime_cpp_codegen.doc.CppDoc` to header and source lines.
|
|
2
|
+
|
|
3
|
+
Two visitors walk the same document tree. :class:`HppWriter` emits declarations;
|
|
4
|
+
:class:`CppWriter` emits definitions. The header sees every member exactly once.
|
|
5
|
+
A source file sees only the definitions assigned to it, so calling
|
|
6
|
+
:func:`cpp_lines` once per ``.cpp`` base name splits a document across as many
|
|
7
|
+
translation units as you like.
|
|
8
|
+
|
|
9
|
+
Deciding where a definition goes is the logic here. Most go into a source file, but
|
|
10
|
+
templates, ``inline`` and ``constexpr`` functions must be visible in every
|
|
11
|
+
translation unit that uses them, as must every member of a templated class; those
|
|
12
|
+
are defined in the header and skipped by the source file. ``= delete`` and
|
|
13
|
+
``= default`` members have no definition to place.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
from collections.abc import Callable, Sequence
|
|
20
|
+
from dataclasses import dataclass, replace
|
|
21
|
+
|
|
22
|
+
from .comments import (
|
|
23
|
+
add_param_comment,
|
|
24
|
+
left_align_directive,
|
|
25
|
+
write_banner,
|
|
26
|
+
write_doxygen_comment_opt,
|
|
27
|
+
write_function_body,
|
|
28
|
+
)
|
|
29
|
+
from .doc import (
|
|
30
|
+
Class,
|
|
31
|
+
ClassMember,
|
|
32
|
+
Constructor,
|
|
33
|
+
CppDoc,
|
|
34
|
+
Definition,
|
|
35
|
+
Destructor,
|
|
36
|
+
Function,
|
|
37
|
+
HppFile,
|
|
38
|
+
Lines,
|
|
39
|
+
Member,
|
|
40
|
+
Namespace,
|
|
41
|
+
Output,
|
|
42
|
+
Param,
|
|
43
|
+
SVQualifier,
|
|
44
|
+
Variable,
|
|
45
|
+
)
|
|
46
|
+
from .errors import CppCodegenError, ValidationError
|
|
47
|
+
from .lines import (
|
|
48
|
+
INDENT_INCREMENT,
|
|
49
|
+
IndentMode,
|
|
50
|
+
Line,
|
|
51
|
+
add_prefix,
|
|
52
|
+
add_suffix,
|
|
53
|
+
blank,
|
|
54
|
+
indent_lines,
|
|
55
|
+
join_lists,
|
|
56
|
+
line,
|
|
57
|
+
lines,
|
|
58
|
+
render,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"Context",
|
|
63
|
+
"CppWriter",
|
|
64
|
+
"DocWriter",
|
|
65
|
+
"HppWriter",
|
|
66
|
+
"cpp_lines",
|
|
67
|
+
"hpp_lines",
|
|
68
|
+
"needs_definition",
|
|
69
|
+
"render_cpp",
|
|
70
|
+
"render_hpp",
|
|
71
|
+
"variable_defined_in_source",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _terminator(d: Definition, *, pure_virtual: bool = False) -> str:
|
|
76
|
+
"""The text ending a declaration: ``;``, ``= delete``, ``= default`` or ``= 0``."""
|
|
77
|
+
what = type(d).__name__.lower()
|
|
78
|
+
if d.deleted and d.defaulted:
|
|
79
|
+
raise ValidationError(f"this {what} is both deleted and defaulted; pick one")
|
|
80
|
+
if (d.deleted or d.defaulted) and d.body:
|
|
81
|
+
raise ValidationError(
|
|
82
|
+
f"this {what} is deleted or defaulted, so it cannot also have a body"
|
|
83
|
+
)
|
|
84
|
+
if pure_virtual and (d.deleted or d.defaulted):
|
|
85
|
+
raise ValidationError(
|
|
86
|
+
f"this {what} is pure virtual, so it cannot also be deleted or defaulted"
|
|
87
|
+
)
|
|
88
|
+
if d.deleted:
|
|
89
|
+
return " = delete;"
|
|
90
|
+
if d.defaulted:
|
|
91
|
+
return " = default;"
|
|
92
|
+
if pure_virtual:
|
|
93
|
+
return " = 0;"
|
|
94
|
+
return ";"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _namespace_opening(name: str) -> str:
|
|
98
|
+
"""The line opening a namespace. An empty name gives an unnamed namespace."""
|
|
99
|
+
return f"namespace {name} {{" if name else "namespace {"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _template_lines(params: str | None) -> list[Line]:
|
|
103
|
+
"""The ``template <...>`` line introducing a templated declaration."""
|
|
104
|
+
return lines(f"template <{params}>") if params is not None else []
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def variable_defined_in_source(v: Variable, *, in_class: bool) -> bool:
|
|
108
|
+
"""Whether ``v`` needs a definition in a source file, separate from its declaration."""
|
|
109
|
+
if v.constexpr:
|
|
110
|
+
return v.out_of_line_definition
|
|
111
|
+
if in_class:
|
|
112
|
+
return v.static
|
|
113
|
+
return v.extern
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _check_variable(v: Variable, *, in_class: bool) -> None:
|
|
117
|
+
"""Reject variable declarations that could not compile."""
|
|
118
|
+
if v.static and v.mutable:
|
|
119
|
+
raise ValidationError(
|
|
120
|
+
f"variable {v.name!r} is both static and mutable, which C++ does not allow"
|
|
121
|
+
)
|
|
122
|
+
if v.mutable and not in_class:
|
|
123
|
+
raise ValidationError(
|
|
124
|
+
f"variable {v.name!r} is mutable, which only means something for a class "
|
|
125
|
+
"data member"
|
|
126
|
+
)
|
|
127
|
+
if v.constexpr and v.init is None:
|
|
128
|
+
raise ValidationError(
|
|
129
|
+
f"variable {v.name!r} is constexpr, so it needs an initialiser"
|
|
130
|
+
)
|
|
131
|
+
if v.extern and v.static:
|
|
132
|
+
raise ValidationError(
|
|
133
|
+
f"variable {v.name!r} is both extern and static, which contradict each "
|
|
134
|
+
"other: one gives it external linkage and the other internal"
|
|
135
|
+
)
|
|
136
|
+
if v.extern and in_class:
|
|
137
|
+
raise ValidationError(
|
|
138
|
+
f"variable {v.name!r} is a class data member, so extern does not apply; "
|
|
139
|
+
"use static instead"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _check_params(params: Sequence[Param]) -> None:
|
|
144
|
+
"""Reject a default argument followed by one without a default.
|
|
145
|
+
|
|
146
|
+
C++ requires default arguments to be trailing.
|
|
147
|
+
"""
|
|
148
|
+
defaulted: str | None = None
|
|
149
|
+
for p in params:
|
|
150
|
+
if p.default is not None:
|
|
151
|
+
defaulted = p.name
|
|
152
|
+
elif defaulted is not None:
|
|
153
|
+
raise ValidationError(
|
|
154
|
+
f"parameter {p.name!r} has no default argument but follows "
|
|
155
|
+
f"{defaulted!r}, which does; C++ requires default arguments to be "
|
|
156
|
+
"trailing"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def needs_definition(d: Definition, *, pure_virtual: bool = False) -> bool:
|
|
161
|
+
"""Whether ``d`` has a definition that must be emitted somewhere.
|
|
162
|
+
|
|
163
|
+
A deleted or defaulted member has none. A pure virtual has none unless given a
|
|
164
|
+
body, which C++ permits as a default implementation a derived class can call.
|
|
165
|
+
"""
|
|
166
|
+
if not d.has_definition:
|
|
167
|
+
return False
|
|
168
|
+
return not (pure_virtual and not d.body)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True)
|
|
172
|
+
class Context:
|
|
173
|
+
"""Where the writer currently is in the document.
|
|
174
|
+
|
|
175
|
+
``class_names`` runs outermost-first and each entry may itself be qualified, so a
|
|
176
|
+
nested class is spelled ``Outer::Inner`` in the source file while its constructor
|
|
177
|
+
is spelled ``Inner``.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
hpp_file: HppFile
|
|
181
|
+
default_cpp_file_name: str
|
|
182
|
+
output_cpp_file_name: str | None = None
|
|
183
|
+
"""The source file currently being written, if not the document default."""
|
|
184
|
+
|
|
185
|
+
class_names: tuple[str, ...] = ()
|
|
186
|
+
|
|
187
|
+
inline_definitions: bool = False
|
|
188
|
+
"""Set inside a templated class, whose members must all be defined in the header.
|
|
189
|
+
Inherited by nested classes."""
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def cpp_file_name(self) -> str:
|
|
193
|
+
"""The source file being written."""
|
|
194
|
+
return self.output_cpp_file_name or self.default_cpp_file_name
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def enclosing_class_qualified(self) -> str:
|
|
198
|
+
"""The enclosing class, fully qualified, e.g. ``"Outer::Inner"``."""
|
|
199
|
+
return "::".join(self.class_names)
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def enclosing_class_unqualified(self) -> str:
|
|
203
|
+
"""The enclosing class with every qualifier stripped, e.g. ``"Inner"``."""
|
|
204
|
+
if not self.class_names:
|
|
205
|
+
raise ValidationError(
|
|
206
|
+
"a constructor or destructor was placed outside a class, so it has "
|
|
207
|
+
"no name to take"
|
|
208
|
+
)
|
|
209
|
+
return self.class_names[-1].split("::")[-1]
|
|
210
|
+
|
|
211
|
+
def nested_in(self, class_name: str, *, inline: bool = False) -> Context:
|
|
212
|
+
"""Return this context, descended into ``class_name``."""
|
|
213
|
+
return replace(
|
|
214
|
+
self,
|
|
215
|
+
class_names=(*self.class_names, class_name),
|
|
216
|
+
inline_definitions=self.inline_definitions or inline,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class DocWriter(ABC):
|
|
221
|
+
"""Dispatch for a document walk. Subclass to render something new."""
|
|
222
|
+
|
|
223
|
+
def visit_member(self, ctx: Context, member: Member) -> list[Line]:
|
|
224
|
+
"""Dispatch a document- or namespace-scope member."""
|
|
225
|
+
if isinstance(member, Class):
|
|
226
|
+
return self.visit_class(ctx, member)
|
|
227
|
+
if isinstance(member, Lines):
|
|
228
|
+
return self.visit_lines(ctx, member)
|
|
229
|
+
if isinstance(member, Function):
|
|
230
|
+
return self.visit_function(ctx, member)
|
|
231
|
+
if isinstance(member, Namespace):
|
|
232
|
+
return self.visit_namespace(ctx, member)
|
|
233
|
+
if isinstance(member, Variable):
|
|
234
|
+
return self.visit_variable(ctx, member)
|
|
235
|
+
if isinstance(member, (Constructor, Destructor)):
|
|
236
|
+
raise CppCodegenError(
|
|
237
|
+
f"a {type(member).__name__} may only appear inside a class, not at "
|
|
238
|
+
"document or namespace scope"
|
|
239
|
+
)
|
|
240
|
+
raise CppCodegenError(f"not a document member: {member!r}")
|
|
241
|
+
|
|
242
|
+
def visit_class_member(self, ctx: Context, member: ClassMember) -> list[Line]:
|
|
243
|
+
"""Dispatch a class-scope member."""
|
|
244
|
+
if isinstance(member, Class):
|
|
245
|
+
return self.visit_class(ctx, member)
|
|
246
|
+
if isinstance(member, Lines):
|
|
247
|
+
return self.visit_lines(ctx, member)
|
|
248
|
+
if isinstance(member, Constructor):
|
|
249
|
+
return self.visit_constructor(ctx, member)
|
|
250
|
+
if isinstance(member, Destructor):
|
|
251
|
+
return self.visit_destructor(ctx, member)
|
|
252
|
+
if isinstance(member, Function):
|
|
253
|
+
return self.visit_function(ctx, member)
|
|
254
|
+
if isinstance(member, Variable):
|
|
255
|
+
return self.visit_variable(ctx, member)
|
|
256
|
+
if isinstance(member, Namespace):
|
|
257
|
+
raise CppCodegenError(
|
|
258
|
+
"a namespace may not be declared inside a class; move it out to "
|
|
259
|
+
"document scope"
|
|
260
|
+
)
|
|
261
|
+
raise CppCodegenError(f"not a class member: {member!r}")
|
|
262
|
+
|
|
263
|
+
def visit_members(self, ctx: Context, members: list[Member]) -> list[Line]:
|
|
264
|
+
"""Dispatch every document- or namespace-scope member in order."""
|
|
265
|
+
return [l for m in members for l in self.visit_member(ctx, m)]
|
|
266
|
+
|
|
267
|
+
def visit_class_members(
|
|
268
|
+
self, ctx: Context, members: list[ClassMember]
|
|
269
|
+
) -> list[Line]:
|
|
270
|
+
"""Dispatch every class-scope member in order."""
|
|
271
|
+
return [l for m in members for l in self.visit_class_member(ctx, m)]
|
|
272
|
+
|
|
273
|
+
def param_string(self, p: Param) -> str:
|
|
274
|
+
"""Render a parameter as ``"<type> <name>"``.
|
|
275
|
+
|
|
276
|
+
Both writers use the header spelling. An out-of-class definition's parameter
|
|
277
|
+
list is looked up in the class's scope, so names resolving unqualified in the
|
|
278
|
+
header resolve here too. Only the return type, which precedes ``Class::``,
|
|
279
|
+
needs the source spelling.
|
|
280
|
+
"""
|
|
281
|
+
return f"{p.type.hpp} {p.name}"
|
|
282
|
+
|
|
283
|
+
@abstractmethod
|
|
284
|
+
def write_params(self, prefix: str, params: list[Param]) -> list[Line]:
|
|
285
|
+
"""Render ``prefix`` followed by a parenthesised parameter list."""
|
|
286
|
+
|
|
287
|
+
@abstractmethod
|
|
288
|
+
def visit_class(self, ctx: Context, c: Class) -> list[Line]: ...
|
|
289
|
+
|
|
290
|
+
@abstractmethod
|
|
291
|
+
def visit_constructor(self, ctx: Context, ctor: Constructor) -> list[Line]: ...
|
|
292
|
+
|
|
293
|
+
@abstractmethod
|
|
294
|
+
def visit_destructor(self, ctx: Context, dtor: Destructor) -> list[Line]: ...
|
|
295
|
+
|
|
296
|
+
@abstractmethod
|
|
297
|
+
def visit_function(self, ctx: Context, fn: Function) -> list[Line]: ...
|
|
298
|
+
|
|
299
|
+
@abstractmethod
|
|
300
|
+
def visit_lines(self, ctx: Context, ll: Lines) -> list[Line]: ...
|
|
301
|
+
|
|
302
|
+
@abstractmethod
|
|
303
|
+
def visit_namespace(self, ctx: Context, ns: Namespace) -> list[Line]: ...
|
|
304
|
+
|
|
305
|
+
@abstractmethod
|
|
306
|
+
def visit_variable(self, ctx: Context, v: Variable) -> list[Line]: ...
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def initializer_lines(initializers: list[str]) -> list[Line]:
|
|
310
|
+
"""Render a member-initializer list, one entry per line, comma-separated."""
|
|
311
|
+
last = len(initializers) - 1
|
|
312
|
+
return [
|
|
313
|
+
line(init + ("," if i < last else "")).indent_in(2 * INDENT_INCREMENT)
|
|
314
|
+
for i, init in enumerate(initializers)
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class HppWriter(DocWriter):
|
|
319
|
+
"""Renders a document's declarations into header lines."""
|
|
320
|
+
|
|
321
|
+
# ------------------------------------------------------------------
|
|
322
|
+
# Parameters
|
|
323
|
+
# ------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
def param_string(self, p: Param) -> str:
|
|
326
|
+
"""Render a parameter, including its default argument if it has one."""
|
|
327
|
+
s = super().param_string(p)
|
|
328
|
+
return f"{s} = {p.default}" if p.default is not None else s
|
|
329
|
+
|
|
330
|
+
def param_lines(self, p: Param, *, comma: bool) -> list[Line]:
|
|
331
|
+
"""Render one parameter, with its post-comment hanging beneath it."""
|
|
332
|
+
return add_param_comment(
|
|
333
|
+
self.param_string(p) + ("," if comma else ""), p.comment
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def write_params(self, prefix: str, params: list[Param]) -> list[Line]:
|
|
337
|
+
"""Render a parameter list, breaking one-per-line when there is more than one.
|
|
338
|
+
|
|
339
|
+
A lone uncommented parameter stays on the same line as the name; anything else
|
|
340
|
+
is exploded so the post-comments have somewhere to go.
|
|
341
|
+
"""
|
|
342
|
+
_check_params(params)
|
|
343
|
+
if not params:
|
|
344
|
+
return lines(f"{prefix}()")
|
|
345
|
+
if len(params) == 1 and params[0].comment is None:
|
|
346
|
+
return lines(f"{prefix}({self.param_string(params[0])})")
|
|
347
|
+
last = len(params) - 1
|
|
348
|
+
body = [
|
|
349
|
+
l for i, p in enumerate(params) for l in self.param_lines(p, comma=i < last)
|
|
350
|
+
]
|
|
351
|
+
return [
|
|
352
|
+
line(f"{prefix}("),
|
|
353
|
+
*indent_lines(body, 2 * INDENT_INCREMENT),
|
|
354
|
+
line(")"),
|
|
355
|
+
]
|
|
356
|
+
|
|
357
|
+
# ------------------------------------------------------------------
|
|
358
|
+
# Include guard
|
|
359
|
+
# ------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
def open_include_guard(self, guard: str) -> list[Line]:
|
|
362
|
+
"""Render the opening half of an include guard."""
|
|
363
|
+
return lines(f"""
|
|
364
|
+
|#ifndef {guard}
|
|
365
|
+
|#define {guard}""")
|
|
366
|
+
|
|
367
|
+
def close_include_guard(self) -> list[Line]:
|
|
368
|
+
"""Render the closing half of an include guard."""
|
|
369
|
+
return lines("""
|
|
370
|
+
|#endif""")
|
|
371
|
+
|
|
372
|
+
# ------------------------------------------------------------------
|
|
373
|
+
# Declaration shaping
|
|
374
|
+
# ------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
def add_trailing(
|
|
377
|
+
self,
|
|
378
|
+
decl: list[Line],
|
|
379
|
+
*,
|
|
380
|
+
const: bool = False,
|
|
381
|
+
noexcept: bool = False,
|
|
382
|
+
override: bool = False,
|
|
383
|
+
final: bool = False,
|
|
384
|
+
) -> list[Line]:
|
|
385
|
+
"""Append trailing specifiers in the order C++ requires them."""
|
|
386
|
+
if const:
|
|
387
|
+
decl = add_suffix(decl, " const")
|
|
388
|
+
if noexcept:
|
|
389
|
+
decl = add_suffix(decl, " noexcept")
|
|
390
|
+
if override:
|
|
391
|
+
decl = add_suffix(decl, " override")
|
|
392
|
+
if final:
|
|
393
|
+
decl = add_suffix(decl, " final")
|
|
394
|
+
return decl
|
|
395
|
+
|
|
396
|
+
def defines_here(
|
|
397
|
+
self, ctx: Context, d: Definition, *, pure_virtual: bool = False
|
|
398
|
+
) -> bool:
|
|
399
|
+
"""Whether ``d``'s definition belongs in the header, next to its declaration."""
|
|
400
|
+
return needs_definition(d, pure_virtual=pure_virtual) and (
|
|
401
|
+
ctx.inline_definitions or d.defined_in_header
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# ------------------------------------------------------------------
|
|
405
|
+
# Members
|
|
406
|
+
# ------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
def visit_class(self, ctx: Context, c: Class) -> list[Line]:
|
|
409
|
+
"""Render a class declaration and everything in it."""
|
|
410
|
+
kind = "struct" if c.struct else "class"
|
|
411
|
+
head = f"{kind} {c.name} final" if c.final else f"{kind} {c.name}"
|
|
412
|
+
if c.superclass_decls is not None:
|
|
413
|
+
open_lines = [
|
|
414
|
+
line(f"{head} :"),
|
|
415
|
+
line(c.superclass_decls).indent_in(),
|
|
416
|
+
line("{"),
|
|
417
|
+
]
|
|
418
|
+
else:
|
|
419
|
+
open_lines = lines(f"{head} {{")
|
|
420
|
+
inner = ctx.nested_in(c.name, inline=c.template is not None)
|
|
421
|
+
body = self.visit_class_members(inner, c.members)
|
|
422
|
+
return [
|
|
423
|
+
*write_doxygen_comment_opt(c.comment),
|
|
424
|
+
*_template_lines(c.template),
|
|
425
|
+
*open_lines,
|
|
426
|
+
*indent_lines(body, 2 * INDENT_INCREMENT),
|
|
427
|
+
blank(),
|
|
428
|
+
line("};"),
|
|
429
|
+
]
|
|
430
|
+
|
|
431
|
+
def visit_constructor(self, ctx: Context, ctor: Constructor) -> list[Line]:
|
|
432
|
+
"""Render a constructor declaration, with its body if that belongs here."""
|
|
433
|
+
decl = self.write_params(ctx.enclosing_class_unqualified, ctor.params)
|
|
434
|
+
if ctor.constexpr:
|
|
435
|
+
decl = add_prefix("constexpr ", decl)
|
|
436
|
+
if ctor.explicit:
|
|
437
|
+
decl = add_prefix("explicit ", decl)
|
|
438
|
+
decl = self.add_trailing(decl, noexcept=ctor.noexcept)
|
|
439
|
+
if self.defines_here(ctx, ctor):
|
|
440
|
+
if ctor.initializers:
|
|
441
|
+
decl = [*add_suffix(decl, " :"), *initializer_lines(ctor.initializers)]
|
|
442
|
+
tail = [*decl, *write_function_body(ctor.body)]
|
|
443
|
+
else:
|
|
444
|
+
tail = join_lists(IndentMode.NO_INDENT, decl, "", lines(_terminator(ctor)))
|
|
445
|
+
return [
|
|
446
|
+
*write_doxygen_comment_opt(ctor.comment),
|
|
447
|
+
*_template_lines(ctor.template),
|
|
448
|
+
*tail,
|
|
449
|
+
]
|
|
450
|
+
|
|
451
|
+
def visit_destructor(self, ctx: Context, dtor: Destructor) -> list[Line]:
|
|
452
|
+
"""Render a destructor declaration, with its body if that belongs here."""
|
|
453
|
+
prefix = "virtual " if dtor.virtual else ""
|
|
454
|
+
decl = self.add_trailing(
|
|
455
|
+
lines(f"{prefix}~{ctx.enclosing_class_unqualified}()"),
|
|
456
|
+
noexcept=dtor.noexcept,
|
|
457
|
+
override=dtor.override,
|
|
458
|
+
)
|
|
459
|
+
if self.defines_here(ctx, dtor):
|
|
460
|
+
tail = [*decl, *write_function_body(dtor.body)]
|
|
461
|
+
else:
|
|
462
|
+
tail = join_lists(IndentMode.NO_INDENT, decl, "", lines(_terminator(dtor)))
|
|
463
|
+
return [*write_doxygen_comment_opt(dtor.comment), *tail]
|
|
464
|
+
|
|
465
|
+
def visit_function(self, ctx: Context, fn: Function) -> list[Line]:
|
|
466
|
+
"""Render a function declaration, with its body if that belongs here."""
|
|
467
|
+
pure = fn.sv is SVQualifier.PURE_VIRTUAL
|
|
468
|
+
lead = ""
|
|
469
|
+
if fn.sv is SVQualifier.STATIC:
|
|
470
|
+
lead += "static "
|
|
471
|
+
elif fn.sv in (SVQualifier.VIRTUAL, SVQualifier.PURE_VIRTUAL):
|
|
472
|
+
lead += "virtual "
|
|
473
|
+
if fn.constexpr:
|
|
474
|
+
lead += "constexpr "
|
|
475
|
+
if fn.inline:
|
|
476
|
+
lead += "inline "
|
|
477
|
+
ret = f"{fn.ret_type.hpp} " if fn.ret_type.hpp else ""
|
|
478
|
+
decl = self.add_trailing(
|
|
479
|
+
self.write_params(f"{lead}{ret}{fn.name}", fn.params),
|
|
480
|
+
const=fn.const,
|
|
481
|
+
noexcept=fn.noexcept,
|
|
482
|
+
override=fn.sv is SVQualifier.OVERRIDE,
|
|
483
|
+
final=fn.sv is SVQualifier.FINAL,
|
|
484
|
+
)
|
|
485
|
+
if self.defines_here(ctx, fn, pure_virtual=pure):
|
|
486
|
+
if pure:
|
|
487
|
+
raise ValidationError(
|
|
488
|
+
f"function {fn.name!r} is pure virtual with a body, and its "
|
|
489
|
+
"definition has to live in the header, but C++ does not allow a "
|
|
490
|
+
"pure virtual to be defined inside its class; emit the "
|
|
491
|
+
"out-of-line definition yourself with Lines"
|
|
492
|
+
)
|
|
493
|
+
tail = [*decl, *write_function_body(fn.body)]
|
|
494
|
+
else:
|
|
495
|
+
tail = join_lists(
|
|
496
|
+
IndentMode.NO_INDENT,
|
|
497
|
+
decl,
|
|
498
|
+
"",
|
|
499
|
+
lines(_terminator(fn, pure_virtual=pure)),
|
|
500
|
+
)
|
|
501
|
+
return [
|
|
502
|
+
*write_doxygen_comment_opt(fn.comment),
|
|
503
|
+
*_template_lines(fn.template),
|
|
504
|
+
*tail,
|
|
505
|
+
]
|
|
506
|
+
|
|
507
|
+
def visit_variable(self, ctx: Context, v: Variable) -> list[Line]:
|
|
508
|
+
"""Render a variable declaration.
|
|
509
|
+
|
|
510
|
+
The initialiser is included only when this declaration is also the definition;
|
|
511
|
+
otherwise it goes to the source file with the definition.
|
|
512
|
+
"""
|
|
513
|
+
in_class = bool(ctx.class_names)
|
|
514
|
+
_check_variable(v, in_class=in_class)
|
|
515
|
+
lead = ""
|
|
516
|
+
if v.extern and not in_class:
|
|
517
|
+
lead += "extern "
|
|
518
|
+
if v.static:
|
|
519
|
+
lead += "static "
|
|
520
|
+
if v.constexpr:
|
|
521
|
+
lead += "constexpr "
|
|
522
|
+
if v.const:
|
|
523
|
+
lead += "const "
|
|
524
|
+
if v.mutable:
|
|
525
|
+
lead += "mutable "
|
|
526
|
+
decl = f"{lead}{v.type.hpp} {v.declarator}"
|
|
527
|
+
if v.init is not None and not variable_defined_in_source(v, in_class=in_class):
|
|
528
|
+
decl += f" = {v.init}"
|
|
529
|
+
return [*write_doxygen_comment_opt(v.comment), *lines(f"{decl};")]
|
|
530
|
+
|
|
531
|
+
def visit_lines(self, ctx: Context, ll: Lines) -> list[Line]:
|
|
532
|
+
"""Emit raw lines unless they are marked source-only."""
|
|
533
|
+
return [] if ll.output is Output.CPP else list(ll.content)
|
|
534
|
+
|
|
535
|
+
def visit_namespace(self, ctx: Context, ns: Namespace) -> list[Line]:
|
|
536
|
+
"""Render a namespace and everything in it.
|
|
537
|
+
|
|
538
|
+
The header emits the namespace even when empty, since a declaration-free
|
|
539
|
+
namespace may exist only to be reopened elsewhere.
|
|
540
|
+
"""
|
|
541
|
+
return [
|
|
542
|
+
blank(),
|
|
543
|
+
line(_namespace_opening(ns.name)),
|
|
544
|
+
*indent_lines(self.visit_members(ctx, ns.members)),
|
|
545
|
+
blank(),
|
|
546
|
+
line("}"),
|
|
547
|
+
]
|
|
548
|
+
|
|
549
|
+
# ------------------------------------------------------------------
|
|
550
|
+
# Document
|
|
551
|
+
# ------------------------------------------------------------------
|
|
552
|
+
|
|
553
|
+
def visit_doc(self, doc: CppDoc) -> list[Line]:
|
|
554
|
+
"""Render the whole header file."""
|
|
555
|
+
ctx = Context(doc.hpp_file, doc.cpp_file_name)
|
|
556
|
+
ext = doc.hpp_file.name.rsplit(".", 1)[-1]
|
|
557
|
+
out = [
|
|
558
|
+
*write_banner(
|
|
559
|
+
doc.file_banner, doc.hpp_file.name, f"{ext} file for {doc.description}"
|
|
560
|
+
),
|
|
561
|
+
*self.open_include_guard(doc.hpp_file.include_guard),
|
|
562
|
+
*self.visit_members(ctx, doc.members),
|
|
563
|
+
*self.close_include_guard(),
|
|
564
|
+
]
|
|
565
|
+
return [left_align_directive(l) for l in out]
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
class CppWriter(DocWriter):
|
|
569
|
+
"""Renders a document's definitions into source lines for one ``.cpp`` file."""
|
|
570
|
+
|
|
571
|
+
# ------------------------------------------------------------------
|
|
572
|
+
# Parameters
|
|
573
|
+
# ------------------------------------------------------------------
|
|
574
|
+
|
|
575
|
+
def write_params(self, prefix: str, params: list[Param]) -> list[Line]:
|
|
576
|
+
"""Render a parameter list. Comments and defaults belong in the header."""
|
|
577
|
+
if not params:
|
|
578
|
+
return lines(f"{prefix}()")
|
|
579
|
+
if len(params) == 1:
|
|
580
|
+
return lines(f"{prefix}({self.param_string(params[0])})")
|
|
581
|
+
last = len(params) - 1
|
|
582
|
+
body = [
|
|
583
|
+
line(self.param_string(p) + ("," if i < last else ""))
|
|
584
|
+
for i, p in enumerate(params)
|
|
585
|
+
]
|
|
586
|
+
return [
|
|
587
|
+
line(f"{prefix}("),
|
|
588
|
+
*indent_lines(body, 2 * INDENT_INCREMENT),
|
|
589
|
+
line(")"),
|
|
590
|
+
]
|
|
591
|
+
|
|
592
|
+
# ------------------------------------------------------------------
|
|
593
|
+
# File selection
|
|
594
|
+
# ------------------------------------------------------------------
|
|
595
|
+
|
|
596
|
+
def write_selected(
|
|
597
|
+
self,
|
|
598
|
+
ctx: Context,
|
|
599
|
+
cpp_file: str | None,
|
|
600
|
+
render_lines: Callable[[], list[Line]],
|
|
601
|
+
) -> list[Line]:
|
|
602
|
+
"""Render only if ``cpp_file`` names the source file being written.
|
|
603
|
+
|
|
604
|
+
``None`` means the document's default source file. This is the mechanism
|
|
605
|
+
behind splitting one document across several ``.cpp`` files.
|
|
606
|
+
"""
|
|
607
|
+
selected = (
|
|
608
|
+
f"{cpp_file}.cpp" if cpp_file is not None else ctx.default_cpp_file_name
|
|
609
|
+
)
|
|
610
|
+
return render_lines() if selected == ctx.cpp_file_name else []
|
|
611
|
+
|
|
612
|
+
def defines_here(self, d: Definition, *, pure_virtual: bool = False) -> bool:
|
|
613
|
+
"""Whether ``d``'s definition belongs in a source file at all."""
|
|
614
|
+
return (
|
|
615
|
+
needs_definition(d, pure_virtual=pure_virtual) and not d.defined_in_header
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
# ------------------------------------------------------------------
|
|
619
|
+
# Members
|
|
620
|
+
# ------------------------------------------------------------------
|
|
621
|
+
|
|
622
|
+
def visit_class(self, ctx: Context, c: Class) -> list[Line]:
|
|
623
|
+
"""Descend into a class. The class itself contributes no source text.
|
|
624
|
+
|
|
625
|
+
A templated class contributes nothing: all its members are defined in the
|
|
626
|
+
header.
|
|
627
|
+
"""
|
|
628
|
+
if c.template is not None:
|
|
629
|
+
return []
|
|
630
|
+
return self.visit_class_members(ctx.nested_in(c.name), c.members)
|
|
631
|
+
|
|
632
|
+
def visit_constructor(self, ctx: Context, ctor: Constructor) -> list[Line]:
|
|
633
|
+
"""Render a constructor definition, member-initializer list included."""
|
|
634
|
+
if not self.defines_here(ctor):
|
|
635
|
+
return []
|
|
636
|
+
|
|
637
|
+
def render_lines() -> list[Line]:
|
|
638
|
+
params = self.write_params(ctx.enclosing_class_unqualified, ctor.params)
|
|
639
|
+
if ctor.noexcept:
|
|
640
|
+
params = add_suffix(params, " noexcept")
|
|
641
|
+
if ctor.initializers:
|
|
642
|
+
params = add_suffix(params, " :")
|
|
643
|
+
return [
|
|
644
|
+
blank(),
|
|
645
|
+
*lines(f"{ctx.enclosing_class_qualified} ::"),
|
|
646
|
+
*indent_lines(params),
|
|
647
|
+
*initializer_lines(ctor.initializers),
|
|
648
|
+
*write_function_body(ctor.body),
|
|
649
|
+
]
|
|
650
|
+
|
|
651
|
+
return self.write_selected(ctx, ctor.cpp_file, render_lines)
|
|
652
|
+
|
|
653
|
+
def visit_destructor(self, ctx: Context, dtor: Destructor) -> list[Line]:
|
|
654
|
+
"""Render a destructor definition."""
|
|
655
|
+
if not self.defines_here(dtor):
|
|
656
|
+
return []
|
|
657
|
+
|
|
658
|
+
def render_lines() -> list[Line]:
|
|
659
|
+
decl = lines(f"~{ctx.enclosing_class_unqualified}()")
|
|
660
|
+
if dtor.noexcept:
|
|
661
|
+
decl = add_suffix(decl, " noexcept")
|
|
662
|
+
return [
|
|
663
|
+
blank(),
|
|
664
|
+
line(f"{ctx.enclosing_class_qualified} ::"),
|
|
665
|
+
*indent_lines(decl),
|
|
666
|
+
*write_function_body(dtor.body),
|
|
667
|
+
]
|
|
668
|
+
|
|
669
|
+
return self.write_selected(ctx, dtor.cpp_file, render_lines)
|
|
670
|
+
|
|
671
|
+
def visit_function(self, ctx: Context, fn: Function) -> list[Line]:
|
|
672
|
+
"""Render a function definition.
|
|
673
|
+
|
|
674
|
+
A pure virtual with a body lands here as a default implementation; one without
|
|
675
|
+
a body has nothing to define.
|
|
676
|
+
"""
|
|
677
|
+
pure = fn.sv is SVQualifier.PURE_VIRTUAL
|
|
678
|
+
if not self.defines_here(fn, pure_virtual=pure):
|
|
679
|
+
return []
|
|
680
|
+
|
|
681
|
+
def render_lines() -> list[Line]:
|
|
682
|
+
prototype = self.write_params(fn.name, fn.params)
|
|
683
|
+
if fn.const:
|
|
684
|
+
prototype = add_suffix(prototype, " const")
|
|
685
|
+
if fn.noexcept:
|
|
686
|
+
prototype = add_suffix(prototype, " noexcept")
|
|
687
|
+
ret = f"{fn.ret_type.cpp} " if fn.ret_type.cpp else ""
|
|
688
|
+
body = write_function_body(fn.body)
|
|
689
|
+
if ctx.class_names:
|
|
690
|
+
start = [
|
|
691
|
+
line(f"{ret}{ctx.enclosing_class_qualified} ::"),
|
|
692
|
+
*indent_lines(prototype),
|
|
693
|
+
]
|
|
694
|
+
content = [*start, *body]
|
|
695
|
+
else:
|
|
696
|
+
content = join_lists(
|
|
697
|
+
IndentMode.NO_INDENT, add_prefix(ret, prototype), " ", body
|
|
698
|
+
)
|
|
699
|
+
return [blank(), *content]
|
|
700
|
+
|
|
701
|
+
return self.write_selected(ctx, fn.cpp_file, render_lines)
|
|
702
|
+
|
|
703
|
+
def visit_variable(self, ctx: Context, v: Variable) -> list[Line]:
|
|
704
|
+
"""Render a variable definition, if this variable needs one out of line."""
|
|
705
|
+
in_class = bool(ctx.class_names)
|
|
706
|
+
_check_variable(v, in_class=in_class)
|
|
707
|
+
if not variable_defined_in_source(v, in_class=in_class):
|
|
708
|
+
return []
|
|
709
|
+
|
|
710
|
+
def render_lines() -> list[Line]:
|
|
711
|
+
qualifier = f"{ctx.enclosing_class_qualified}::" if in_class else ""
|
|
712
|
+
if v.constexpr:
|
|
713
|
+
# The initialiser stays with the in-class declaration; this definition
|
|
714
|
+
# exists only to give the constant an address.
|
|
715
|
+
decl = f"constexpr {v.type.cpp} {qualifier}{v.declarator}"
|
|
716
|
+
else:
|
|
717
|
+
lead = "const " if v.const else ""
|
|
718
|
+
decl = f"{lead}{v.type.cpp} {qualifier}{v.declarator}"
|
|
719
|
+
if v.init is not None:
|
|
720
|
+
decl += f" = {v.init}"
|
|
721
|
+
return [blank(), *lines(f"{decl};")]
|
|
722
|
+
|
|
723
|
+
return self.write_selected(ctx, v.cpp_file, render_lines)
|
|
724
|
+
|
|
725
|
+
def visit_lines(self, ctx: Context, ll: Lines) -> list[Line]:
|
|
726
|
+
"""Emit raw lines unless they are marked header-only."""
|
|
727
|
+
if ll.output is Output.HPP:
|
|
728
|
+
return []
|
|
729
|
+
return self.write_selected(ctx, ll.cpp_file, lambda: list(ll.content))
|
|
730
|
+
|
|
731
|
+
def visit_namespace(self, ctx: Context, ns: Namespace) -> list[Line]:
|
|
732
|
+
"""Render a namespace, or nothing at all if it contributes no definitions.
|
|
733
|
+
|
|
734
|
+
Namespaces routinely come out empty here, since a document split across
|
|
735
|
+
several source files has members belonging to a different ``.cpp`` than the
|
|
736
|
+
one being written.
|
|
737
|
+
"""
|
|
738
|
+
body = self.visit_members(ctx, ns.members)
|
|
739
|
+
if not body:
|
|
740
|
+
return []
|
|
741
|
+
return [
|
|
742
|
+
blank(),
|
|
743
|
+
line(_namespace_opening(ns.name)),
|
|
744
|
+
*indent_lines(body),
|
|
745
|
+
blank(),
|
|
746
|
+
line("}"),
|
|
747
|
+
]
|
|
748
|
+
|
|
749
|
+
# ------------------------------------------------------------------
|
|
750
|
+
# Document
|
|
751
|
+
# ------------------------------------------------------------------
|
|
752
|
+
|
|
753
|
+
def visit_doc(self, doc: CppDoc, cpp_file: str | None = None) -> list[Line]:
|
|
754
|
+
"""Render one source file. ``cpp_file`` is a base name without extension."""
|
|
755
|
+
output = f"{cpp_file}.cpp" if cpp_file is not None else None
|
|
756
|
+
ctx = Context(doc.hpp_file, doc.cpp_file_name, output)
|
|
757
|
+
out = [
|
|
758
|
+
*write_banner(
|
|
759
|
+
doc.file_banner, ctx.cpp_file_name, f"cpp file for {doc.description}"
|
|
760
|
+
),
|
|
761
|
+
*self.visit_members(ctx, doc.members),
|
|
762
|
+
]
|
|
763
|
+
return [left_align_directive(l) for l in out]
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def hpp_lines(doc: CppDoc) -> list[Line]:
|
|
767
|
+
"""Render ``doc``'s header as lines."""
|
|
768
|
+
return HppWriter().visit_doc(doc)
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def cpp_lines(doc: CppDoc, cpp_file: str | None = None) -> list[Line]:
|
|
772
|
+
"""Render one of ``doc``'s source files as lines.
|
|
773
|
+
|
|
774
|
+
``cpp_file`` is a base name without extension; ``None`` selects the document's
|
|
775
|
+
default source file.
|
|
776
|
+
"""
|
|
777
|
+
return CppWriter().visit_doc(doc, cpp_file)
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def render_hpp(doc: CppDoc) -> str:
|
|
781
|
+
"""Render ``doc``'s header as file text."""
|
|
782
|
+
return render(hpp_lines(doc))
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def render_cpp(doc: CppDoc, cpp_file: str | None = None) -> str:
|
|
786
|
+
"""Render one of ``doc``'s source files as file text."""
|
|
787
|
+
return render(cpp_lines(doc, cpp_file))
|