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