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,332 @@
1
+ """Abstract syntax tree for the CPN ML subset.
2
+
3
+ Nodes are plain frozen dataclasses -- no behaviour, no evaluation logic. The
4
+ evaluator walks them in :mod:`openprocess.ml.evaluator` and the binder walks the
5
+ pattern nodes in :mod:`openprocess.sim.binding`. Keeping the tree behaviour-free
6
+ means we can serialise, pretty-print, or analyse it (for example to collect a
7
+ transition's free variables) without dragging the interpreter along.
8
+
9
+ The tree is split into three families:
10
+
11
+ ``Expr``
12
+ Things that evaluate to a value: literals, variables, applications,
13
+ ``if``/``let``/``case``, and the CPN multiset forms.
14
+
15
+ ``Pattern``
16
+ Things that *destructure* a value and produce variable bindings. Used by
17
+ ``fn``, ``case``, ``val``, and -- most importantly -- by the binder, which
18
+ matches input arc inscriptions against the tokens actually in a place.
19
+
20
+ ``Decl``
21
+ ``val`` and ``fun`` bindings, appearing inside ``let`` and at the top level
22
+ of a model's declaration blocks.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass, field
28
+ from typing import Any
29
+
30
+
31
+ # ===========================================================================
32
+ # Expressions
33
+ # ===========================================================================
34
+ class Expr:
35
+ """Base class for expression nodes.
36
+
37
+ ``position`` is the source offset of the construct, carried so that runtime
38
+ errors can be reported against the exact spot in the inscription.
39
+ """
40
+
41
+ position: int = 0
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class Literal(Expr):
46
+ """An integer, real, string, character, boolean or ``()`` literal."""
47
+
48
+ value: Any
49
+ position: int = 0
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class Var(Expr):
54
+ """An identifier: a bound variable, a function, or a nullary constructor.
55
+
56
+ Which of the three it is cannot be decided syntactically -- ``red`` looks
57
+ identical whether it is a variable or an enumeration constant -- so the
58
+ decision is deferred to the evaluator, which consults the environment.
59
+ """
60
+
61
+ name: str
62
+ position: int = 0
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class TupleExpr(Expr):
67
+ """``(a, b, c)`` -- a value of a product colour set."""
68
+
69
+ items: tuple[Expr, ...]
70
+ position: int = 0
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class ListExpr(Expr):
75
+ """``[a, b, c]`` -- a value of a list colour set."""
76
+
77
+ items: tuple[Expr, ...]
78
+ position: int = 0
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class RecordExpr(Expr):
83
+ """``{name = a, age = b}`` -- a value of a record colour set."""
84
+
85
+ fields: tuple[tuple[str, Expr], ...]
86
+ position: int = 0
87
+
88
+
89
+ @dataclass(frozen=True)
90
+ class Selector(Expr):
91
+ """``#field`` or ``#2`` used as a function.
92
+
93
+ In ML ``#name`` is an ordinary first-class function that projects a field
94
+ out of a record, and ``#2`` does the same for a tuple component (1-based).
95
+ Representing it as an atom rather than as special syntax means ``#name r``
96
+ is simply an :class:`App`, and ``List.map #name people`` works for free.
97
+ """
98
+
99
+ field_name: str
100
+ position: int = 0
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class App(Expr):
105
+ """Function application ``f x``.
106
+
107
+ Curried: ``f x y`` is ``App(App(f, x), y)``. Constructor application
108
+ (``Car(v)``) uses the same node; the evaluator distinguishes them by what
109
+ ``f`` evaluates to.
110
+ """
111
+
112
+ function: Expr
113
+ argument: Expr
114
+ position: int = 0
115
+
116
+
117
+ @dataclass(frozen=True)
118
+ class BinOp(Expr):
119
+ """An infix operator application, e.g. ``a + b`` or ``m1 ++ m2``."""
120
+
121
+ operator: str
122
+ left: Expr
123
+ right: Expr
124
+ position: int = 0
125
+
126
+
127
+ @dataclass(frozen=True)
128
+ class UnOp(Expr):
129
+ """A prefix operator: ``~x`` (negation) or ``not b``."""
130
+
131
+ operator: str
132
+ operand: Expr
133
+ position: int = 0
134
+
135
+
136
+ @dataclass(frozen=True)
137
+ class IfExpr(Expr):
138
+ """``if c then a else b``. The ``else`` branch is mandatory in ML."""
139
+
140
+ condition: Expr
141
+ then_branch: Expr
142
+ else_branch: Expr
143
+ position: int = 0
144
+
145
+
146
+ @dataclass(frozen=True)
147
+ class LetExpr(Expr):
148
+ """``let val x = e1 ... in e2 end`` -- local declarations then a body."""
149
+
150
+ declarations: tuple["Decl", ...]
151
+ body: Expr
152
+ position: int = 0
153
+
154
+
155
+ @dataclass(frozen=True)
156
+ class CaseExpr(Expr):
157
+ """``case e of p1 => e1 | p2 => e2``.
158
+
159
+ Rules are tried top to bottom; the first pattern that matches wins, exactly
160
+ as in ML. If none matches, evaluation raises ``MatchError``.
161
+ """
162
+
163
+ scrutinee: Expr
164
+ rules: tuple[tuple["Pattern", Expr], ...]
165
+ position: int = 0
166
+
167
+
168
+ @dataclass(frozen=True)
169
+ class FnExpr(Expr):
170
+ """``fn p1 => e1 | p2 => e2`` -- an anonymous function."""
171
+
172
+ rules: tuple[tuple["Pattern", Expr], ...]
173
+ position: int = 0
174
+
175
+
176
+ # -- CPN-specific multiset syntax -------------------------------------------
177
+ @dataclass(frozen=True)
178
+ class Coefficient(Expr):
179
+ """``n`v`` -- the multiset containing ``n`` copies of ``v``.
180
+
181
+ This is the one piece of syntax that is CPN's rather than ML's. Note the
182
+ coefficient may itself be an arbitrary expression, as in ``(k+1)`x``.
183
+ """
184
+
185
+ count: Expr
186
+ value: Expr
187
+ position: int = 0
188
+
189
+
190
+ @dataclass(frozen=True)
191
+ class EmptyMultiset(Expr):
192
+ """The literal ``empty`` -- the multiset with no tokens."""
193
+
194
+ position: int = 0
195
+
196
+
197
+ @dataclass(frozen=True)
198
+ class Delay(Expr):
199
+ """``e @+ d`` -- an arc expression with a time delay.
200
+
201
+ Only legal at the top level of an arc inscription on an output arc into a
202
+ timed place. ``e`` yields the tokens, ``d`` yields the delay to add to the
203
+ current model clock to get their time stamp.
204
+ """
205
+
206
+ base: Expr
207
+ delay: Expr
208
+ position: int = 0
209
+
210
+
211
+ # ===========================================================================
212
+ # Patterns
213
+ # ===========================================================================
214
+ class Pattern:
215
+ """Base class for pattern nodes."""
216
+
217
+ position: int = 0
218
+
219
+
220
+ @dataclass(frozen=True)
221
+ class PWildcard(Pattern):
222
+ """``_`` -- matches anything, binds nothing."""
223
+
224
+ position: int = 0
225
+
226
+
227
+ @dataclass(frozen=True)
228
+ class PVar(Pattern):
229
+ """``x`` -- matches anything and binds it to ``x``."""
230
+
231
+ name: str
232
+ position: int = 0
233
+
234
+
235
+ @dataclass(frozen=True)
236
+ class PLiteral(Pattern):
237
+ """A constant pattern: matches only that exact value."""
238
+
239
+ value: Any
240
+ position: int = 0
241
+
242
+
243
+ @dataclass(frozen=True)
244
+ class PTuple(Pattern):
245
+ """``(p1, p2)`` -- matches a tuple of the same arity, component-wise."""
246
+
247
+ items: tuple[Pattern, ...]
248
+ position: int = 0
249
+
250
+
251
+ @dataclass(frozen=True)
252
+ class PList(Pattern):
253
+ """``[p1, p2]`` -- matches a list of exactly that length."""
254
+
255
+ items: tuple[Pattern, ...]
256
+ position: int = 0
257
+
258
+
259
+ @dataclass(frozen=True)
260
+ class PRecord(Pattern):
261
+ """``{name = p, ...}`` -- matches a record.
262
+
263
+ ``open_ended`` records the ``...`` ellipsis, which lets a pattern mention
264
+ only some of the fields.
265
+ """
266
+
267
+ fields: tuple[tuple[str, Pattern], ...]
268
+ open_ended: bool = False
269
+ position: int = 0
270
+
271
+
272
+ @dataclass(frozen=True)
273
+ class PCons(Pattern):
274
+ """``h :: t`` -- matches a non-empty list, splitting head from tail."""
275
+
276
+ head: Pattern
277
+ tail: Pattern
278
+ position: int = 0
279
+
280
+
281
+ @dataclass(frozen=True)
282
+ class PConstructor(Pattern):
283
+ """``Car p`` or a bare ``red`` -- matches a union/enumeration value.
284
+
285
+ ``argument`` is ``None`` for nullary constructors.
286
+ """
287
+
288
+ name: str
289
+ argument: Pattern | None = None
290
+ position: int = 0
291
+
292
+
293
+ @dataclass(frozen=True)
294
+ class PAs(Pattern):
295
+ """``x as p`` -- match ``p`` but also bind the whole value to ``x``."""
296
+
297
+ name: str
298
+ pattern: Pattern
299
+ position: int = 0
300
+
301
+
302
+ # ===========================================================================
303
+ # Declarations
304
+ # ===========================================================================
305
+ class Decl:
306
+ """Base class for declaration nodes."""
307
+
308
+ position: int = 0
309
+
310
+
311
+ @dataclass(frozen=True)
312
+ class ValDecl(Decl):
313
+ """``val p = e`` -- evaluate ``e`` and bind whatever ``p`` destructures."""
314
+
315
+ pattern: Pattern
316
+ expression: Expr
317
+ position: int = 0
318
+
319
+
320
+ @dataclass(frozen=True)
321
+ class FunDecl(Decl):
322
+ """``fun f p1 = e1 | f p2 = e2`` -- a (possibly recursive) function.
323
+
324
+ ``clauses`` holds one entry per bar-separated alternative; each entry is a
325
+ tuple of argument patterns (more than one for a curried function) and the
326
+ body. Recursion works because the evaluator binds ``f`` in the closure's
327
+ own environment before the body is ever run.
328
+ """
329
+
330
+ name: str
331
+ clauses: tuple[tuple[tuple[Pattern, ...], Expr], ...] = field(default=())
332
+ position: int = 0
@@ -0,0 +1,364 @@
1
+ """The standard basis available to every inscription.
2
+
3
+ CPN ML inherits most of the Standard ML Basis Library. Reimplementing all of
4
+ it is neither possible nor useful, so this module provides the subset that real
5
+ CPN models actually use, in three groups:
6
+
7
+ 1. **Standard ML basis** -- arithmetic, strings, and the ``List`` structure.
8
+ 2. **CPN'MS multiset functions** -- ``size``, ``cf``, ``ms_to_col`` and friends,
9
+ which CPN Tools defines in its multiset structure.
10
+ 3. **Random distributions** -- ``discrete``, ``uniform``, ``exponential``,
11
+ ``normal``, ``poisson`` and so on. These are what timed models use to draw
12
+ delays, and they are the reason a CPN simulation is stochastic.
13
+
14
+ Calling convention
15
+ ------------------
16
+ ML functions take exactly one argument. A function that appears to take two
17
+ is either *tupled* (``min (a, b)`` receives one tuple) or *curried*
18
+ (``List.map f xs`` is ``(List.map f) xs``). Both shapes are supported:
19
+ :class:`Builtin` with ``arity=1`` for tupled functions, and ``arity>1`` for
20
+ curried ones, where applying too few arguments yields a
21
+ :class:`PartialApplication` that waits for the rest.
22
+
23
+ Reproducibility
24
+ ---------------
25
+ All randomness goes through a single :class:`random.Random` instance owned by
26
+ the evaluation environment, so seeding it makes a whole simulation replayable.
27
+ That is essential for debugging a model: without it, "it deadlocked once" is
28
+ not something you can investigate.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import math
34
+ import random
35
+ from typing import Any, Callable
36
+
37
+ from .errors import EvalError
38
+ from .multiset import Multiset
39
+ from .values import UNIT, Constructor, MLList, Record, Unit, format_value
40
+
41
+
42
+ class Builtin:
43
+ """A primitive function implemented in Python.
44
+
45
+ ``arity`` is the number of *curried* arguments. ``function`` receives them
46
+ as a Python tuple once they have all arrived.
47
+ """
48
+
49
+ __slots__ = ("name", "function", "arity")
50
+
51
+ def __init__(self, name: str, function: Callable[..., Any], arity: int = 1) -> None:
52
+ self.name = name
53
+ self.function = function
54
+ self.arity = arity
55
+
56
+ def __repr__(self) -> str:
57
+ return f"fn {self.name}"
58
+
59
+
60
+ class PartialApplication:
61
+ """A builtin that has received some, but not all, of its curried arguments."""
62
+
63
+ __slots__ = ("builtin", "arguments")
64
+
65
+ def __init__(self, builtin: Builtin, arguments: tuple[Any, ...]) -> None:
66
+ self.builtin = builtin
67
+ self.arguments = arguments
68
+
69
+ def __repr__(self) -> str:
70
+ return f"fn {self.builtin.name} (partially applied)"
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Small helpers shared by many builtins
75
+ # ---------------------------------------------------------------------------
76
+ def _expect_int(value: Any, where: str) -> int:
77
+ if isinstance(value, bool) or not isinstance(value, int):
78
+ raise EvalError(f"{where} expects an integer, got {format_value(value)}")
79
+ return value
80
+
81
+
82
+ def _expect_number(value: Any, where: str) -> float:
83
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
84
+ raise EvalError(f"{where} expects a number, got {format_value(value)}")
85
+ return float(value)
86
+
87
+
88
+ def _expect_list(value: Any, where: str) -> MLList:
89
+ if not isinstance(value, MLList):
90
+ raise EvalError(f"{where} expects a list, got {format_value(value)}")
91
+ return value
92
+
93
+
94
+ def _expect_pair(value: Any, where: str) -> tuple[Any, Any]:
95
+ if not isinstance(value, tuple) or len(value) != 2:
96
+ raise EvalError(f"{where} expects a pair, got {format_value(value)}")
97
+ return value[0], value[1]
98
+
99
+
100
+ def _expect_multiset(value: Any, where: str) -> Multiset:
101
+ if isinstance(value, Multiset):
102
+ return value
103
+ raise EvalError(f"{where} expects a multiset, got {format_value(value)}")
104
+
105
+
106
+ def _raise(message: str) -> Any:
107
+ raise EvalError(message)
108
+
109
+
110
+ def make_builtins(rng: random.Random, apply_function: Callable[[Any, Any], Any]) -> dict[str, Any]:
111
+ """Build the standard environment.
112
+
113
+ ``apply_function`` is injected by the evaluator so that higher-order
114
+ builtins such as ``List.map`` can call back into user-defined closures
115
+ without this module importing the evaluator (which would be circular).
116
+ """
117
+ table: dict[str, Any] = {}
118
+
119
+ def register(name: str, function: Callable[..., Any], arity: int = 1, *aliases: str) -> None:
120
+ builtin = Builtin(name, function, arity)
121
+ table[name] = builtin
122
+ for alias in aliases:
123
+ table[alias] = builtin
124
+
125
+ # -- arithmetic ---------------------------------------------------------
126
+ register("abs", lambda v: abs(_expect_number(v, "abs")) if isinstance(v, float) else abs(_expect_int(v, "abs")),
127
+ 1, "Int.abs", "Real.abs")
128
+ register("min", lambda v: min(_expect_pair(v, "min")[0], _expect_pair(v, "min")[1]), 1, "Int.min")
129
+ register("max", lambda v: max(_expect_pair(v, "max")[0], _expect_pair(v, "max")[1]), 1, "Int.max")
130
+ register("real", lambda v: float(_expect_int(v, "real")), 1, "Real.fromInt")
131
+ register("floor", lambda v: math.floor(_expect_number(v, "floor")), 1, "Real.floor")
132
+ register("ceil", lambda v: math.ceil(_expect_number(v, "ceil")), 1, "Real.ceil")
133
+ register("trunc", lambda v: math.trunc(_expect_number(v, "trunc")), 1, "Real.trunc")
134
+ # ML's `round` is round-half-to-even, which is also Python's rule.
135
+ register("round", lambda v: round(_expect_number(v, "round")), 1, "Real.round")
136
+ register("Math.sqrt", lambda v: math.sqrt(_expect_number(v, "sqrt")), 1, "sqrt")
137
+ register("Math.ln", lambda v: math.log(_expect_number(v, "ln")), 1, "ln")
138
+ register("Math.exp", lambda v: math.exp(_expect_number(v, "exp")), 1, "exp")
139
+ register("Math.pow", lambda v: math.pow(*(_expect_pair(v, "pow"))), 1, "pow")
140
+
141
+ # -- conversions to and from strings ------------------------------------
142
+ def _int_to_string(value: Any) -> str:
143
+ number = _expect_int(value, "Int.toString")
144
+ # ML prints negative integers with a leading tilde.
145
+ return f"~{-number}" if number < 0 else str(number)
146
+
147
+ register("Int.toString", _int_to_string)
148
+ register("Real.toString", lambda v: (lambda x: f"~{-x!r}" if x < 0 else repr(x))(_expect_number(v, "Real.toString")))
149
+ register("Bool.toString", lambda v: "true" if v else "false")
150
+ register("str", lambda v: v if isinstance(v, str) else format_value(v))
151
+ def _from_string(pattern: str, convert: Callable[[str], Any]) -> Callable[[Any], Any]:
152
+ """``Int.fromString "12"`` is ``SOME 12``, and ``NONE`` for "abc".
153
+
154
+ Like Standard ML: leading blanks are skipped, ``~`` or ``-`` makes the
155
+ number negative, and whatever follows the number is ignored.
156
+ """
157
+ import re
158
+ compiled = re.compile(r"\s*([~+-]?)(" + pattern + ")")
159
+
160
+ def parse(value: Any) -> Any:
161
+ if not isinstance(value, str):
162
+ raise EvalError(f"fromString expects a string, got {format_value(value)}")
163
+ found = compiled.match(value)
164
+ if not found:
165
+ return Constructor("NONE")
166
+ sign = "-" if found.group(1) in ("~", "-") else ""
167
+ return Constructor("SOME", convert(sign + found.group(2)))
168
+ return parse
169
+
170
+ register("Int.fromString", _from_string(r"\d+", int))
171
+ register("Real.fromString",
172
+ _from_string(r"\d+(?:\.\d+)?(?:[eE][~+-]?\d+)?|\.\d+",
173
+ lambda text: float(text.replace("~", "-"))))
174
+
175
+ # -- options (SOME x / NONE) ----------------------------------------------
176
+ def _is_some(value: Any) -> bool:
177
+ if isinstance(value, Constructor) and value.name in ("SOME", "NONE"):
178
+ return value.name == "SOME"
179
+ raise EvalError(f"expected an option (SOME x or NONE), got {format_value(value)}")
180
+
181
+ def _value_of(value: Any) -> Any:
182
+ if not _is_some(value):
183
+ raise EvalError("valOf applied to NONE (exception Option)")
184
+ return value.argument
185
+
186
+ register("isSome", _is_some, 1, "Option.isSome")
187
+ register("valOf", _value_of, 1, "Option.valOf")
188
+ register("getOpt", lambda v: (lambda o, d: o.argument if _is_some(o) else d)(*_expect_pair(v, "getOpt")),
189
+ 1, "Option.getOpt")
190
+
191
+ # -- strings ------------------------------------------------------------
192
+ register("size", lambda v: len(v) if isinstance(v, str) else _multiset_size(v), 1, "String.size")
193
+ register("String.concat", lambda v: "".join(_expect_list(v, "String.concat")))
194
+ register("String.str", lambda v: str(v))
195
+ register("explode", lambda v: MLList(tuple(str(v))), 1, "String.explode")
196
+ register("implode", lambda v: "".join(_expect_list(v, "implode")), 1, "String.implode")
197
+ register("concat", lambda v: "".join(_expect_list(v, "concat")))
198
+
199
+ def _string_sub(value: Any) -> str:
200
+ text, index = _expect_pair(value, "String.sub")
201
+ index = _expect_int(index, "String.sub")
202
+ if not isinstance(text, str) or not 0 <= index < len(text):
203
+ raise EvalError(f"String.sub: index {index} is outside the string (exception Subscript)")
204
+ return text[index]
205
+
206
+ register("String.sub", _string_sub)
207
+ # Characters are one-letter strings in this implementation.
208
+ register("ord", lambda v: ord(v) if isinstance(v, str) and len(v) == 1 else
209
+ _raise(f"ord expects a character, got {format_value(v)}"), 1, "Char.ord")
210
+ register("chr", lambda v: chr(_expect_int(v, "chr")), 1, "Char.chr")
211
+ register(
212
+ "substring",
213
+ lambda v: str(v[0])[_expect_int(v[1], "substring"): _expect_int(v[1], "substring") + _expect_int(v[2], "substring")],
214
+ 1,
215
+ "String.substring",
216
+ )
217
+
218
+ # -- lists --------------------------------------------------------------
219
+ def _head(value: Any) -> Any:
220
+ items = _expect_list(value, "hd")
221
+ if not len(items):
222
+ raise EvalError("hd applied to the empty list")
223
+ return items[0]
224
+
225
+ def _tail(value: Any) -> Any:
226
+ items = _expect_list(value, "tl")
227
+ if not len(items):
228
+ raise EvalError("tl applied to the empty list")
229
+ return MLList(items.items[1:])
230
+
231
+ register("hd", _head, 1, "List.hd")
232
+ register("tl", _tail, 1, "List.tl")
233
+ register("null", lambda v: len(_expect_list(v, "null")) == 0, 1, "List.null")
234
+ register("length", lambda v: len(_expect_list(v, "length")), 1, "List.length")
235
+ register("rev", lambda v: MLList(reversed(_expect_list(v, "rev").items)), 1, "List.rev")
236
+ register("List.last", lambda v: _expect_list(v, "List.last")[-1])
237
+ register("List.concat",
238
+ lambda v: MLList(item for inner in _expect_list(v, "List.concat")
239
+ for item in _expect_list(inner, "List.concat")))
240
+ register(
241
+ "List.find",
242
+ lambda f, xs: next((Constructor("SOME", x) for x in _expect_list(xs, "List.find")
243
+ if apply_function(f, x)), Constructor("NONE")),
244
+ 2,
245
+ )
246
+ register(
247
+ "List.partition",
248
+ lambda f, xs: (lambda items: (MLList(x for x in items if apply_function(f, x)),
249
+ MLList(x for x in items if not apply_function(f, x))))(
250
+ _expect_list(xs, "List.partition").items),
251
+ 2,
252
+ )
253
+ register(
254
+ "List.nth",
255
+ lambda v: _expect_list(v[0], "List.nth")[_expect_int(v[1], "List.nth")],
256
+ )
257
+ register(
258
+ "List.take",
259
+ lambda v: MLList(_expect_list(v[0], "List.take").items[: _expect_int(v[1], "List.take")]),
260
+ )
261
+ register(
262
+ "List.drop",
263
+ lambda v: MLList(_expect_list(v[0], "List.drop").items[_expect_int(v[1], "List.drop"):]),
264
+ )
265
+
266
+ # Higher-order list functions. Curried, hence ``arity=2``/``3``.
267
+ register(
268
+ "List.map",
269
+ lambda f, xs: MLList(apply_function(f, x) for x in _expect_list(xs, "List.map")),
270
+ 2,
271
+ "map",
272
+ )
273
+ register(
274
+ "List.filter",
275
+ lambda f, xs: MLList(x for x in _expect_list(xs, "List.filter") if apply_function(f, x)),
276
+ 2,
277
+ )
278
+ register(
279
+ "List.exists",
280
+ lambda f, xs: any(bool(apply_function(f, x)) for x in _expect_list(xs, "List.exists")),
281
+ 2,
282
+ )
283
+ register(
284
+ "List.all",
285
+ lambda f, xs: all(bool(apply_function(f, x)) for x in _expect_list(xs, "List.all")),
286
+ 2,
287
+ )
288
+ register(
289
+ "List.app",
290
+ lambda f, xs: ([apply_function(f, x) for x in _expect_list(xs, "List.app")], UNIT)[1],
291
+ 2,
292
+ "app",
293
+ )
294
+
295
+ def _foldl(f: Any, initial: Any, xs: Any) -> Any:
296
+ # ML's foldl passes (element, accumulator) as a pair.
297
+ accumulator = initial
298
+ for item in _expect_list(xs, "List.foldl"):
299
+ accumulator = apply_function(f, (item, accumulator))
300
+ return accumulator
301
+
302
+ def _foldr(f: Any, initial: Any, xs: Any) -> Any:
303
+ accumulator = initial
304
+ for item in reversed(_expect_list(xs, "List.foldr").items):
305
+ accumulator = apply_function(f, (item, accumulator))
306
+ return accumulator
307
+
308
+ register("List.foldl", _foldl, 3, "foldl")
309
+ register("List.foldr", _foldr, 3, "foldr")
310
+ register(
311
+ "List.tabulate",
312
+ lambda v: MLList(apply_function(v[1], i) for i in range(_expect_int(v[0], "List.tabulate"))),
313
+ )
314
+
315
+ # -- multisets (CPN'MS) --------------------------------------------------
316
+ def _multiset_size(value: Any) -> int:
317
+ return _expect_multiset(value, "size").size()
318
+
319
+ def _ms_to_col(value: Any) -> Any:
320
+ """The one colour of a multiset of size 1 (CPN Tools' definition)."""
321
+ tokens = _expect_multiset(value, "ms_to_col")
322
+ if tokens.size() != 1:
323
+ raise EvalError(f"ms_to_col needs a multiset of size 1, got {tokens}")
324
+ return next(tokens.expand())
325
+
326
+ register("ms_to_col", _ms_to_col)
327
+ register("ms_to_list", lambda v: MLList(_expect_multiset(v, "ms_to_list").expand()))
328
+ register("list_to_ms", lambda v: Multiset.from_values(_expect_list(v, "list_to_ms")))
329
+ register("cf", lambda v: _expect_multiset(v[1], "cf").count(v[0]))
330
+ register("empty_ms", lambda _v: Multiset.empty())
331
+ register("List.length_ms", _multiset_size)
332
+
333
+ # -- random distributions ------------------------------------------------
334
+ # Each returns a single draw. ``discrete`` and ``uniform`` take an
335
+ # inclusive pair; the others take their usual parameters.
336
+ register("discrete", lambda v: rng.randint(*(int(x) for x in _expect_pair(v, "discrete"))))
337
+ register("uniform", lambda v: rng.uniform(*(float(x) for x in _expect_pair(v, "uniform"))))
338
+ register("exponential", lambda v: rng.expovariate(_expect_number(v, "exponential")))
339
+ register("normal", lambda v: rng.gauss(*(float(x) for x in _expect_pair(v, "normal"))))
340
+ register("erlang", lambda v: sum(rng.expovariate(float(v[1])) for _ in range(int(v[0]))))
341
+ register("bernoulli", lambda v: 1 if rng.random() < _expect_number(v, "bernoulli") else 0, 1, "Bernoulli")
342
+ register(
343
+ "binomial",
344
+ lambda v: sum(1 for _ in range(int(v[0])) if rng.random() < float(v[1])),
345
+ )
346
+
347
+ def _poisson(value: Any) -> int:
348
+ """Knuth's algorithm -- adequate for the small means used in models."""
349
+ mean = _expect_number(value, "poisson")
350
+ limit = math.exp(-mean)
351
+ count, product = 0, 1.0
352
+ while True:
353
+ product *= rng.random()
354
+ if product <= limit:
355
+ return count
356
+ count += 1
357
+
358
+ register("poisson", _poisson)
359
+
360
+ # -- miscellaneous -------------------------------------------------------
361
+ register("ignore", lambda _v: UNIT)
362
+ register("not", lambda v: not v)
363
+
364
+ return table