spacetime-dsl 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.
- spacetime/__init__.py +50 -0
- spacetime/__main__.py +10 -0
- spacetime/ast.py +243 -0
- spacetime/builder.py +300 -0
- spacetime/cli.py +103 -0
- spacetime/compiler.py +91 -0
- spacetime/errors.py +107 -0
- spacetime/grammar.lark +159 -0
- spacetime/parser.py +50 -0
- spacetime/render.py +124 -0
- spacetime/semantics.py +157 -0
- spacetime/svg.py +355 -0
- spacetime_dsl-0.1.0.dist-info/METADATA +86 -0
- spacetime_dsl-0.1.0.dist-info/RECORD +17 -0
- spacetime_dsl-0.1.0.dist-info/WHEEL +5 -0
- spacetime_dsl-0.1.0.dist-info/entry_points.txt +2 -0
- spacetime_dsl-0.1.0.dist-info/top_level.txt +1 -0
spacetime/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Space-Time DSL: compile a message history into a refined AST.
|
|
2
|
+
|
|
3
|
+
The public surface is re-exported here; see ``__all__``. Typical use is
|
|
4
|
+
:func:`compile_source` or :func:`compile_file`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .ast import (
|
|
8
|
+
Ast,
|
|
9
|
+
History,
|
|
10
|
+
HistoryRefined,
|
|
11
|
+
Ident,
|
|
12
|
+
MessageDeclaration,
|
|
13
|
+
RecvEvent,
|
|
14
|
+
RecvEventRefined,
|
|
15
|
+
RefinedAst,
|
|
16
|
+
SendEvent,
|
|
17
|
+
SendEventRefined,
|
|
18
|
+
SourceLocation,
|
|
19
|
+
)
|
|
20
|
+
from .builder import AstBuilder, build_ast
|
|
21
|
+
from .compiler import StageObserver, compile_file, compile_source
|
|
22
|
+
from .errors import DslError, ParseError, SemanticError
|
|
23
|
+
from .parser import parse
|
|
24
|
+
from .semantics import create_refined_ast
|
|
25
|
+
from .svg import render_svg
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Ast",
|
|
29
|
+
"AstBuilder",
|
|
30
|
+
"DslError",
|
|
31
|
+
"History",
|
|
32
|
+
"HistoryRefined",
|
|
33
|
+
"Ident",
|
|
34
|
+
"MessageDeclaration",
|
|
35
|
+
"ParseError",
|
|
36
|
+
"RecvEvent",
|
|
37
|
+
"RecvEventRefined",
|
|
38
|
+
"RefinedAst",
|
|
39
|
+
"SemanticError",
|
|
40
|
+
"SendEvent",
|
|
41
|
+
"SendEventRefined",
|
|
42
|
+
"SourceLocation",
|
|
43
|
+
"StageObserver",
|
|
44
|
+
"build_ast",
|
|
45
|
+
"compile_file",
|
|
46
|
+
"compile_source",
|
|
47
|
+
"create_refined_ast",
|
|
48
|
+
"parse",
|
|
49
|
+
"render_svg",
|
|
50
|
+
]
|
spacetime/__main__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Entry point for ``python -m spacetime <file>``.
|
|
2
|
+
|
|
3
|
+
Mirrors the ``spacetime`` console script declared in pyproject.toml; both
|
|
4
|
+
delegate to :func:`spacetime.cli.main`, whose return value is the exit code.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .cli import main
|
|
8
|
+
|
|
9
|
+
if __name__ == "__main__":
|
|
10
|
+
raise SystemExit(main())
|
spacetime/ast.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Syntax tree data structures for the Space-Time DSL.
|
|
2
|
+
|
|
3
|
+
The compiler works with two tree shapes:
|
|
4
|
+
|
|
5
|
+
``Ast``
|
|
6
|
+
Raw output of :mod:`spacetime.builder`. Identifiers keep the
|
|
7
|
+
:class:`SourceLocation` needed to report errors, and message payloads
|
|
8
|
+
stay attached to their declarations.
|
|
9
|
+
|
|
10
|
+
``RefinedAst``
|
|
11
|
+
Output of :mod:`spacetime.semantics`. Names are resolved to plain
|
|
12
|
+
strings, event names and message declarations are inlined into the
|
|
13
|
+
events that use them, and every event has a temporal position.
|
|
14
|
+
|
|
15
|
+
All nodes are frozen dataclasses: building a tree never mutates an existing
|
|
16
|
+
node, and callers may treat nodes as immutable values. Lists are still
|
|
17
|
+
mutable by Python semantics; treat them as read-only after construction.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class SourceLocation:
|
|
25
|
+
"""A 1-based position in the DSL source.
|
|
26
|
+
|
|
27
|
+
Invariants:
|
|
28
|
+
``line >= 1`` and ``column >= 1``. The builder falls back to
|
|
29
|
+
``SourceLocation(1, 1)`` when Lark did not attach position metadata
|
|
30
|
+
to a rule, so a location may be approximate but is never absent.
|
|
31
|
+
|
|
32
|
+
Failure modes:
|
|
33
|
+
None: this is a passive value and performs no validation.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
line: int
|
|
37
|
+
column: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class Ident:
|
|
42
|
+
"""A source identifier together with the position where it appeared.
|
|
43
|
+
|
|
44
|
+
Used for process names, message names and values, event names, and
|
|
45
|
+
references between them.
|
|
46
|
+
|
|
47
|
+
Invariants:
|
|
48
|
+
``name`` matches the grammar's ``IDENTIFIER`` token: it starts with
|
|
49
|
+
a lowercase ASCII letter and continues with letters, digits, or
|
|
50
|
+
underscores.
|
|
51
|
+
|
|
52
|
+
Failure modes:
|
|
53
|
+
None. Whether the name is declared is checked in semantics.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
name: str
|
|
57
|
+
location: SourceLocation
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class MessageDeclaration:
|
|
62
|
+
"""A ``name := "value"`` entry in a ``Messages`` block.
|
|
63
|
+
|
|
64
|
+
Invariants:
|
|
65
|
+
``value`` is the payload text with the surrounding double quotes
|
|
66
|
+
removed. No escape processing is applied, so it may contain any
|
|
67
|
+
character the grammar allows between the quotes (no unescaped
|
|
68
|
+
double quote and no line terminator).
|
|
69
|
+
|
|
70
|
+
Failure modes:
|
|
71
|
+
Duplicate names are rejected while building the block, with a
|
|
72
|
+
:class:`spacetime.errors.SemanticError` at the second declaration.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
name: Ident
|
|
76
|
+
value: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class SendEvent:
|
|
81
|
+
"""A named send event from a ``History`` block.
|
|
82
|
+
|
|
83
|
+
``name`` is the event identifier bound with ``:=`` (for example
|
|
84
|
+
``m1``), not the message name. ``processes_to_ids`` may list several
|
|
85
|
+
recipients, which models a multicast as a single event.
|
|
86
|
+
|
|
87
|
+
Invariants:
|
|
88
|
+
- ``name`` is unique among send events; duplicates are rejected in
|
|
89
|
+
semantics.
|
|
90
|
+
- ``processes_to_ids`` is non-empty because the grammar requires at
|
|
91
|
+
least one recipient.
|
|
92
|
+
- Referenced message and process names may still be undeclared at
|
|
93
|
+
this stage.
|
|
94
|
+
|
|
95
|
+
Failure modes:
|
|
96
|
+
No validation here. :func:`spacetime.semantics.create_refined_ast`
|
|
97
|
+
raises :class:`spacetime.errors.SemanticError` for undeclared
|
|
98
|
+
messages or processes and for duplicate event names.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
name: Ident
|
|
102
|
+
message_id: Ident
|
|
103
|
+
process_from_id: Ident
|
|
104
|
+
processes_to_ids: list[Ident]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class RecvEvent:
|
|
109
|
+
"""A ``Recv(event) :: p, q`` entry in a ``History`` block.
|
|
110
|
+
|
|
111
|
+
A multicast receive stays one event; every listed process is recorded
|
|
112
|
+
in ``process_ids``.
|
|
113
|
+
|
|
114
|
+
Invariants:
|
|
115
|
+
- ``send_event_ref`` names a send event that appears *earlier* in
|
|
116
|
+
the same history; forward references are rejected.
|
|
117
|
+
- ``process_ids`` is non-empty and every entry must be a recipient
|
|
118
|
+
of the referenced send event.
|
|
119
|
+
|
|
120
|
+
Failure modes:
|
|
121
|
+
Declaration order and recipient membership are validated in
|
|
122
|
+
:func:`spacetime.semantics.create_refined_ast`.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
send_event_ref: Ident
|
|
126
|
+
process_ids: list[Ident]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True)
|
|
130
|
+
class History:
|
|
131
|
+
"""The ordered event list of a ``History`` block.
|
|
132
|
+
|
|
133
|
+
Invariants:
|
|
134
|
+
- Order is source order, the only notion of time in the DSL; there
|
|
135
|
+
is no explicit timestamp.
|
|
136
|
+
- An empty list is valid and represents a program without events.
|
|
137
|
+
|
|
138
|
+
Failure modes:
|
|
139
|
+
None at this layer; cross-event rules live in semantics.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
events: list[SendEvent | RecvEvent]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True)
|
|
146
|
+
class Ast:
|
|
147
|
+
"""Raw syntax tree produced by :func:`spacetime.builder.build_ast`.
|
|
148
|
+
|
|
149
|
+
Invariants:
|
|
150
|
+
- ``process_declarations`` preserves source order and contains no
|
|
151
|
+
duplicate names.
|
|
152
|
+
- ``message_declarations`` preserves source order and contains no
|
|
153
|
+
duplicate names.
|
|
154
|
+
- Absent blocks become empty lists, so an empty program is a valid
|
|
155
|
+
``Ast`` with no declarations and no history.
|
|
156
|
+
|
|
157
|
+
Failure modes:
|
|
158
|
+
Constructing an ``Ast`` directly performs no checks; violations are
|
|
159
|
+
only detected when semantics runs.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
process_declarations: list[Ident]
|
|
163
|
+
message_declarations: list[MessageDeclaration]
|
|
164
|
+
history: History
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(frozen=True)
|
|
168
|
+
class SendEventRefined:
|
|
169
|
+
"""A send event after name resolution.
|
|
170
|
+
|
|
171
|
+
``message`` is the payload string from the matching declaration and
|
|
172
|
+
``message_name`` is the declared message name itself (kept for the SVG
|
|
173
|
+
caption legend, which decodes arrow colors by message name). The
|
|
174
|
+
event's own name is intentionally dropped.
|
|
175
|
+
|
|
176
|
+
Invariants:
|
|
177
|
+
- ``temporal_pos`` is the 0-based index of the event in
|
|
178
|
+
``HistoryRefined.events``.
|
|
179
|
+
- ``message_name`` names a declaration in the ``Messages`` block.
|
|
180
|
+
- ``message_type`` is the 0-based declaration index of the message
|
|
181
|
+
in the ``Messages`` block.
|
|
182
|
+
- ``process_from_id`` and every entry in ``processes_to_ids`` name
|
|
183
|
+
declared processes.
|
|
184
|
+
- ``processes_to_ids`` is non-empty and may repeat a process if the
|
|
185
|
+
source did.
|
|
186
|
+
|
|
187
|
+
Failure modes:
|
|
188
|
+
None; the instance only exists after semantic validation succeeded.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
temporal_pos: int
|
|
192
|
+
message: str
|
|
193
|
+
message_name: str
|
|
194
|
+
message_type: int
|
|
195
|
+
process_from_id: str
|
|
196
|
+
processes_to_ids: list[str]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@dataclass(frozen=True)
|
|
200
|
+
class RecvEventRefined:
|
|
201
|
+
"""A receive event after name resolution.
|
|
202
|
+
|
|
203
|
+
Invariants:
|
|
204
|
+
- ``related_send_event`` is the same ``SendEventRefined`` instance
|
|
205
|
+
stored earlier in the refined history.
|
|
206
|
+
- ``temporal_pos`` is greater than the related send's position.
|
|
207
|
+
- ``process_ids`` is a non-empty subset of
|
|
208
|
+
``related_send_event.processes_to_ids``.
|
|
209
|
+
|
|
210
|
+
Failure modes:
|
|
211
|
+
None; the instance only exists after semantic validation succeeded.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
temporal_pos: int
|
|
215
|
+
related_send_event: SendEventRefined
|
|
216
|
+
process_ids: list[str]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass(frozen=True)
|
|
220
|
+
class HistoryRefined:
|
|
221
|
+
"""A resolved history whose events are ordered by ``temporal_pos``."""
|
|
222
|
+
|
|
223
|
+
events: list[SendEventRefined | RecvEventRefined]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass(frozen=True)
|
|
227
|
+
class RefinedAst:
|
|
228
|
+
"""Fully resolved program returned by
|
|
229
|
+
:func:`spacetime.compiler.compile_source`.
|
|
230
|
+
|
|
231
|
+
Message declarations are not kept: payloads are inlined in
|
|
232
|
+
``SendEventRefined.message``.
|
|
233
|
+
|
|
234
|
+
Invariants:
|
|
235
|
+
- ``process_declarations`` preserves source order as plain names.
|
|
236
|
+
- ``history.events`` is ordered by ``temporal_pos``.
|
|
237
|
+
|
|
238
|
+
Failure modes:
|
|
239
|
+
None; constructing one directly is not validated.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
process_declarations: list[str]
|
|
243
|
+
history: HistoryRefined
|
spacetime/builder.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Lark parse tree to ``Ast`` translation (stage 2 of the pipeline).
|
|
2
|
+
|
|
3
|
+
:class:`AstBuilder` is a Lark ``Interpreter`` whose method names match the
|
|
4
|
+
rules in ``grammar.lark``. It drops purely syntactic nodes, attaches source
|
|
5
|
+
locations to identifiers, and enforces structural rules that the grammar
|
|
6
|
+
cannot express: no repeated block and no duplicate declaration inside a
|
|
7
|
+
block.
|
|
8
|
+
|
|
9
|
+
Failure modes:
|
|
10
|
+
Structural violations raise :class:`spacetime.errors.SemanticError` with
|
|
11
|
+
the location of the offending block or declaration. An unexpected tree
|
|
12
|
+
shape raises :class:`TypeError`, signalling that the grammar and this
|
|
13
|
+
builder have drifted apart.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
from lark import Token, Tree
|
|
19
|
+
from lark.visitors import Interpreter
|
|
20
|
+
|
|
21
|
+
from .ast import (
|
|
22
|
+
Ast,
|
|
23
|
+
History,
|
|
24
|
+
Ident,
|
|
25
|
+
MessageDeclaration,
|
|
26
|
+
RecvEvent,
|
|
27
|
+
SendEvent,
|
|
28
|
+
SourceLocation,
|
|
29
|
+
)
|
|
30
|
+
from .errors import SemanticError
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class ProcessesBlock:
|
|
35
|
+
"""Intermediate result for one ``Processes`` block.
|
|
36
|
+
|
|
37
|
+
``location`` is the block's own position and is used only to report a
|
|
38
|
+
repeated block. ``declarations`` preserves source order.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
declarations: list[Ident]
|
|
42
|
+
location: SourceLocation
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class MessagesBlock:
|
|
47
|
+
"""Intermediate result for one ``Messages`` block.
|
|
48
|
+
|
|
49
|
+
``location`` is the block's own position and is used only to report a
|
|
50
|
+
repeated block. ``declarations`` preserves source order.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
declarations: list[MessageDeclaration]
|
|
54
|
+
location: SourceLocation
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class HistoryBlock:
|
|
59
|
+
"""Intermediate result for one ``History`` block.
|
|
60
|
+
|
|
61
|
+
``location`` is the block's own position and is used only to report a
|
|
62
|
+
repeated block. ``events`` preserves source order.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
events: list[SendEvent | RecvEvent]
|
|
66
|
+
location: SourceLocation
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Any block produced by ``AstBuilder`` before the blocks are merged into a
|
|
70
|
+
# single ``Ast``.
|
|
71
|
+
Block = ProcessesBlock | MessagesBlock | HistoryBlock
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _location_from_tree(tree: Tree) -> SourceLocation:
|
|
75
|
+
"""Return the best available source position for ``tree``.
|
|
76
|
+
|
|
77
|
+
Lark attaches ``line``/``column`` to rule nodes because the parser runs
|
|
78
|
+
with ``propagate_positions=True``. Some rules expose positions only on
|
|
79
|
+
their children, so this falls back to the first positioned child and
|
|
80
|
+
finally to ``SourceLocation(1, 1)``. The fallback keeps error reporting
|
|
81
|
+
total instead of raising while another diagnostic is being produced.
|
|
82
|
+
"""
|
|
83
|
+
line = getattr(tree.meta, "line", None)
|
|
84
|
+
column = getattr(tree.meta, "column", None)
|
|
85
|
+
if line is not None and column is not None:
|
|
86
|
+
return SourceLocation(line, column)
|
|
87
|
+
|
|
88
|
+
for child in tree.children:
|
|
89
|
+
if isinstance(child, Tree) and child.children:
|
|
90
|
+
child = child.children[0]
|
|
91
|
+
|
|
92
|
+
line = getattr(child, "line", None)
|
|
93
|
+
column = getattr(child, "column", None)
|
|
94
|
+
if line is not None and column is not None:
|
|
95
|
+
return SourceLocation(line, column)
|
|
96
|
+
|
|
97
|
+
return SourceLocation(1, 1)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _ident_from_token(token: Token) -> Ident:
|
|
101
|
+
"""Wrap a Lark identifier token as an ``Ident`` with its location.
|
|
102
|
+
|
|
103
|
+
Invariants:
|
|
104
|
+
``token`` comes from one of the grammar's identifier rules, so it
|
|
105
|
+
has ``value``, ``line`` and ``column``.
|
|
106
|
+
"""
|
|
107
|
+
return Ident(
|
|
108
|
+
name=token.value,
|
|
109
|
+
location=SourceLocation(token.line, token.column),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class AstBuilder(Interpreter):
|
|
114
|
+
"""Translate a parse tree into an :class:`Ast`.
|
|
115
|
+
|
|
116
|
+
Methods are named after grammar rules and return the node for that
|
|
117
|
+
rule. The visitor holds no state: create one instance per tree.
|
|
118
|
+
|
|
119
|
+
Failure modes:
|
|
120
|
+
See the module docstring: structural errors raise
|
|
121
|
+
:class:`SemanticError` and unexpected shapes raise ``TypeError``.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def start(self, tree: Tree) -> Ast:
|
|
125
|
+
"""Merge the optional blocks into a single ``Ast``.
|
|
126
|
+
|
|
127
|
+
Blocks may appear in any order. Missing blocks default to empty
|
|
128
|
+
declarations and an empty history, so the empty source is valid.
|
|
129
|
+
|
|
130
|
+
Failure modes:
|
|
131
|
+
:class:`SemanticError` at the second occurrence of a block
|
|
132
|
+
kind; ``TypeError`` for an unknown block type.
|
|
133
|
+
"""
|
|
134
|
+
blocks: list[Block] = [self.visit(child) for child in tree.children]
|
|
135
|
+
|
|
136
|
+
process_declarations: list[Ident] = []
|
|
137
|
+
message_declarations: list[MessageDeclaration] = []
|
|
138
|
+
history = History(events=[])
|
|
139
|
+
seen_blocks: set[str] = set()
|
|
140
|
+
|
|
141
|
+
for block in blocks:
|
|
142
|
+
match block:
|
|
143
|
+
case ProcessesBlock(declarations=declarations, location=location):
|
|
144
|
+
if "processes" in seen_blocks:
|
|
145
|
+
raise SemanticError(
|
|
146
|
+
"duplicate Processes block", location=location
|
|
147
|
+
)
|
|
148
|
+
seen_blocks.add("processes")
|
|
149
|
+
process_declarations = declarations
|
|
150
|
+
case MessagesBlock(declarations=declarations, location=location):
|
|
151
|
+
if "messages" in seen_blocks:
|
|
152
|
+
raise SemanticError(
|
|
153
|
+
"duplicate Messages block", location=location
|
|
154
|
+
)
|
|
155
|
+
seen_blocks.add("messages")
|
|
156
|
+
message_declarations = declarations
|
|
157
|
+
case HistoryBlock(events=events, location=location):
|
|
158
|
+
if "history" in seen_blocks:
|
|
159
|
+
raise SemanticError(
|
|
160
|
+
"duplicate History block", location=location
|
|
161
|
+
)
|
|
162
|
+
seen_blocks.add("history")
|
|
163
|
+
history = History(events=events)
|
|
164
|
+
case _:
|
|
165
|
+
raise TypeError(f"unexpected block: {block!r}")
|
|
166
|
+
|
|
167
|
+
return Ast(
|
|
168
|
+
process_declarations=process_declarations,
|
|
169
|
+
message_declarations=message_declarations,
|
|
170
|
+
history=history,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def processes_block(self, tree: Tree) -> ProcessesBlock:
|
|
174
|
+
"""Collect process declarations, rejecting duplicate names.
|
|
175
|
+
|
|
176
|
+
Invariants:
|
|
177
|
+
The returned declarations are in source order and have unique
|
|
178
|
+
names.
|
|
179
|
+
|
|
180
|
+
Failure modes:
|
|
181
|
+
:class:`SemanticError` at the second occurrence of a name.
|
|
182
|
+
"""
|
|
183
|
+
declarations: list[Ident] = []
|
|
184
|
+
seen: set[str] = set()
|
|
185
|
+
|
|
186
|
+
for child in tree.children:
|
|
187
|
+
declaration = self.visit(child)
|
|
188
|
+
if declaration.name in seen:
|
|
189
|
+
raise SemanticError(
|
|
190
|
+
f"duplicate process declaration: '{declaration.name}'",
|
|
191
|
+
location=declaration.location,
|
|
192
|
+
)
|
|
193
|
+
seen.add(declaration.name)
|
|
194
|
+
declarations.append(declaration)
|
|
195
|
+
|
|
196
|
+
return ProcessesBlock(declarations, _location_from_tree(tree))
|
|
197
|
+
|
|
198
|
+
def messages_block(self, tree: Tree) -> MessagesBlock:
|
|
199
|
+
"""Collect message declarations, rejecting duplicate names.
|
|
200
|
+
|
|
201
|
+
Invariants:
|
|
202
|
+
The returned declarations are in source order and have unique
|
|
203
|
+
names.
|
|
204
|
+
|
|
205
|
+
Failure modes:
|
|
206
|
+
:class:`SemanticError` at the second occurrence of a name.
|
|
207
|
+
"""
|
|
208
|
+
declarations: list[MessageDeclaration] = []
|
|
209
|
+
seen: set[str] = set()
|
|
210
|
+
|
|
211
|
+
for child in tree.children:
|
|
212
|
+
declaration = self.visit(child)
|
|
213
|
+
name = declaration.name.name
|
|
214
|
+
if name in seen:
|
|
215
|
+
raise SemanticError(
|
|
216
|
+
f"duplicate message declaration: '{name}'",
|
|
217
|
+
location=declaration.name.location,
|
|
218
|
+
)
|
|
219
|
+
seen.add(name)
|
|
220
|
+
declarations.append(declaration)
|
|
221
|
+
|
|
222
|
+
return MessagesBlock(declarations, _location_from_tree(tree))
|
|
223
|
+
|
|
224
|
+
def history_block(self, tree: Tree) -> HistoryBlock:
|
|
225
|
+
"""Collect history events in source order.
|
|
226
|
+
|
|
227
|
+
No cross-event validation happens here: receives may still refer to
|
|
228
|
+
unknown or later events until semantics runs.
|
|
229
|
+
"""
|
|
230
|
+
events = [self.visit(child) for child in tree.children]
|
|
231
|
+
return HistoryBlock(events, _location_from_tree(tree))
|
|
232
|
+
|
|
233
|
+
def process_declaration(self, tree: Tree) -> Ident:
|
|
234
|
+
"""Return a declared process name with its location."""
|
|
235
|
+
return _ident_from_token(tree.children[0])
|
|
236
|
+
|
|
237
|
+
def message_decl(self, tree: Tree) -> MessageDeclaration:
|
|
238
|
+
"""Build a ``name := "value"`` declaration from its two children."""
|
|
239
|
+
name_node, value_node = tree.children
|
|
240
|
+
name = self.visit(name_node)
|
|
241
|
+
value = self.visit(value_node)
|
|
242
|
+
return MessageDeclaration(name=name, value=value)
|
|
243
|
+
|
|
244
|
+
def message_name(self, tree: Tree) -> Ident:
|
|
245
|
+
"""Return the name identifier on the left of a message declaration."""
|
|
246
|
+
return _ident_from_token(tree.children[0])
|
|
247
|
+
|
|
248
|
+
def message_value(self, tree: Tree) -> str:
|
|
249
|
+
"""Return the payload with the outer double quotes stripped.
|
|
250
|
+
|
|
251
|
+
No escape processing is performed; see
|
|
252
|
+
:class:`spacetime.ast.MessageDeclaration`.
|
|
253
|
+
"""
|
|
254
|
+
return tree.children[0].value[1:-1]
|
|
255
|
+
|
|
256
|
+
def message_identifier(self, tree: Tree) -> Ident:
|
|
257
|
+
"""Return the message reference inside ``Send(...)``."""
|
|
258
|
+
return _ident_from_token(tree.children[0])
|
|
259
|
+
|
|
260
|
+
def process_identifier(self, tree: Tree) -> Ident:
|
|
261
|
+
"""Return a process used as sender, recipient, or receive target."""
|
|
262
|
+
return _ident_from_token(tree.children[0])
|
|
263
|
+
|
|
264
|
+
def event_identifier(self, tree: Tree) -> Ident:
|
|
265
|
+
"""Return an event name bound by a send or referenced by ``Recv``."""
|
|
266
|
+
return _ident_from_token(tree.children[0])
|
|
267
|
+
|
|
268
|
+
def process_list(self, tree: Tree) -> list[Ident]:
|
|
269
|
+
"""Return the comma-separated process identifiers in source order."""
|
|
270
|
+
return [self.visit(child) for child in tree.children]
|
|
271
|
+
|
|
272
|
+
def event(self, tree: Tree) -> SendEvent | RecvEvent:
|
|
273
|
+
"""Unwrap a history event into its send or receive form."""
|
|
274
|
+
return self.visit(tree.children[0])
|
|
275
|
+
|
|
276
|
+
def send_event(self, tree: Tree) -> SendEvent:
|
|
277
|
+
"""Build a send event from name, message, sender, and recipients."""
|
|
278
|
+
name, message_id, process_from_id, processes_to_ids = self.visit_children(
|
|
279
|
+
tree
|
|
280
|
+
)
|
|
281
|
+
return SendEvent(
|
|
282
|
+
name=name,
|
|
283
|
+
message_id=message_id,
|
|
284
|
+
process_from_id=process_from_id,
|
|
285
|
+
processes_to_ids=processes_to_ids,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def recv_event(self, tree: Tree) -> RecvEvent:
|
|
289
|
+
"""Build a receive event from the referenced send name and targets."""
|
|
290
|
+
send_event_ref, process_ids = self.visit_children(tree)
|
|
291
|
+
return RecvEvent(send_event_ref=send_event_ref, process_ids=process_ids)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def build_ast(tree: Tree) -> Ast:
|
|
295
|
+
"""Translate ``tree`` (the ``start`` rule) into an ``Ast``.
|
|
296
|
+
|
|
297
|
+
Failure modes:
|
|
298
|
+
Same as :class:`AstBuilder`.
|
|
299
|
+
"""
|
|
300
|
+
return AstBuilder().visit(tree)
|
spacetime/cli.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Command-line entry point: compile one DSL file and print its result."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from .compiler import compile_file
|
|
7
|
+
from .errors import DslError
|
|
8
|
+
from .render import format_ast, format_parse_tree, format_refined_ast
|
|
9
|
+
from .svg import render_svg
|
|
10
|
+
|
|
11
|
+
_STAGE_TITLES = {
|
|
12
|
+
"parse": "parse tree",
|
|
13
|
+
"ast": "ast",
|
|
14
|
+
"refined": "refined ast",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_EPILOG = """\
|
|
18
|
+
the language: three blocks, in any order
|
|
19
|
+
|
|
20
|
+
Processes { p q } declare process names, one per line
|
|
21
|
+
Messages { m := "payload" } declare a message name and its payload text
|
|
22
|
+
History { ... } the events, in the order they happen
|
|
23
|
+
|
|
24
|
+
History events:
|
|
25
|
+
|
|
26
|
+
m1 := Send(msg) :: p -> q, r process p sends message `msg` to q and r
|
|
27
|
+
Recv(m1) :: q process q receives the message sent as m1
|
|
28
|
+
|
|
29
|
+
super simple example (save as hello.st, then run `spacetime hello.st`):
|
|
30
|
+
|
|
31
|
+
Processes {
|
|
32
|
+
p
|
|
33
|
+
q
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Messages {
|
|
37
|
+
hello := "Hello"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
History {
|
|
41
|
+
m1 := Send(hello) :: p -> q
|
|
42
|
+
Recv(m1) :: q
|
|
43
|
+
}
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _print_stage(stage: str, output: object) -> None:
|
|
48
|
+
"""Print one compiler stage with a header identifying the stage."""
|
|
49
|
+
renderers = {
|
|
50
|
+
"parse": format_parse_tree,
|
|
51
|
+
"ast": format_ast,
|
|
52
|
+
"refined": format_refined_ast,
|
|
53
|
+
}
|
|
54
|
+
print(f"== {_STAGE_TITLES[stage]} ==")
|
|
55
|
+
print(renderers[stage](output))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main(argv: list[str] | None = None) -> int:
|
|
59
|
+
"""Compile the file named on the command line and print its SVG diagram.
|
|
60
|
+
|
|
61
|
+
By default the SVG space-time diagram of the program is printed to
|
|
62
|
+
stdout. With ``--print-stages``, only the frontend runs: the parse tree
|
|
63
|
+
and both intermediate ASTs are printed as the pipeline produces them,
|
|
64
|
+
and no SVG is rendered.
|
|
65
|
+
|
|
66
|
+
Returns a process exit code: ``0`` on success, ``1`` when the file
|
|
67
|
+
cannot be read or the program contains a DSL error. Diagnostics go to
|
|
68
|
+
stderr; rendered output goes to stdout.
|
|
69
|
+
"""
|
|
70
|
+
parser = argparse.ArgumentParser(
|
|
71
|
+
prog="space-time-dsl",
|
|
72
|
+
description="Compile a space-time diagram DSL file and print its SVG diagram.",
|
|
73
|
+
epilog=_EPILOG,
|
|
74
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument("path", help="DSL source file")
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--print-stages",
|
|
79
|
+
action="store_true",
|
|
80
|
+
help="run only the frontend: print the parse tree and intermediate ASTs after every compiler stage",
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--to-svg",
|
|
84
|
+
action="store_true",
|
|
85
|
+
help="print the SVG space-time diagram (this is the default)",
|
|
86
|
+
)
|
|
87
|
+
args = parser.parse_args(argv)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
program = compile_file(
|
|
91
|
+
args.path,
|
|
92
|
+
observe=_print_stage if args.print_stages else None,
|
|
93
|
+
)
|
|
94
|
+
except (DslError, OSError) as exc:
|
|
95
|
+
print(exc, file=sys.stderr)
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
if not args.print_stages:
|
|
99
|
+
# write, not print: render_svg already ends with a newline, and the
|
|
100
|
+
# golden file docs/sample_v2.svg must diff byte-exact against this
|
|
101
|
+
# output.
|
|
102
|
+
sys.stdout.write(render_svg(program))
|
|
103
|
+
return 0
|