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,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))