openprocess 0.7.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.
Files changed (173) hide show
  1. cpnpy/__init__.py +46 -0
  2. openprocess/__init__.py +57 -0
  3. openprocess/analysis/__init__.py +0 -0
  4. openprocess/analysis/state_space.py +521 -0
  5. openprocess/analysis/state_space_process.py +251 -0
  6. openprocess/cli.py +742 -0
  7. openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +31 -0
  8. openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +38 -0
  9. openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +27 -0
  10. openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +29 -0
  11. openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +65 -0
  12. openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +1 -0
  13. openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +67 -0
  14. openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +75 -0
  15. openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +5 -0
  16. openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +27 -0
  17. openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +65 -0
  18. openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +1 -0
  19. openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +62 -0
  20. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +1 -0
  21. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +36 -0
  22. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +28 -0
  23. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +30 -0
  24. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +36 -0
  25. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +69 -0
  26. openprocess/exercises/pack.md +14 -0
  27. openprocess/flow/__init__.py +50 -0
  28. openprocess/flow/box.py +466 -0
  29. openprocess/flow/boxes/__init__.py +7 -0
  30. openprocess/flow/boxes/check.py +119 -0
  31. openprocess/flow/boxes/compare.py +16 -0
  32. openprocess/flow/boxes/cpn.py +53 -0
  33. openprocess/flow/boxes/discover.py +124 -0
  34. openprocess/flow/boxes/filter.py +80 -0
  35. openprocess/flow/boxes/input.py +124 -0
  36. openprocess/flow/boxes/output.py +52 -0
  37. openprocess/flow/boxes/predict.py +186 -0
  38. openprocess/flow/boxes/science.py +159 -0
  39. openprocess/flow/boxes/sweeps.py +18 -0
  40. openprocess/flow/convert.py +187 -0
  41. openprocess/flow/datasets.py +198 -0
  42. openprocess/flow/explain.py +115 -0
  43. openprocess/flow/library.py +222 -0
  44. openprocess/flow/record.py +385 -0
  45. openprocess/flow/runner.py +357 -0
  46. openprocess/flow/sweep.py +92 -0
  47. openprocess/flow/types.py +290 -0
  48. openprocess/flow/workflow.py +628 -0
  49. openprocess/gui/__init__.py +0 -0
  50. openprocess/gui/app.py +90 -0
  51. openprocess/gui/arc_editing.py +295 -0
  52. openprocess/gui/canvas.py +1414 -0
  53. openprocess/gui/flow/__init__.py +8 -0
  54. openprocess/gui/flow/canvas.py +854 -0
  55. openprocess/gui/flow/page.py +972 -0
  56. openprocess/gui/flow/templates.py +131 -0
  57. openprocess/gui/flow/viewers.py +665 -0
  58. openprocess/gui/items.py +1275 -0
  59. openprocess/gui/learn/answer_boxes.py +978 -0
  60. openprocess/gui/learn/concealment.py +91 -0
  61. openprocess/gui/learn/mode.py +1181 -0
  62. openprocess/gui/panning.py +241 -0
  63. openprocess/gui/resources/openprocess-icon.png +0 -0
  64. openprocess/gui/studio/__init__.py +1 -0
  65. openprocess/gui/studio/__main__.py +3 -0
  66. openprocess/gui/studio/app.py +4031 -0
  67. openprocess/gui/studio/charts.py +115 -0
  68. openprocess/gui/studio/compare_page.py +487 -0
  69. openprocess/gui/studio/cpn_page.py +1858 -0
  70. openprocess/gui/studio/definition_view.py +284 -0
  71. openprocess/gui/studio/derivation_view.py +421 -0
  72. openprocess/gui/studio/documents.py +152 -0
  73. openprocess/gui/studio/dotted_chart.py +1401 -0
  74. openprocess/gui/studio/file_dialogs.py +143 -0
  75. openprocess/gui/studio/filter_dialog.py +247 -0
  76. openprocess/gui/studio/graph_builders.py +176 -0
  77. openprocess/gui/studio/graph_view.py +682 -0
  78. openprocess/gui/studio/instances.py +413 -0
  79. openprocess/gui/studio/log_editor.py +675 -0
  80. openprocess/gui/studio/log_page.py +800 -0
  81. openprocess/gui/studio/markdown_view.py +127 -0
  82. openprocess/gui/studio/mathtext.py +260 -0
  83. openprocess/gui/studio/ml_highlighter.py +75 -0
  84. openprocess/gui/studio/model_page.py +760 -0
  85. openprocess/gui/studio/net_comparison.py +124 -0
  86. openprocess/gui/studio/notes_overlay.py +275 -0
  87. openprocess/gui/studio/petri_page.py +844 -0
  88. openprocess/gui/studio/regions_view.py +502 -0
  89. openprocess/gui/studio/sidebar.py +149 -0
  90. openprocess/gui/studio/style.py +503 -0
  91. openprocess/gui/studio/tool_icons.py +134 -0
  92. openprocess/gui/studio/updates.py +439 -0
  93. openprocess/gui/studio/widgets.py +899 -0
  94. openprocess/gui/studio/workers.py +60 -0
  95. openprocess/gui/studio/workspace.py +447 -0
  96. openprocess/gui/theme.py +394 -0
  97. openprocess/gui/tidy.py +86 -0
  98. openprocess/io/__init__.py +0 -0
  99. openprocess/io/cpn_reader.py +389 -0
  100. openprocess/io/cpn_writer.py +357 -0
  101. openprocess/learn/__init__.py +23 -0
  102. openprocess/learn/answers.py +188 -0
  103. openprocess/learn/checks.py +953 -0
  104. openprocess/learn/computed.py +1180 -0
  105. openprocess/learn/context.py +145 -0
  106. openprocess/learn/exam.py +169 -0
  107. openprocess/learn/exercise-packs.md +325 -0
  108. openprocess/learn/importer.py +216 -0
  109. openprocess/learn/notation.py +474 -0
  110. openprocess/learn/pack.py +511 -0
  111. openprocess/learn/sheet.py +296 -0
  112. openprocess/mining/__init__.py +73 -0
  113. openprocess/mining/analysis.py +689 -0
  114. openprocess/mining/columns.py +282 -0
  115. openprocess/mining/compare_nets.py +246 -0
  116. openprocess/mining/conformance/__init__.py +0 -0
  117. openprocess/mining/conformance/alignments.py +263 -0
  118. openprocess/mining/conformance/quality.py +145 -0
  119. openprocess/mining/conformance/token_replay.py +252 -0
  120. openprocess/mining/csv_import.py +222 -0
  121. openprocess/mining/definitions.py +584 -0
  122. openprocess/mining/dfg.py +187 -0
  123. openprocess/mining/discovery/__init__.py +0 -0
  124. openprocess/mining/discovery/alpha.py +168 -0
  125. openprocess/mining/discovery/heuristics.py +332 -0
  126. openprocess/mining/discovery/inductive.py +477 -0
  127. openprocess/mining/discovery/state_regions.py +62 -0
  128. openprocess/mining/filtering.py +237 -0
  129. openprocess/mining/footprint.py +183 -0
  130. openprocess/mining/invariants.py +191 -0
  131. openprocess/mining/layout.py +279 -0
  132. openprocess/mining/log.py +364 -0
  133. openprocess/mining/petrinet.py +354 -0
  134. openprocess/mining/playout.py +75 -0
  135. openprocess/mining/pm4py_bridge.py +82 -0
  136. openprocess/mining/pnml.py +223 -0
  137. openprocess/mining/processtree.py +216 -0
  138. openprocess/mining/regions.py +476 -0
  139. openprocess/mining/stats.py +160 -0
  140. openprocess/mining/structure.py +374 -0
  141. openprocess/mining/transition_system.py +409 -0
  142. openprocess/mining/xes.py +399 -0
  143. openprocess/ml/__init__.py +0 -0
  144. openprocess/ml/ast_nodes.py +332 -0
  145. openprocess/ml/builtins.py +364 -0
  146. openprocess/ml/colorsets.py +522 -0
  147. openprocess/ml/errors.py +60 -0
  148. openprocess/ml/evaluator.py +754 -0
  149. openprocess/ml/lexer.py +277 -0
  150. openprocess/ml/multiset.py +417 -0
  151. openprocess/ml/parser.py +737 -0
  152. openprocess/ml/values.py +319 -0
  153. openprocess/model/__init__.py +0 -0
  154. openprocess/model/declarations.py +617 -0
  155. openprocess/model/examples.py +98 -0
  156. openprocess/model/net.py +701 -0
  157. openprocess/model/plain.py +192 -0
  158. openprocess/references.py +280 -0
  159. openprocess/sim/__init__.py +0 -0
  160. openprocess/sim/binding.py +620 -0
  161. openprocess/sim/export.py +66 -0
  162. openprocess/sim/simulator.py +315 -0
  163. openprocess/teaching/__init__.py +4 -0
  164. openprocess/teaching/answers.py +4 -0
  165. openprocess/teaching/checks.py +5 -0
  166. openprocess/teaching/pack.py +4 -0
  167. openprocess/teaching/sheet.py +4 -0
  168. openprocess-0.7.0.dist-info/METADATA +927 -0
  169. openprocess-0.7.0.dist-info/RECORD +173 -0
  170. openprocess-0.7.0.dist-info/WHEEL +5 -0
  171. openprocess-0.7.0.dist-info/entry_points.txt +6 -0
  172. openprocess-0.7.0.dist-info/licenses/LICENSE +21 -0
  173. openprocess-0.7.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,737 @@
1
+ """Recursive-descent parser for the CPN ML subset.
2
+
3
+ Grammar and precedence
4
+ ----------------------
5
+ The infix precedence table follows Standard ML, with the two CPN additions
6
+ (``++``/``--`` for multisets and the backquote coefficient) slotted in where
7
+ CPN Tools puts them. Lowest binding power first:
8
+
9
+ ==== ======================== ==============
10
+ Lvl Operators Associativity
11
+ ==== ======================== ==============
12
+ 1 ``orelse`` left
13
+ 2 ``andalso`` left
14
+ 3 ``= <> < > <= >=`` left
15
+ 4 ``+++ ---`` (timed) left
16
+ 5 ``@+`` (time delay) non-associative
17
+ 6 ``:: @ ^^`` right
18
+ 7 ``+ - ^ ++ --`` left
19
+ 8 ``* / div mod`` left
20
+ 9 ``~`` ``not`` (prefix) --
21
+ 10 `` ` `` (coefficient) non-associative
22
+ 11 application ``f x`` left
23
+ 12 atoms --
24
+ ==== ======================== ==============
25
+
26
+ The timed levels follow CPN Tools' timed multisets:
27
+ ``1`x@+5 +++ 1`y@+3`` delays two tokens differently, ``(1`x ++ 1`y)@+5``
28
+ and ``1`x ++ 1`y @+ 5`` delay both, and ``1`3@5 +++ 1`4@0`` gives an
29
+ initial marking with time stamps (``@`` with a number on the right is a time
30
+ stamp; between two lists it is list append).
31
+
32
+ Two consequences are worth internalising, because they explain how ordinary
33
+ CPN inscriptions parse:
34
+
35
+ * ``2`x ++ 3`y`` groups as ``(2`x) ++ (3`y)`` -- the coefficient binds tighter
36
+ than multiset union, so the familiar multiset literal works without brackets.
37
+ * ``1`f(a)`` groups as ``1`(f(a))`` -- application binds tighter still, so a
38
+ computed token value needs no brackets either.
39
+
40
+ Everything else is plain ML. Where ML is ambiguous (a ``case`` nested inside
41
+ another ``case``) we resolve greedily, as every ML compiler does, meaning the
42
+ inner ``case`` swallows following ``|`` rules and you must parenthesise to get
43
+ the other reading.
44
+
45
+ Entry points
46
+ ------------
47
+ ``parse_expression``
48
+ A general expression -- guards, initial markings, ``let`` bodies.
49
+ ``parse_arc_expression``
50
+ Same, but additionally accepts a trailing ``@+ delay`` (only meaningful on
51
+ an arc, hence the separate entry point).
52
+ ``parse_declarations``
53
+ A sequence of ``val`` / ``fun`` declarations, as found in a model's
54
+ declaration block.
55
+ ``expression_to_pattern``
56
+ Reinterprets an already-parsed expression as a pattern. Input arc
57
+ inscriptions are written as expressions but *used* as patterns during
58
+ binding, and this is the bridge between the two.
59
+ """
60
+
61
+ from __future__ import annotations
62
+
63
+ from typing import Any, Callable, Sequence
64
+
65
+ from .ast_nodes import (
66
+ App, BinOp, CaseExpr, Coefficient, Decl, Delay, EmptyMultiset, Expr,
67
+ FnExpr, FunDecl, IfExpr, LetExpr, ListExpr, Literal, PAs, PCons,
68
+ PConstructor, PList, PLiteral, PRecord, PTuple, PVar, PWildcard, Pattern,
69
+ RecordExpr, Selector, TupleExpr, UnOp, ValDecl, Var,
70
+ )
71
+ from .errors import ParseError
72
+ from .lexer import Token, tokenise
73
+ from .values import UNIT
74
+
75
+ # Binary operator levels. Each entry maps a level number to the set of
76
+ # operator spellings at that level; ``_RIGHT_ASSOCIATIVE`` lists the levels that
77
+ # fold to the right instead of the left.
78
+ _BINARY_LEVELS: dict[int, frozenset[str]] = {
79
+ 1: frozenset({"orelse"}),
80
+ 2: frozenset({"andalso"}),
81
+ 3: frozenset({"=", "<>", "<", ">", "<=", ">="}),
82
+ 4: frozenset({"+++", "---"}),
83
+ 5: frozenset({"@+"}),
84
+ 6: frozenset({"::", "@", "^^"}),
85
+ 7: frozenset({"+", "-", "^", "++", "--"}),
86
+ 8: frozenset({"*", "/", "div", "mod"}),
87
+ }
88
+ _RIGHT_ASSOCIATIVE = frozenset({6})
89
+ _DELAY_LEVEL = 5
90
+ _LOWEST_LEVEL = 1
91
+ _HIGHEST_BINARY_LEVEL = 8
92
+
93
+ # Tokens that can begin an atom. The application parser uses this to decide
94
+ # whether the next token continues an application (``f x``) or ends it.
95
+ _ATOM_START_KEYWORDS = frozenset({"true", "false", "nil", "empty", "op"})
96
+
97
+ # Infix operators that ``op`` turns into ordinary two-argument functions:
98
+ # ``op +`` is ``fn (a, b) => a + b``, as in Standard ML.
99
+ _OP_SECTIONS = frozenset(
100
+ spelling for level in (3, 4, 6, 7, 8) for spelling in _BINARY_LEVELS[level]
101
+ )
102
+ _ATOM_START_OPERATORS = frozenset({"(", "[", "{", "#"})
103
+
104
+
105
+ class Parser:
106
+ """A cursor over a token list plus one method per grammar production."""
107
+
108
+ def __init__(self, tokens: Sequence[Token], source: str = "") -> None:
109
+ self.tokens = list(tokens)
110
+ self.index = 0
111
+ self.source = source
112
+
113
+ # -- cursor helpers ------------------------------------------------------
114
+ def peek(self, offset: int = 0) -> Token:
115
+ position = min(self.index + offset, len(self.tokens) - 1)
116
+ return self.tokens[position]
117
+
118
+ def next_token(self) -> Token:
119
+ token = self.peek()
120
+ self.index += 1
121
+ return token
122
+
123
+ def at_operator(self, *spellings: str) -> bool:
124
+ token = self.peek()
125
+ return token.kind == "OP" and token.value in spellings
126
+
127
+ def at_keyword(self, *words: str) -> bool:
128
+ token = self.peek()
129
+ return token.kind == "KEYWORD" and token.value in words
130
+
131
+ def at_end(self) -> bool:
132
+ return self.peek().kind == "EOF"
133
+
134
+ def expect_operator(self, spelling: str) -> Token:
135
+ if not self.at_operator(spelling):
136
+ raise ParseError(
137
+ f"expected {spelling!r} but found {self._describe(self.peek())}",
138
+ self.peek().position,
139
+ )
140
+ return self.next_token()
141
+
142
+ def expect_keyword(self, word: str) -> Token:
143
+ if not self.at_keyword(word):
144
+ raise ParseError(
145
+ f"expected {word!r} but found {self._describe(self.peek())}",
146
+ self.peek().position,
147
+ )
148
+ return self.next_token()
149
+
150
+ def expect_identifier(self) -> str:
151
+ if self.peek().kind != "ID":
152
+ raise ParseError(
153
+ f"expected an identifier but found {self._describe(self.peek())}",
154
+ self.peek().position,
155
+ )
156
+ return str(self.next_token().value)
157
+
158
+ @staticmethod
159
+ def _describe(token: Token) -> str:
160
+ if token.kind == "EOF":
161
+ return "end of input"
162
+ return f"{token.value!r}"
163
+
164
+ # =======================================================================
165
+ # Expressions
166
+ # =======================================================================
167
+ def parse_expression(self) -> Expr:
168
+ """Top of the expression grammar."""
169
+ token = self.peek()
170
+
171
+ if token.kind == "KEYWORD":
172
+ if token.value == "if":
173
+ return self._parse_if()
174
+ if token.value == "let":
175
+ return self._parse_let()
176
+ if token.value == "case":
177
+ return self._parse_case()
178
+ if token.value == "fn":
179
+ return self._parse_fn()
180
+ if token.value == "raise":
181
+ # `raise Exn` -- we parse and discard the exception name so that
182
+ # models using it load; evaluating one is a runtime error.
183
+ start = self.next_token().position
184
+ self.parse_expression()
185
+ return App(Var("raise", start), Literal(UNIT, start), start)
186
+
187
+ return self._parse_binary(_LOWEST_LEVEL)
188
+
189
+ def _parse_if(self) -> Expr:
190
+ start = self.expect_keyword("if").position
191
+ condition = self.parse_expression()
192
+ self.expect_keyword("then")
193
+ then_branch = self.parse_expression()
194
+ self.expect_keyword("else")
195
+ else_branch = self.parse_expression()
196
+ return IfExpr(condition, then_branch, else_branch, start)
197
+
198
+ def _parse_let(self) -> Expr:
199
+ start = self.expect_keyword("let").position
200
+ declarations: list[Decl] = []
201
+ while not self.at_keyword("in"):
202
+ if self.at_end():
203
+ raise ParseError("unterminated 'let': expected 'in'", start)
204
+ declarations.append(self.parse_declaration())
205
+ # Semicolons between declarations are optional in ML.
206
+ while self.at_operator(";"):
207
+ self.next_token()
208
+ self.expect_keyword("in")
209
+ body = self.parse_expression()
210
+ self.expect_keyword("end")
211
+ return LetExpr(tuple(declarations), body, start)
212
+
213
+ def _parse_case(self) -> Expr:
214
+ start = self.expect_keyword("case").position
215
+ scrutinee = self.parse_expression()
216
+ self.expect_keyword("of")
217
+ return CaseExpr(scrutinee, self._parse_match_rules(), start)
218
+
219
+ def _parse_fn(self) -> Expr:
220
+ start = self.expect_keyword("fn").position
221
+ return FnExpr(self._parse_match_rules(), start)
222
+
223
+ def _parse_match_rules(self) -> tuple[tuple[Pattern, Expr], ...]:
224
+ """Parse ``p => e | p => e | ...`` -- shared by ``fn`` and ``case``."""
225
+ rules: list[tuple[Pattern, Expr]] = []
226
+ while True:
227
+ pattern = self.parse_pattern()
228
+ self.expect_operator("=>")
229
+ body = self.parse_expression()
230
+ rules.append((pattern, body))
231
+ if self.at_operator("|"):
232
+ self.next_token()
233
+ continue
234
+ return tuple(rules)
235
+
236
+ # -- infix operators -----------------------------------------------------
237
+ def _parse_binary(self, level: int) -> Expr:
238
+ """Precedence-climbing over :data:`_BINARY_LEVELS`."""
239
+ if level > _HIGHEST_BINARY_LEVEL:
240
+ return self._parse_unary()
241
+
242
+ operators = _BINARY_LEVELS[level]
243
+ left = self._parse_binary(level + 1)
244
+
245
+ if level == _DELAY_LEVEL:
246
+ # `tokens @+ delay`: at most once, the delay being an ordinary
247
+ # arithmetic expression (`x @+ d + 1` delays by d + 1).
248
+ if self.at_operator("@+"):
249
+ token = self.next_token()
250
+ return Delay(left, self._parse_binary(level + 1), token.position)
251
+ return left
252
+
253
+ while True:
254
+ token = self.peek()
255
+ # `div` and `mod` are keywords, every other operator is an OP token.
256
+ spelling = token.value if token.kind in ("OP", "KEYWORD") else None
257
+ if spelling not in operators:
258
+ return left
259
+ self.next_token()
260
+ if level in _RIGHT_ASSOCIATIVE:
261
+ # Right associative: recurse at the *same* level so that
262
+ # `a :: b :: c` becomes `a :: (b :: c)`.
263
+ right = self._parse_binary(level)
264
+ return BinOp(str(spelling), left, right, token.position)
265
+ right = self._parse_binary(level + 1)
266
+ left = BinOp(str(spelling), left, right, token.position)
267
+
268
+ def _parse_unary(self) -> Expr:
269
+ """Prefix ``~`` (arithmetic negation) and ``not`` (boolean negation)."""
270
+ if self.at_operator("~"):
271
+ token = self.next_token()
272
+ return UnOp("~", self._parse_unary(), token.position)
273
+ if self.at_keyword("not"):
274
+ token = self.next_token()
275
+ return UnOp("not", self._parse_unary(), token.position)
276
+ return self._parse_coefficient()
277
+
278
+ def _parse_coefficient(self) -> Expr:
279
+ """``n`v`` -- multiset coefficient, binding tighter than ``++``."""
280
+ left = self._parse_application()
281
+ if self.at_operator("`"):
282
+ token = self.next_token()
283
+ value = self._parse_application()
284
+ return Coefficient(left, value, token.position)
285
+ return left
286
+
287
+ def _parse_application(self) -> Expr:
288
+ """Juxtaposition: ``f x y`` is ``(f x) y``."""
289
+ result = self._parse_atom()
290
+ while self._starts_atom():
291
+ argument = self._parse_atom()
292
+ result = App(result, argument, result.position)
293
+ return result
294
+
295
+ def _starts_atom(self) -> bool:
296
+ token = self.peek()
297
+ if token.kind in ("ID", "INT", "REAL", "STRING", "CHAR"):
298
+ return True
299
+ if token.kind == "KEYWORD":
300
+ return token.value in _ATOM_START_KEYWORDS
301
+ if token.kind == "OP":
302
+ return token.value in _ATOM_START_OPERATORS
303
+ return False
304
+
305
+ # -- atoms ---------------------------------------------------------------
306
+ def _parse_atom(self) -> Expr:
307
+ token = self.peek()
308
+
309
+ if token.kind in ("INT", "REAL", "STRING", "CHAR"):
310
+ self.next_token()
311
+ return Literal(token.value, token.position)
312
+
313
+ if token.kind == "ID":
314
+ self.next_token()
315
+ return Var(str(token.value), token.position)
316
+
317
+ if token.kind == "KEYWORD":
318
+ if token.value in ("true", "false"):
319
+ self.next_token()
320
+ return Literal(token.value == "true", token.position)
321
+ if token.value == "nil":
322
+ self.next_token()
323
+ return ListExpr((), token.position)
324
+ if token.value == "empty":
325
+ self.next_token()
326
+ return EmptyMultiset(token.position)
327
+ if token.value == "op":
328
+ return self._parse_op_section()
329
+ # `if`/`let`/`case`/`fn` in argument position must be bracketed in
330
+ # ML, so reaching here means the input really is malformed.
331
+ raise ParseError(
332
+ f"unexpected keyword {token.value!r} in expression", token.position
333
+ )
334
+
335
+ if token.kind == "OP":
336
+ if token.value == "(":
337
+ return self._parse_parenthesised()
338
+ if token.value == "[":
339
+ return self._parse_list()
340
+ if token.value == "{":
341
+ return self._parse_record()
342
+ if token.value == "#":
343
+ return self._parse_selector()
344
+
345
+ raise ParseError(
346
+ f"unexpected {self._describe(token)} where an expression was expected",
347
+ token.position,
348
+ )
349
+
350
+ def _parse_op_section(self) -> Expr:
351
+ """``op +``: an infix operator as a function on pairs."""
352
+ start = self.next_token().position # the `op` keyword
353
+ token = self.next_token()
354
+ spelling = token.value if token.kind in ("OP", "KEYWORD") else None
355
+ if spelling not in _OP_SECTIONS:
356
+ raise ParseError(
357
+ f"'op' must be followed by an infix operator, not {self._describe(token)}",
358
+ token.position,
359
+ )
360
+ left, right = "op'left", "op'right" # cannot clash with ML names
361
+ pattern = PTuple((PVar(left, start), PVar(right, start)), start)
362
+ body = BinOp(str(spelling), Var(left, start), Var(right, start), start)
363
+ return FnExpr(((pattern, body),), start)
364
+
365
+ def _parse_parenthesised(self) -> Expr:
366
+ """``()``, ``(e)`` or ``(e1, e2, ...)`` -- unit, grouping, or a tuple."""
367
+ start = self.expect_operator("(").position
368
+ if self.at_operator(")"):
369
+ self.next_token()
370
+ return Literal(UNIT, start)
371
+ items = [self.parse_expression()]
372
+ while self.at_operator(","):
373
+ self.next_token()
374
+ items.append(self.parse_expression())
375
+ self.expect_operator(")")
376
+ if len(items) == 1:
377
+ return items[0]
378
+ return TupleExpr(tuple(items), start)
379
+
380
+ def _parse_list(self) -> Expr:
381
+ start = self.expect_operator("[").position
382
+ items: list[Expr] = []
383
+ if not self.at_operator("]"):
384
+ items.append(self.parse_expression())
385
+ while self.at_operator(","):
386
+ self.next_token()
387
+ items.append(self.parse_expression())
388
+ self.expect_operator("]")
389
+ return ListExpr(tuple(items), start)
390
+
391
+ def _parse_record(self) -> Expr:
392
+ start = self.expect_operator("{").position
393
+ fields: list[tuple[str, Expr]] = []
394
+ if not self.at_operator("}"):
395
+ while True:
396
+ name = self.expect_identifier()
397
+ self.expect_operator("=")
398
+ fields.append((name, self.parse_expression()))
399
+ if self.at_operator(","):
400
+ self.next_token()
401
+ continue
402
+ break
403
+ self.expect_operator("}")
404
+ return RecordExpr(tuple(fields), start)
405
+
406
+ def _parse_selector(self) -> Expr:
407
+ """``#field`` (record projection) or ``#2`` (tuple projection)."""
408
+ start = self.expect_operator("#").position
409
+ token = self.next_token()
410
+ if token.kind == "ID":
411
+ return Selector(str(token.value), start)
412
+ if token.kind == "INT":
413
+ return Selector(str(token.value), start)
414
+ raise ParseError("'#' must be followed by a field name or a number", start)
415
+
416
+ # =======================================================================
417
+ # Patterns
418
+ # =======================================================================
419
+ def parse_pattern(self) -> Pattern:
420
+ """Patterns support ``as`` at the outermost level and ``::`` inside."""
421
+ # `x as p`
422
+ if self.peek().kind == "ID" and self.peek(1).kind == "KEYWORD" and self.peek(1).value == "as":
423
+ name_token = self.next_token()
424
+ self.next_token() # consume `as`
425
+ return PAs(str(name_token.value), self.parse_pattern(), name_token.position)
426
+ return self._parse_cons_pattern()
427
+
428
+ def _parse_cons_pattern(self) -> Pattern:
429
+ """``h :: t`` -- right associative, like the expression form."""
430
+ head = self._parse_applied_pattern()
431
+ if self.at_operator("::"):
432
+ token = self.next_token()
433
+ return PCons(head, self._parse_cons_pattern(), token.position)
434
+ return head
435
+
436
+ def _parse_applied_pattern(self) -> Pattern:
437
+ """A constructor applied to an argument: ``Car (a, b)``, ``ph(i)``.
438
+
439
+ This level exists because ML distinguishes two pattern positions. In
440
+ ``fun f p1 p2 = e`` the parameters are *atomic* patterns, so ``f x y``
441
+ means two parameters rather than one constructor application. Anywhere
442
+ else -- inside brackets, in a ``case`` rule, on the left of ``=>`` --
443
+ juxtaposition **is** constructor application. :meth:`_parse_fun`
444
+ therefore calls :meth:`_parse_atomic_pattern` directly and everything
445
+ else comes through here.
446
+
447
+ Whether the head really names a constructor is not decided now: the
448
+ matcher resolves it against the environment, so ``ph(i)`` works whether
449
+ ``ph`` is an index tag or a union constructor.
450
+ """
451
+ head = self._parse_atomic_pattern()
452
+ if isinstance(head, PConstructor) and head.argument is None and self._starts_atomic_pattern():
453
+ return PConstructor(head.name, self._parse_atomic_pattern(), head.position)
454
+ if isinstance(head, PVar) and self._starts_atomic_pattern():
455
+ return PConstructor(head.name, self._parse_atomic_pattern(), head.position)
456
+ return head
457
+
458
+ def _parse_atomic_pattern(self) -> Pattern:
459
+ token = self.peek()
460
+
461
+ if token.kind in ("INT", "REAL", "STRING", "CHAR"):
462
+ self.next_token()
463
+ return PLiteral(token.value, token.position)
464
+
465
+ if token.kind == "KEYWORD":
466
+ if token.value in ("true", "false"):
467
+ self.next_token()
468
+ return PLiteral(token.value == "true", token.position)
469
+ if token.value == "nil":
470
+ self.next_token()
471
+ return PList((), token.position)
472
+ raise ParseError(f"unexpected keyword {token.value!r} in pattern", token.position)
473
+
474
+ if token.kind == "ID":
475
+ self.next_token()
476
+ name = str(token.value)
477
+ # A capitalised identifier is *probably* a nullary constructor, but
478
+ # it could equally be a variable the modeller chose to capitalise,
479
+ # so we emit PConstructor and let the matcher resolve it against
480
+ # the environment. Constructor *application* is handled one level
481
+ # up, in :meth:`_parse_applied_pattern`.
482
+ if name[:1].isupper():
483
+ return PConstructor(name, None, token.position)
484
+ return PVar(name, token.position)
485
+
486
+ if token.kind == "OP":
487
+ if token.value == "_":
488
+ self.next_token()
489
+ return PWildcard(token.position)
490
+ if token.value == "(":
491
+ return self._parse_parenthesised_pattern()
492
+ if token.value == "[":
493
+ return self._parse_list_pattern()
494
+ if token.value == "{":
495
+ return self._parse_record_pattern()
496
+ if token.value == "~" and self.peek(1).kind in ("INT", "REAL"):
497
+ # `~1` reaches here only if the lexer split it, which it does
498
+ # not, but handle it defensively.
499
+ self.next_token()
500
+ number = self.next_token()
501
+ return PLiteral(-number.value, token.position) # type: ignore[operator]
502
+
503
+ raise ParseError(
504
+ f"unexpected {self._describe(token)} where a pattern was expected",
505
+ token.position,
506
+ )
507
+
508
+ def _starts_atomic_pattern(self) -> bool:
509
+ token = self.peek()
510
+ if token.kind in ("ID", "INT", "REAL", "STRING", "CHAR"):
511
+ return True
512
+ if token.kind == "KEYWORD":
513
+ return token.value in ("true", "false", "nil")
514
+ if token.kind == "OP":
515
+ return token.value in ("(", "[", "{", "_")
516
+ return False
517
+
518
+ def _parse_parenthesised_pattern(self) -> Pattern:
519
+ start = self.expect_operator("(").position
520
+ if self.at_operator(")"):
521
+ self.next_token()
522
+ return PLiteral(UNIT, start)
523
+ items = [self.parse_pattern()]
524
+ while self.at_operator(","):
525
+ self.next_token()
526
+ items.append(self.parse_pattern())
527
+ self.expect_operator(")")
528
+ if len(items) == 1:
529
+ return items[0]
530
+ return PTuple(tuple(items), start)
531
+
532
+ def _parse_list_pattern(self) -> Pattern:
533
+ start = self.expect_operator("[").position
534
+ items: list[Pattern] = []
535
+ if not self.at_operator("]"):
536
+ items.append(self.parse_pattern())
537
+ while self.at_operator(","):
538
+ self.next_token()
539
+ items.append(self.parse_pattern())
540
+ self.expect_operator("]")
541
+ return PList(tuple(items), start)
542
+
543
+ def _parse_record_pattern(self) -> Pattern:
544
+ start = self.expect_operator("{").position
545
+ fields: list[tuple[str, Pattern]] = []
546
+ open_ended = False
547
+ if not self.at_operator("}"):
548
+ while True:
549
+ if self.at_operator("."):
550
+ # `...` ellipsis: three separate '.' tokens, or one '..'
551
+ # plus a '.', depending on how the lexer split them.
552
+ while self.at_operator(".") or self.at_operator(".."):
553
+ self.next_token()
554
+ open_ended = True
555
+ break
556
+ name = self.expect_identifier()
557
+ if self.at_operator("="):
558
+ self.next_token()
559
+ fields.append((name, self.parse_pattern()))
560
+ else:
561
+ # ML shorthand: `{name}` binds the field to a variable of
562
+ # the same name.
563
+ fields.append((name, PVar(name, start)))
564
+ if self.at_operator(","):
565
+ self.next_token()
566
+ continue
567
+ break
568
+ self.expect_operator("}")
569
+ return PRecord(tuple(fields), open_ended, start)
570
+
571
+ # =======================================================================
572
+ # Declarations
573
+ # =======================================================================
574
+ def parse_declaration(self) -> Decl:
575
+ token = self.peek()
576
+ if self.at_keyword("val"):
577
+ start = self.next_token().position
578
+ pattern = self.parse_pattern()
579
+ self.expect_operator("=")
580
+ return ValDecl(pattern, self.parse_expression(), start)
581
+ if self.at_keyword("fun"):
582
+ return self._parse_fun()
583
+ raise ParseError(
584
+ f"expected 'val' or 'fun' but found {self._describe(token)}", token.position
585
+ )
586
+
587
+ def _parse_fun(self) -> Decl:
588
+ """``fun f p1 p2 = e | f q1 q2 = e2`` -- curried, multi-clause."""
589
+ start = self.expect_keyword("fun").position
590
+ name = self.expect_identifier()
591
+ clauses: list[tuple[tuple[Pattern, ...], Expr]] = []
592
+ while True:
593
+ parameters: list[Pattern] = []
594
+ while not self.at_operator("="):
595
+ parameters.append(self._parse_atomic_pattern())
596
+ if not parameters:
597
+ raise ParseError(f"function '{name}' has no parameters", start)
598
+ self.expect_operator("=")
599
+ clauses.append((tuple(parameters), self.parse_expression()))
600
+ # Another clause? It starts with `|` followed by the same name.
601
+ if self.at_operator("|"):
602
+ save = self.index
603
+ self.next_token()
604
+ if self.peek().kind == "ID" and self.peek().value == name:
605
+ self.next_token()
606
+ continue
607
+ self.index = save
608
+ return FunDecl(name, tuple(clauses), start)
609
+
610
+
611
+ # ===========================================================================
612
+ # Public entry points
613
+ # ===========================================================================
614
+ def parse_expression(source: str) -> Expr:
615
+ """Parse a complete expression; error if anything is left over."""
616
+ parser = Parser(tokenise(source), source)
617
+ expression = parser.parse_expression()
618
+ if not parser.at_end():
619
+ raise ParseError(
620
+ f"unexpected {Parser._describe(parser.peek())} after the expression",
621
+ parser.peek().position,
622
+ )
623
+ return expression
624
+
625
+
626
+ def parse_arc_expression(source: str) -> Expr:
627
+ """Parse an arc inscription, allowing a trailing ``@+ delay``."""
628
+ parser = Parser(tokenise(source), source)
629
+ expression = parser.parse_expression()
630
+ if parser.at_operator("@+"):
631
+ token = parser.next_token()
632
+ expression = Delay(expression, parser.parse_expression(), token.position)
633
+ if not parser.at_end():
634
+ raise ParseError(
635
+ f"unexpected {Parser._describe(parser.peek())} after the arc expression",
636
+ parser.peek().position,
637
+ )
638
+ return expression
639
+
640
+
641
+ def parse_declarations(source: str) -> list[Decl]:
642
+ """Parse a whole declaration block (a sequence of ``val`` / ``fun``)."""
643
+ parser = Parser(tokenise(source), source)
644
+ declarations: list[Decl] = []
645
+ while not parser.at_end():
646
+ if parser.at_operator(";"):
647
+ parser.next_token()
648
+ continue
649
+ declarations.append(parser.parse_declaration())
650
+ return declarations
651
+
652
+
653
+ #: How :func:`expression_to_pattern` should read a bare identifier.
654
+ #: ``"constructor"`` -- a union/enumeration tag, matched as a constant;
655
+ #: ``"variable"`` -- a net variable, bound by the match;
656
+ #: ``"other"`` -- anything else (a function, an ML constant), which means
657
+ #: the expression is *not* a pattern and must be evaluated instead.
658
+ IdentifierRole = str
659
+
660
+
661
+ def default_identifier_role(name: str) -> IdentifierRole:
662
+ """Fallback classifier: ML's capitalisation convention.
663
+
664
+ Used when no environment is available (tests, tooling). The binder always
665
+ supplies a real classifier built from the model's declarations, because
666
+ the convention is only a convention -- plenty of CPN models capitalise
667
+ their function names.
668
+ """
669
+ return "constructor" if name[:1].isupper() else "variable"
670
+
671
+
672
+ def expression_to_pattern(expression: Expr,
673
+ identifier_role: Callable[[str], IdentifierRole] | None = None) -> Pattern:
674
+ """Reinterpret an expression as a pattern.
675
+
676
+ Why this exists
677
+ ---------------
678
+ An input arc carries an inscription such as ``(x, n)`` or ``1`p``. CPN
679
+ semantics say the transition is enabled when the inscription *evaluates* to
680
+ a sub-multiset of the place's marking -- but the variables in it are
681
+ unbound, so we cannot evaluate it yet. The practical algorithm (the same
682
+ one CPN Tools uses for the common case) is to treat the inscription as a
683
+ pattern and match it against the tokens that are actually there, which
684
+ determines the variable values directly instead of guessing them.
685
+
686
+ Not every expression is a pattern: ``x + 1`` is not, because matching it
687
+ would require inverting addition. When that happens we raise
688
+ :class:`ParseError`, and the binder falls back to enumerating the
689
+ variable's colour set and *evaluating* the expression instead. So this
690
+ function failing is a performance question, not a correctness one.
691
+ """
692
+ role = identifier_role or default_identifier_role
693
+
694
+ def convert(node: Expr) -> Pattern:
695
+ if isinstance(node, Literal):
696
+ return PLiteral(node.value, node.position)
697
+
698
+ if isinstance(node, Var):
699
+ kind = role(node.name)
700
+ if kind == "constructor":
701
+ return PConstructor(node.name, None, node.position)
702
+ if kind == "variable":
703
+ return PVar(node.name, node.position)
704
+ # An ML constant or a function name: not something we can match
705
+ # against, so the whole inscription must be evaluated instead.
706
+ raise ParseError(
707
+ f"'{node.name}' is not a variable or a constructor, so this "
708
+ "expression cannot be used as a pattern",
709
+ node.position,
710
+ )
711
+
712
+ if isinstance(node, TupleExpr):
713
+ return PTuple(tuple(convert(i) for i in node.items), node.position)
714
+
715
+ if isinstance(node, ListExpr):
716
+ return PList(tuple(convert(i) for i in node.items), node.position)
717
+
718
+ if isinstance(node, RecordExpr):
719
+ return PRecord(
720
+ tuple((n, convert(v)) for n, v in node.fields), False, node.position
721
+ )
722
+
723
+ if isinstance(node, BinOp) and node.operator == "::":
724
+ return PCons(convert(node.left), convert(node.right), node.position)
725
+
726
+ if isinstance(node, App) and isinstance(node.function, Var):
727
+ # `Car (a, b)` -- but only if `Car` really is a constructor. A
728
+ # capitalised *function* such as `Chopsticks(p)` must not be read
729
+ # as a pattern, or we would try to match tokens against it.
730
+ if role(node.function.name) == "constructor":
731
+ return PConstructor(
732
+ node.function.name, convert(node.argument), node.position
733
+ )
734
+
735
+ raise ParseError("this expression cannot be used as a pattern", node.position)
736
+
737
+ return convert(expression)