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