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,55 @@
1
+ """The builder protocol and the state shared across one document."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from collections.abc import Sequence
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Generic, TypeVar
9
+
10
+ from ..body import Code
11
+
12
+ _T = TypeVar("_T")
13
+ _T2 = TypeVar("_T2")
14
+
15
+ #: What a body-shaped argument accepts. See :data:`fprime_cpp_codegen.body.Code`.
16
+ BodyLike = Code
17
+
18
+
19
+ class _Builder(ABC, Generic[_T]):
20
+ """Something that turns into an IR node when the document is built."""
21
+
22
+ @abstractmethod
23
+ def build(self) -> _T:
24
+ """Produce the IR node. Safe to call more than once."""
25
+
26
+ def build_members(self) -> list[Any]:
27
+ """The IR members this builder contributes, in order.
28
+
29
+ Decoration repeated per output file -- a banner, a preprocessor guard --
30
+ overrides this to contribute several members at once.
31
+ """
32
+ return [self.build()]
33
+
34
+
35
+ def _resolve(items: Sequence[object]) -> list[Any]:
36
+ """Turn a mixed list of IR nodes and builders into IR nodes, preserving order."""
37
+ out: list[Any] = []
38
+ for item in items:
39
+ if isinstance(item, _Builder):
40
+ out.extend(item.build_members())
41
+ else:
42
+ out.append(item)
43
+ return out
44
+
45
+
46
+ @dataclass
47
+ class _DocContext:
48
+ """State shared by every builder in one document."""
49
+
50
+ cpp_files: list[str | None] = field(default_factory=lambda: [None])
51
+
52
+ @property
53
+ def cpp_file(self) -> str | None:
54
+ """The source file definitions currently default to."""
55
+ return self.cpp_files[-1]
@@ -0,0 +1,97 @@
1
+ """Coercion and validation of the loose argument shapes the builders accept."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Sequence
6
+
7
+ from ..body import Code, stmts
8
+ from ..doc import Param, SVQualifier, as_type
9
+ from ..errors import ValidationError
10
+ from ..lines import Line
11
+
12
+
13
+ def _as_body_lines(body: Code) -> list[Line]:
14
+ """Coerce whatever the caller passed as a body into lines."""
15
+ return stmts(body)
16
+
17
+
18
+ def _as_params(
19
+ params: Iterable[Param | tuple[str, ...] | Sequence[str]],
20
+ ) -> list[Param]:
21
+ """Coerce a parameter spec list into :class:`Param` objects.
22
+
23
+ A tuple is read as ``(type, name)`` optionally followed by ``comment`` and
24
+ ``default``. An empty string in either position means absent.
25
+ """
26
+ out: list[Param] = []
27
+ for p in params:
28
+ if isinstance(p, Param):
29
+ out.append(p)
30
+ continue
31
+ parts = list(p)
32
+ if not 2 <= len(parts) <= 4:
33
+ raise ValidationError(
34
+ f"parameter {p!r} should be a Param, or a tuple of "
35
+ "(type, name[, comment[, default]])"
36
+ )
37
+ type_name, name, *rest = parts
38
+ comment = rest[0] or None if len(rest) > 0 else None
39
+ default = rest[1] or None if len(rest) > 1 else None
40
+ out.append(Param(as_type(type_name), name, comment, default))
41
+ return out
42
+
43
+
44
+ def _sv_qualifier(
45
+ *,
46
+ static: bool,
47
+ virtual: bool,
48
+ pure_virtual: bool,
49
+ override: bool,
50
+ final: bool,
51
+ ) -> SVQualifier:
52
+ """Collapse the mutually exclusive static/virtual flags into one qualifier."""
53
+ if pure_virtual:
54
+ # virtual is redundant alongside pure_virtual, so it is permitted.
55
+ conflicts = [
56
+ n
57
+ for n, v in (("static", static), ("override", override), ("final", final))
58
+ if v
59
+ ]
60
+ if conflicts:
61
+ raise ValidationError(
62
+ f"a pure virtual function cannot also be {' or '.join(conflicts)}"
63
+ )
64
+ return SVQualifier.PURE_VIRTUAL
65
+ chosen = [
66
+ n
67
+ for n, v in (
68
+ ("static", static),
69
+ ("virtual", virtual),
70
+ ("override", override),
71
+ ("final", final),
72
+ )
73
+ if v
74
+ ]
75
+ if len(chosen) > 1:
76
+ raise ValidationError(
77
+ f"a function cannot be {' and '.join(chosen)} at once; pick one"
78
+ )
79
+ if static:
80
+ return SVQualifier.STATIC
81
+ if virtual:
82
+ return SVQualifier.VIRTUAL
83
+ if override:
84
+ return SVQualifier.OVERRIDE
85
+ if final:
86
+ return SVQualifier.FINAL
87
+ return SVQualifier.NONE
88
+
89
+
90
+ def _extends(extends: str | Sequence[str] | None) -> str | None:
91
+ """Normalise a base-class specification into the text after the colon."""
92
+ if extends is None:
93
+ return None
94
+ if isinstance(extends, str):
95
+ return extends
96
+ joined = ", ".join(extends)
97
+ return joined or None
@@ -0,0 +1,201 @@
1
+ """Decoration wrapped around a run of members: banners, guards, access sections.
2
+
3
+ None of it emits code of its own, so where it lands depends on where the members it
4
+ decorates landed -- which is only known once the enclosing scope is built.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Sequence
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from ..comments import write_access_tag, write_banner_comment
13
+ from ..doc import (
14
+ Class,
15
+ Constructor,
16
+ Destructor,
17
+ Function,
18
+ Lines,
19
+ Output,
20
+ SVQualifier,
21
+ Variable,
22
+ )
23
+ from ..lines import Line, blank
24
+ from ..lines import lines as _lines
25
+ from ..writer import needs_definition, variable_defined_in_source
26
+ from .base import _Builder
27
+
28
+ if TYPE_CHECKING:
29
+ from .scopes import ClassBuilder
30
+
31
+
32
+ def _source_targets(members: Sequence[object], *, in_class: bool) -> list[str | None]:
33
+ """Which source files ``members`` contribute definitions to.
34
+
35
+ Entries are ``cpp_file`` base names, with ``None`` standing for the document's
36
+ default source file, in order of first appearance. An empty result means these
37
+ members are header-only.
38
+
39
+ Anything decorating a group of members -- a banner, a preprocessor guard -- needs
40
+ this to land in the same files as the members themselves.
41
+ """
42
+ found: list[str | None] = []
43
+
44
+ def note(target: str | None) -> None:
45
+ if target not in found:
46
+ found.append(target)
47
+
48
+ def visit(items: Sequence[object]) -> None:
49
+ for m in items:
50
+ if isinstance(m, _Builder):
51
+ visit(m.build_members())
52
+ elif isinstance(m, Lines):
53
+ if m.output is not Output.HPP and m.content:
54
+ note(m.cpp_file)
55
+ elif isinstance(m, (Function, Constructor, Destructor)):
56
+ pure = getattr(m, "sv", None) is SVQualifier.PURE_VIRTUAL
57
+ if needs_definition(m, pure_virtual=pure) and not m.defined_in_header:
58
+ note(m.cpp_file)
59
+ elif isinstance(m, Variable):
60
+ if variable_defined_in_source(m, in_class=in_class):
61
+ note(m.cpp_file)
62
+ elif isinstance(m, Class):
63
+ # A templated class defines everything in the header.
64
+ if m.template is None:
65
+ visit(m.members)
66
+
67
+ visit(members)
68
+ return found
69
+
70
+
71
+ def _per_file_lines(
72
+ content: list[Line], output: Output, targets: Sequence[str | None]
73
+ ) -> list[Lines]:
74
+ """Repeat ``content`` once for the header and once per source file in ``targets``."""
75
+ out: list[Lines] = []
76
+ if output is not Output.CPP:
77
+ out.append(Lines(content, Output.HPP))
78
+ if output is not Output.HPP:
79
+ out.extend(Lines(content, Output.CPP, target) for target in targets)
80
+ return out
81
+
82
+
83
+ class _SectionBanner(_Builder[Lines]):
84
+ """The banner comment heading an access section.
85
+
86
+ Where it goes is decided at build time, once the section's contents are known. A
87
+ header-only section -- nested types, member variables -- keeps its banner out of
88
+ the source files. Otherwise the banner follows the members: into the default
89
+ source file, or into the supplemental files when that is where the section's
90
+ definitions went.
91
+ """
92
+
93
+ def __init__(self, comment: str) -> None:
94
+ self.comment = comment
95
+ self.members: list[object] = []
96
+
97
+ def _targets(self) -> list[str | None]:
98
+ targets = _source_targets(self.members, in_class=True)
99
+ # One copy suffices for decoration, so relocate only when the default source
100
+ # file holds none of the section. A guard cannot do this; see _Guard.
101
+ if None in targets:
102
+ return [None]
103
+ return targets
104
+
105
+ def build(self) -> Lines:
106
+ return self._members()[0]
107
+
108
+ def _members(self) -> list[Lines]:
109
+ content = write_banner_comment(self.comment)
110
+ return _per_file_lines(content, Output.BOTH, self._targets())
111
+
112
+ def build_members(self) -> list[Any]:
113
+ return list(self._members())
114
+
115
+
116
+ class _Guard:
117
+ """A preprocessor guard bracketing a run of members.
118
+
119
+ Repeated into every source file receiving a guarded definition: code escaping the
120
+ guard would be compiled unconditionally.
121
+ """
122
+
123
+ def __init__(self, directive: str, output: Output, *, in_class: bool) -> None:
124
+ self.directive = directive
125
+ self.output = output
126
+ self.in_class = in_class
127
+ self.members: list[object] = []
128
+
129
+ def targets(self) -> list[str | None]:
130
+ return _source_targets(self.members, in_class=self.in_class)
131
+
132
+ def open_members(self) -> list[Lines]:
133
+ if not self.members:
134
+ return []
135
+ return _per_file_lines(
136
+ _lines(f"\n{self.directive}"), self.output, self.targets()
137
+ )
138
+
139
+ def close_members(self) -> list[Lines]:
140
+ if not self.members:
141
+ return []
142
+ content = [blank(), *_lines("#endif")]
143
+ return _per_file_lines(content, self.output, self.targets())
144
+
145
+
146
+ class _GuardOpen(_Builder[Lines]):
147
+ """Placeholder for a guard's opening directive, resolved at build time."""
148
+
149
+ def __init__(self, guard: _Guard) -> None:
150
+ self._guard = guard
151
+
152
+ def build(self) -> Lines:
153
+ members = self._guard.open_members()
154
+ return members[0] if members else Lines([], self._guard.output)
155
+
156
+ def build_members(self) -> list[Any]:
157
+ return list(self._guard.open_members())
158
+
159
+
160
+ class _GuardClose(_Builder[Lines]):
161
+ """Placeholder for a guard's ``#endif``, resolved at build time."""
162
+
163
+ def __init__(self, guard: _Guard) -> None:
164
+ self._guard = guard
165
+
166
+ def build(self) -> Lines:
167
+ members = self._guard.close_members()
168
+ return members[0] if members else Lines([], self._guard.output)
169
+
170
+ def build_members(self) -> list[Any]:
171
+ return list(self._guard.close_members())
172
+
173
+
174
+ class AccessSection:
175
+ """An access-specifier section of a class.
176
+
177
+ The ``public:`` label goes in immediately, so ``cls.public("Interface")`` works
178
+ on its own. As a ``with`` block, a section that ends up with no members takes
179
+ its label and banner back out again.
180
+ """
181
+
182
+ def __init__(self, scope: ClassBuilder, tag: str, comment: str | None) -> None:
183
+ self._scope = scope
184
+ self._count = 1
185
+ scope._add(Lines(write_access_tag(tag), Output.HPP))
186
+ self._banner: _SectionBanner | None = None
187
+ if comment is not None:
188
+ self._banner = scope._add(_SectionBanner(comment))
189
+ self._count += 1
190
+ self._start = len(scope._pending)
191
+
192
+ def __enter__(self) -> ClassBuilder:
193
+ return self._scope
194
+
195
+ def __exit__(self, *exc: object) -> None:
196
+ pending = self._scope._pending
197
+ if len(pending) == self._start:
198
+ del pending[self._start - self._count : self._start]
199
+ elif self._banner is not None:
200
+ self._banner.members = pending[self._start :]
201
+ return None