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,522 @@
1
+ """The colour set system: CPN's type system.
2
+
3
+ A *colour set* in CPN terminology is a type. Every place is annotated with
4
+ one, and a token in that place must carry a value belonging to it. CPN Tools
5
+ offers a fixed catalogue of colour set forms, and this module implements them
6
+ one class each.
7
+
8
+ The catalogue, with the CPN Tools declaration syntax we parse:
9
+
10
+ ================================== ===========================
11
+ Declaration Class
12
+ ================================== ===========================
13
+ ``colset U = unit;`` :class:`UnitColourSet`
14
+ ``colset B = bool;`` :class:`BoolColourSet`
15
+ ``colset I = int;`` :class:`IntColourSet`
16
+ ``colset I = int with 1..10;`` :class:`IntColourSet` (ranged)
17
+ ``colset R = real;`` :class:`RealColourSet`
18
+ ``colset S = string;`` :class:`StringColourSet`
19
+ ``colset E = with a | b | c;`` :class:`EnumColourSet`
20
+ ``colset X = index i with 1..5;`` :class:`IndexColourSet`
21
+ ``colset P = product A * B;`` :class:`ProductColourSet`
22
+ ``colset Rc = record f:A * g:B;`` :class:`RecordColourSet`
23
+ ``colset L = list A;`` :class:`ListColourSet`
24
+ ``colset Un = union C1:A + C2:B;`` :class:`UnionColourSet`
25
+ ``colset Sub = subset A by pred;`` :class:`SubsetColourSet`
26
+ ``colset Al = A;`` :class:`AliasColourSet`
27
+ ================================== ===========================
28
+
29
+ Any of these may carry the ``timed`` keyword, which does not change the set of
30
+ values -- it changes how the *place* stores them (see
31
+ :class:`~openprocess.ml.multiset.TimedMultiset`).
32
+
33
+ Two operations matter for the rest of the system:
34
+
35
+ ``contains(value)``
36
+ Type checking. Called when a place's initial marking is evaluated and
37
+ whenever a binding assigns a value to a typed variable.
38
+
39
+ ``members()``
40
+ Enumeration. The binder falls back to enumerating a variable's colour set
41
+ when the variable cannot be determined from the input tokens (for example
42
+ a variable that appears only on an output arc). Infinite colour sets raise
43
+ :class:`InfiniteColourSetError`, which the binder turns into a clear
44
+ "cannot enumerate INT, constrain this variable" message rather than hanging.
45
+ """
46
+
47
+ from __future__ import annotations
48
+
49
+ import itertools
50
+ from typing import Any, Callable, Iterator, Sequence
51
+
52
+ from .errors import CPNMLError
53
+ from .values import UNIT, Constructor, MLList, Record, Unit, format_value
54
+
55
+
56
+ class InfiniteColourSetError(CPNMLError):
57
+ """Raised by :meth:`ColourSet.members` on a colour set that cannot be listed."""
58
+
59
+
60
+ class ColourSet:
61
+ """Abstract base class for all colour sets."""
62
+
63
+ #: Whether values of this colour set carry time stamps in a place.
64
+ timed: bool
65
+
66
+ def __init__(self, name: str, timed: bool = False) -> None:
67
+ self.name = name
68
+ self.timed = timed
69
+
70
+ # -- to be provided by subclasses ---------------------------------------
71
+ def contains(self, value: Any) -> bool:
72
+ """Is ``value`` a legal member of this colour set?"""
73
+ raise NotImplementedError
74
+
75
+ def members(self) -> Iterator[Any]:
76
+ """Yield every member. Raises for infinite colour sets."""
77
+ raise InfiniteColourSetError(
78
+ f"colour set '{self.name}' has infinitely many members and cannot be enumerated"
79
+ )
80
+
81
+ def is_finite(self) -> bool:
82
+ return False
83
+
84
+ # -- shared helpers ------------------------------------------------------
85
+ def check(self, value: Any) -> Any:
86
+ """Return ``value`` if it belongs here, else raise a typing error."""
87
+ if not self.contains(value):
88
+ raise CPNMLError(
89
+ f"value {format_value(value)} is not a member of colour set '{self.name}'"
90
+ )
91
+ return value
92
+
93
+ def size(self) -> int:
94
+ """Number of members; only meaningful when :meth:`is_finite`."""
95
+ return sum(1 for _ in self.members())
96
+
97
+ def __repr__(self) -> str:
98
+ return f"<{type(self).__name__} {self.name}{' timed' if self.timed else ''}>"
99
+
100
+
101
+ # ---------------------------------------------------------------------------
102
+ # Simple (atomic) colour sets
103
+ # ---------------------------------------------------------------------------
104
+ class UnitColourSet(ColourSet):
105
+ """``colset U = unit;`` -- exactly one value, ``()``.
106
+
107
+ ``unit with e`` renames that single value to the identifier ``e``; we keep
108
+ the alias for printing but the value is still :data:`~openprocess.ml.values.UNIT`.
109
+ """
110
+
111
+ def __init__(self, name: str, timed: bool = False, alias: str | None = None) -> None:
112
+ super().__init__(name, timed)
113
+ self.alias = alias
114
+
115
+ def contains(self, value: Any) -> bool:
116
+ return isinstance(value, Unit)
117
+
118
+ def members(self) -> Iterator[Any]:
119
+ yield UNIT
120
+
121
+ def is_finite(self) -> bool:
122
+ return True
123
+
124
+
125
+ class BoolColourSet(ColourSet):
126
+ """``colset B = bool;`` -- ``{false, true}``."""
127
+
128
+ def __init__(self, name: str, timed: bool = False,
129
+ false_alias: str | None = None, true_alias: str | None = None) -> None:
130
+ super().__init__(name, timed)
131
+ self.false_alias = false_alias
132
+ self.true_alias = true_alias
133
+
134
+ def contains(self, value: Any) -> bool:
135
+ return isinstance(value, bool)
136
+
137
+ def members(self) -> Iterator[Any]:
138
+ yield False
139
+ yield True
140
+
141
+ def is_finite(self) -> bool:
142
+ return True
143
+
144
+
145
+ class IntColourSet(ColourSet):
146
+ """``colset I = int;`` or ``colset I = int with 1..10;``.
147
+
148
+ Without a range this is infinite (and so not enumerable). With a range it
149
+ behaves like a finite interval, which is the usual way modellers make a
150
+ variable enumerable.
151
+ """
152
+
153
+ def __init__(self, name: str, timed: bool = False,
154
+ low: int | None = None, high: int | None = None) -> None:
155
+ super().__init__(name, timed)
156
+ self.low = low
157
+ self.high = high
158
+
159
+ def contains(self, value: Any) -> bool:
160
+ # ``bool`` is a subclass of ``int`` in Python but is a different colour
161
+ # set in CPN, so exclude it explicitly.
162
+ if isinstance(value, bool) or not isinstance(value, int):
163
+ return False
164
+ if self.low is not None and value < self.low:
165
+ return False
166
+ if self.high is not None and value > self.high:
167
+ return False
168
+ return True
169
+
170
+ def is_finite(self) -> bool:
171
+ return self.low is not None and self.high is not None
172
+
173
+ def members(self) -> Iterator[Any]:
174
+ if not self.is_finite():
175
+ return super().members()
176
+ return iter(range(self.low, self.high + 1)) # type: ignore[arg-type]
177
+
178
+
179
+ class IntInfColourSet(IntColourSet):
180
+ """``colset II = intinf;`` -- arbitrary precision integers.
181
+
182
+ Python integers are already arbitrary precision, so this differs from
183
+ :class:`IntColourSet` only in name. It exists so that round-tripping a
184
+ ``.cpn`` file preserves the declaration the modeller wrote.
185
+ """
186
+
187
+
188
+ class RealColourSet(ColourSet):
189
+ """``colset R = real;`` -- floating point numbers, never enumerable."""
190
+
191
+ def __init__(self, name: str, timed: bool = False,
192
+ low: float | None = None, high: float | None = None) -> None:
193
+ super().__init__(name, timed)
194
+ self.low = low
195
+ self.high = high
196
+
197
+ def contains(self, value: Any) -> bool:
198
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
199
+ return False
200
+ value = float(value)
201
+ if self.low is not None and value < self.low:
202
+ return False
203
+ if self.high is not None and value > self.high:
204
+ return False
205
+ return True
206
+
207
+
208
+ class StringColourSet(ColourSet):
209
+ """``colset S = string;``, optionally constrained.
210
+
211
+ CPN Tools allows ``string with "a".."z"`` (restrict the character set) and
212
+ ``and 1..8`` (restrict the length). Both are supported here.
213
+ """
214
+
215
+ def __init__(self, name: str, timed: bool = False,
216
+ char_low: str | None = None, char_high: str | None = None,
217
+ length_low: int | None = None, length_high: int | None = None) -> None:
218
+ super().__init__(name, timed)
219
+ self.char_low = char_low
220
+ self.char_high = char_high
221
+ self.length_low = length_low
222
+ self.length_high = length_high
223
+
224
+ def contains(self, value: Any) -> bool:
225
+ if not isinstance(value, str):
226
+ return False
227
+ if self.length_low is not None and len(value) < self.length_low:
228
+ return False
229
+ if self.length_high is not None and len(value) > self.length_high:
230
+ return False
231
+ if self.char_low is not None or self.char_high is not None:
232
+ low = self.char_low or "\x00"
233
+ high = self.char_high or "\U0010ffff"
234
+ if any(not (low <= ch <= high) for ch in value):
235
+ return False
236
+ return True
237
+
238
+ def is_finite(self) -> bool:
239
+ # Finite only when both the alphabet and the maximum length are bounded.
240
+ return (
241
+ self.char_low is not None
242
+ and self.char_high is not None
243
+ and self.length_high is not None
244
+ )
245
+
246
+ def members(self) -> Iterator[Any]:
247
+ if not self.is_finite():
248
+ return super().members()
249
+ alphabet = [chr(c) for c in range(ord(self.char_low), ord(self.char_high) + 1)] # type: ignore[arg-type]
250
+ low = self.length_low or 0
251
+ for length in range(low, self.length_high + 1): # type: ignore[arg-type]
252
+ for combination in itertools.product(alphabet, repeat=length):
253
+ yield "".join(combination)
254
+
255
+
256
+ class EnumColourSet(ColourSet):
257
+ """``colset E = with red | green | blue;``.
258
+
259
+ Members are :class:`~openprocess.ml.values.Constructor` values without arguments.
260
+ The identifiers are also injected into the global evaluation environment as
261
+ constants, so an inscription can just write ``red``.
262
+ """
263
+
264
+ def __init__(self, name: str, constants: Sequence[str], timed: bool = False) -> None:
265
+ super().__init__(name, timed)
266
+ self.constants = tuple(constants)
267
+
268
+ def contains(self, value: Any) -> bool:
269
+ return (
270
+ isinstance(value, Constructor)
271
+ and not value.has_argument
272
+ and value.name in self.constants
273
+ )
274
+
275
+ def members(self) -> Iterator[Any]:
276
+ for constant in self.constants:
277
+ yield Constructor(constant)
278
+
279
+ def is_finite(self) -> bool:
280
+ return True
281
+
282
+
283
+ class IndexColourSet(ColourSet):
284
+ """``colset X = index proc with 1..5;``.
285
+
286
+ Values are constructors carrying an integer, printed ``proc(1)``. This is
287
+ CPN Tools' idiomatic way of naming a finite family of otherwise identical
288
+ entities (processes, resources, sites).
289
+ """
290
+
291
+ def __init__(self, name: str, tag: str, low: int, high: int, timed: bool = False) -> None:
292
+ super().__init__(name, timed)
293
+ self.tag = tag
294
+ self.low = low
295
+ self.high = high
296
+
297
+ def contains(self, value: Any) -> bool:
298
+ return (
299
+ isinstance(value, Constructor)
300
+ and value.name == self.tag
301
+ and value.has_argument
302
+ and isinstance(value.argument, int)
303
+ and not isinstance(value.argument, bool)
304
+ and self.low <= value.argument <= self.high
305
+ )
306
+
307
+ def members(self) -> Iterator[Any]:
308
+ for index in range(self.low, self.high + 1):
309
+ yield Constructor(self.tag, index)
310
+
311
+ def is_finite(self) -> bool:
312
+ return True
313
+
314
+
315
+ # ---------------------------------------------------------------------------
316
+ # Compound colour sets
317
+ # ---------------------------------------------------------------------------
318
+ class ProductColourSet(ColourSet):
319
+ """``colset P = product A * B * C;`` -- Cartesian product, values are tuples."""
320
+
321
+ def __init__(self, name: str, components: Sequence[ColourSet], timed: bool = False) -> None:
322
+ super().__init__(name, timed)
323
+ self.components = tuple(components)
324
+
325
+ def contains(self, value: Any) -> bool:
326
+ return (
327
+ isinstance(value, tuple)
328
+ and len(value) == len(self.components)
329
+ and all(cs.contains(v) for cs, v in zip(self.components, value))
330
+ )
331
+
332
+ def is_finite(self) -> bool:
333
+ return all(cs.is_finite() for cs in self.components)
334
+
335
+ def members(self) -> Iterator[Any]:
336
+ if not self.is_finite():
337
+ return super().members()
338
+ # ``itertools.product`` materialises each component, which is fine
339
+ # because we have already established that each one is finite.
340
+ return itertools.product(*(list(cs.members()) for cs in self.components))
341
+
342
+
343
+ class RecordColourSet(ColourSet):
344
+ """``colset R = record name:STRING * age:INT;`` -- labelled product."""
345
+
346
+ def __init__(self, name: str, fields: Sequence[tuple[str, ColourSet]], timed: bool = False) -> None:
347
+ super().__init__(name, timed)
348
+ self.fields = tuple(fields)
349
+
350
+ def contains(self, value: Any) -> bool:
351
+ if not isinstance(value, Record):
352
+ return False
353
+ if value.names() != tuple(n for n, _ in self.fields):
354
+ return False
355
+ return all(cs.contains(v) for (_, cs), (_, v) in zip(self.fields, value.fields))
356
+
357
+ def is_finite(self) -> bool:
358
+ return all(cs.is_finite() for _, cs in self.fields)
359
+
360
+ def members(self) -> Iterator[Any]:
361
+ if not self.is_finite():
362
+ return super().members()
363
+ names = [n for n, _ in self.fields]
364
+ for combination in itertools.product(*(list(cs.members()) for _, cs in self.fields)):
365
+ yield Record(tuple(zip(names, combination)))
366
+
367
+
368
+ class ListColourSet(ColourSet):
369
+ """``colset L = list A;`` or ``list A with 0..3;`` (bounded length).
370
+
371
+ Unbounded lists are infinite even over a finite element colour set, so
372
+ enumeration requires the ``with`` bound.
373
+ """
374
+
375
+ def __init__(self, name: str, element: ColourSet, timed: bool = False,
376
+ length_low: int | None = None, length_high: int | None = None) -> None:
377
+ super().__init__(name, timed)
378
+ self.element = element
379
+ self.length_low = length_low
380
+ self.length_high = length_high
381
+
382
+ def contains(self, value: Any) -> bool:
383
+ if not isinstance(value, MLList):
384
+ return False
385
+ if self.length_low is not None and len(value) < self.length_low:
386
+ return False
387
+ if self.length_high is not None and len(value) > self.length_high:
388
+ return False
389
+ return all(self.element.contains(v) for v in value)
390
+
391
+ def is_finite(self) -> bool:
392
+ return self.element.is_finite() and self.length_high is not None
393
+
394
+ def members(self) -> Iterator[Any]:
395
+ if not self.is_finite():
396
+ return super().members()
397
+ element_values = list(self.element.members())
398
+ low = self.length_low or 0
399
+ for length in range(low, self.length_high + 1): # type: ignore[arg-type]
400
+ for combination in itertools.product(element_values, repeat=length):
401
+ yield MLList(combination)
402
+
403
+
404
+ class UnionColourSet(ColourSet):
405
+ """``colset U = union Car:CARS + Bike:BIKES + Walk;`` -- tagged sum.
406
+
407
+ A constructor may be *nullary* (``Walk`` above), in which case its member is
408
+ a bare :class:`~openprocess.ml.values.Constructor` with no argument, exactly like
409
+ an enumeration constant.
410
+ """
411
+
412
+ def __init__(self, name: str, variants: Sequence[tuple[str, ColourSet | None]],
413
+ timed: bool = False) -> None:
414
+ super().__init__(name, timed)
415
+ self.variants = tuple(variants)
416
+
417
+ def _variant(self, tag: str) -> tuple[str, ColourSet | None] | None:
418
+ for variant in self.variants:
419
+ if variant[0] == tag:
420
+ return variant
421
+ return None
422
+
423
+ def contains(self, value: Any) -> bool:
424
+ if not isinstance(value, Constructor):
425
+ return False
426
+ variant = self._variant(value.name)
427
+ if variant is None:
428
+ return False
429
+ _, payload_cs = variant
430
+ if payload_cs is None:
431
+ return not value.has_argument
432
+ return value.has_argument and payload_cs.contains(value.argument)
433
+
434
+ def is_finite(self) -> bool:
435
+ return all(cs is None or cs.is_finite() for _, cs in self.variants)
436
+
437
+ def members(self) -> Iterator[Any]:
438
+ if not self.is_finite():
439
+ return super().members()
440
+ for tag, payload_cs in self.variants:
441
+ if payload_cs is None:
442
+ yield Constructor(tag)
443
+ else:
444
+ for payload in payload_cs.members():
445
+ yield Constructor(tag, payload)
446
+
447
+
448
+ class SubsetColourSet(ColourSet):
449
+ """``colset S = subset A by pred;`` -- members of ``A`` satisfying a predicate.
450
+
451
+ The predicate is supplied as a Python callable by the declaration compiler,
452
+ which wraps the user's CPN ML function. Enumeration filters the base set,
453
+ so a subset of an infinite colour set stays non-enumerable.
454
+ """
455
+
456
+ def __init__(self, name: str, base: ColourSet, predicate: Callable[[Any], bool],
457
+ timed: bool = False, predicate_source: str = "",
458
+ members: Sequence[Any] | None = None) -> None:
459
+ super().__init__(name, timed)
460
+ self.base = base
461
+ self.predicate = predicate
462
+ # Kept so that the writer can reproduce the original declaration text.
463
+ self.predicate_source = predicate_source
464
+ #: ``subset A with [v1, v2]``: the listed values, in order. Finite
465
+ #: even when ``A`` is not.
466
+ self.listed = list(members) if members is not None else None
467
+
468
+ def contains(self, value: Any) -> bool:
469
+ return self.base.contains(value) and bool(self.predicate(value))
470
+
471
+ def is_finite(self) -> bool:
472
+ return self.listed is not None or self.base.is_finite()
473
+
474
+ def members(self) -> Iterator[Any]:
475
+ if self.listed is not None:
476
+ yield from self.listed
477
+ return
478
+ for value in self.base.members():
479
+ if self.predicate(value):
480
+ yield value
481
+
482
+
483
+ class AliasColourSet(ColourSet):
484
+ """``colset MyInt = INT;`` -- a new name for an existing colour set.
485
+
486
+ Delegates every operation to the target. We keep the indirection rather
487
+ than collapsing it so that the declaration survives a save/load round trip
488
+ and so that error messages can name the alias the modeller actually used.
489
+ """
490
+
491
+ def __init__(self, name: str, target: ColourSet, timed: bool | None = None) -> None:
492
+ super().__init__(name, target.timed if timed is None else timed)
493
+ self.target = target
494
+
495
+ def contains(self, value: Any) -> bool:
496
+ return self.target.contains(value)
497
+
498
+ def is_finite(self) -> bool:
499
+ return self.target.is_finite()
500
+
501
+ def members(self) -> Iterator[Any]:
502
+ return self.target.members()
503
+
504
+
505
+ # ---------------------------------------------------------------------------
506
+ # The standard colour sets that CPN Tools predeclares
507
+ # ---------------------------------------------------------------------------
508
+ def standard_colour_sets() -> dict[str, ColourSet]:
509
+ """The colour sets available in every model without being declared.
510
+
511
+ CPN Tools makes ``UNIT``, ``BOOL``, ``INT``, ``INTINF``, ``REAL`` and
512
+ ``STRING`` available implicitly. We create a fresh dictionary per call so
513
+ that two models can never share (and accidentally mutate) the same objects.
514
+ """
515
+ return {
516
+ "UNIT": UnitColourSet("UNIT"),
517
+ "BOOL": BoolColourSet("BOOL"),
518
+ "INT": IntColourSet("INT"),
519
+ "INTINF": IntInfColourSet("INTINF"),
520
+ "REAL": RealColourSet("REAL"),
521
+ "STRING": StringColourSet("STRING"),
522
+ }
@@ -0,0 +1,60 @@
1
+ """Exception hierarchy for the CPN ML subsystem.
2
+
3
+ Every failure that the user can cause by writing a bad model raises a subclass
4
+ of :class:`CPNMLError`. The GUI catches that one class and shows the message
5
+ next to the offending inscription, so it is important that nothing in the ML
6
+ layer raises a bare ``ValueError`` for a *user* mistake. Internal invariant
7
+ violations should still raise ``AssertionError`` -- those are our bugs, not the
8
+ modeller's.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+
14
+ class CPNMLError(Exception):
15
+ """Base class for all errors caused by the contents of a model."""
16
+
17
+ def __init__(self, message: str, position: int | None = None) -> None:
18
+ super().__init__(message)
19
+ self.message = message
20
+ # Character offset into the source inscription, when we know it. The
21
+ # editor uses this to underline the exact token.
22
+ self.position = position
23
+
24
+ def __str__(self) -> str: # pragma: no cover - trivial
25
+ if self.position is None:
26
+ return self.message
27
+ return f"{self.message} (at character {self.position})"
28
+
29
+
30
+ class LexError(CPNMLError):
31
+ """The inscription contains a character sequence that is not a token."""
32
+
33
+
34
+ class ParseError(CPNMLError):
35
+ """The tokens are individually valid but do not form a legal expression."""
36
+
37
+
38
+ class TypeError_(CPNMLError):
39
+ """A value does not belong to the colour set it is being used as.
40
+
41
+ Named with a trailing underscore so it does not shadow the builtin
42
+ ``TypeError``; exported as ``CPNTypeError`` for readability.
43
+ """
44
+
45
+
46
+ CPNTypeError = TypeError_
47
+
48
+
49
+ class EvalError(CPNMLError):
50
+ """Evaluation failed: unbound identifier, division by zero, no matching
51
+ case branch, wrong number of arguments, and so on."""
52
+
53
+
54
+ class MatchError(EvalError):
55
+ """A pattern could not be matched against a value.
56
+
57
+ This is a *recoverable* error during binding search -- the binder catches
58
+ it and simply moves on to the next candidate token -- so it gets its own
59
+ class rather than being reported to the user directly.
60
+ """