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.
- cpnpy/__init__.py +46 -0
- openprocess/__init__.py +57 -0
- openprocess/analysis/__init__.py +0 -0
- openprocess/analysis/state_space.py +521 -0
- openprocess/analysis/state_space_process.py +251 -0
- openprocess/cli.py +742 -0
- openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +31 -0
- openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +38 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +27 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +29 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +65 -0
- openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +1 -0
- openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +67 -0
- openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +75 -0
- openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +5 -0
- openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +27 -0
- openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +65 -0
- openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +1 -0
- openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +62 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +1 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +36 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +28 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +30 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +36 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +69 -0
- openprocess/exercises/pack.md +14 -0
- openprocess/flow/__init__.py +50 -0
- openprocess/flow/box.py +466 -0
- openprocess/flow/boxes/__init__.py +7 -0
- openprocess/flow/boxes/check.py +119 -0
- openprocess/flow/boxes/compare.py +16 -0
- openprocess/flow/boxes/cpn.py +53 -0
- openprocess/flow/boxes/discover.py +124 -0
- openprocess/flow/boxes/filter.py +80 -0
- openprocess/flow/boxes/input.py +124 -0
- openprocess/flow/boxes/output.py +52 -0
- openprocess/flow/boxes/predict.py +186 -0
- openprocess/flow/boxes/science.py +159 -0
- openprocess/flow/boxes/sweeps.py +18 -0
- openprocess/flow/convert.py +187 -0
- openprocess/flow/datasets.py +198 -0
- openprocess/flow/explain.py +115 -0
- openprocess/flow/library.py +222 -0
- openprocess/flow/record.py +385 -0
- openprocess/flow/runner.py +357 -0
- openprocess/flow/sweep.py +92 -0
- openprocess/flow/types.py +290 -0
- openprocess/flow/workflow.py +628 -0
- openprocess/gui/__init__.py +0 -0
- openprocess/gui/app.py +90 -0
- openprocess/gui/arc_editing.py +295 -0
- openprocess/gui/canvas.py +1414 -0
- openprocess/gui/flow/__init__.py +8 -0
- openprocess/gui/flow/canvas.py +854 -0
- openprocess/gui/flow/page.py +972 -0
- openprocess/gui/flow/templates.py +131 -0
- openprocess/gui/flow/viewers.py +665 -0
- openprocess/gui/items.py +1275 -0
- openprocess/gui/learn/answer_boxes.py +978 -0
- openprocess/gui/learn/concealment.py +91 -0
- openprocess/gui/learn/mode.py +1181 -0
- openprocess/gui/panning.py +241 -0
- openprocess/gui/resources/openprocess-icon.png +0 -0
- openprocess/gui/studio/__init__.py +1 -0
- openprocess/gui/studio/__main__.py +3 -0
- openprocess/gui/studio/app.py +4031 -0
- openprocess/gui/studio/charts.py +115 -0
- openprocess/gui/studio/compare_page.py +487 -0
- openprocess/gui/studio/cpn_page.py +1858 -0
- openprocess/gui/studio/definition_view.py +284 -0
- openprocess/gui/studio/derivation_view.py +421 -0
- openprocess/gui/studio/documents.py +152 -0
- openprocess/gui/studio/dotted_chart.py +1401 -0
- openprocess/gui/studio/file_dialogs.py +143 -0
- openprocess/gui/studio/filter_dialog.py +247 -0
- openprocess/gui/studio/graph_builders.py +176 -0
- openprocess/gui/studio/graph_view.py +682 -0
- openprocess/gui/studio/instances.py +413 -0
- openprocess/gui/studio/log_editor.py +675 -0
- openprocess/gui/studio/log_page.py +800 -0
- openprocess/gui/studio/markdown_view.py +127 -0
- openprocess/gui/studio/mathtext.py +260 -0
- openprocess/gui/studio/ml_highlighter.py +75 -0
- openprocess/gui/studio/model_page.py +760 -0
- openprocess/gui/studio/net_comparison.py +124 -0
- openprocess/gui/studio/notes_overlay.py +275 -0
- openprocess/gui/studio/petri_page.py +844 -0
- openprocess/gui/studio/regions_view.py +502 -0
- openprocess/gui/studio/sidebar.py +149 -0
- openprocess/gui/studio/style.py +503 -0
- openprocess/gui/studio/tool_icons.py +134 -0
- openprocess/gui/studio/updates.py +439 -0
- openprocess/gui/studio/widgets.py +899 -0
- openprocess/gui/studio/workers.py +60 -0
- openprocess/gui/studio/workspace.py +447 -0
- openprocess/gui/theme.py +394 -0
- openprocess/gui/tidy.py +86 -0
- openprocess/io/__init__.py +0 -0
- openprocess/io/cpn_reader.py +389 -0
- openprocess/io/cpn_writer.py +357 -0
- openprocess/learn/__init__.py +23 -0
- openprocess/learn/answers.py +188 -0
- openprocess/learn/checks.py +953 -0
- openprocess/learn/computed.py +1180 -0
- openprocess/learn/context.py +145 -0
- openprocess/learn/exam.py +169 -0
- openprocess/learn/exercise-packs.md +325 -0
- openprocess/learn/importer.py +216 -0
- openprocess/learn/notation.py +474 -0
- openprocess/learn/pack.py +511 -0
- openprocess/learn/sheet.py +296 -0
- openprocess/mining/__init__.py +73 -0
- openprocess/mining/analysis.py +689 -0
- openprocess/mining/columns.py +282 -0
- openprocess/mining/compare_nets.py +246 -0
- openprocess/mining/conformance/__init__.py +0 -0
- openprocess/mining/conformance/alignments.py +263 -0
- openprocess/mining/conformance/quality.py +145 -0
- openprocess/mining/conformance/token_replay.py +252 -0
- openprocess/mining/csv_import.py +222 -0
- openprocess/mining/definitions.py +584 -0
- openprocess/mining/dfg.py +187 -0
- openprocess/mining/discovery/__init__.py +0 -0
- openprocess/mining/discovery/alpha.py +168 -0
- openprocess/mining/discovery/heuristics.py +332 -0
- openprocess/mining/discovery/inductive.py +477 -0
- openprocess/mining/discovery/state_regions.py +62 -0
- openprocess/mining/filtering.py +237 -0
- openprocess/mining/footprint.py +183 -0
- openprocess/mining/invariants.py +191 -0
- openprocess/mining/layout.py +279 -0
- openprocess/mining/log.py +364 -0
- openprocess/mining/petrinet.py +354 -0
- openprocess/mining/playout.py +75 -0
- openprocess/mining/pm4py_bridge.py +82 -0
- openprocess/mining/pnml.py +223 -0
- openprocess/mining/processtree.py +216 -0
- openprocess/mining/regions.py +476 -0
- openprocess/mining/stats.py +160 -0
- openprocess/mining/structure.py +374 -0
- openprocess/mining/transition_system.py +409 -0
- openprocess/mining/xes.py +399 -0
- openprocess/ml/__init__.py +0 -0
- openprocess/ml/ast_nodes.py +332 -0
- openprocess/ml/builtins.py +364 -0
- openprocess/ml/colorsets.py +522 -0
- openprocess/ml/errors.py +60 -0
- openprocess/ml/evaluator.py +754 -0
- openprocess/ml/lexer.py +277 -0
- openprocess/ml/multiset.py +417 -0
- openprocess/ml/parser.py +737 -0
- openprocess/ml/values.py +319 -0
- openprocess/model/__init__.py +0 -0
- openprocess/model/declarations.py +617 -0
- openprocess/model/examples.py +98 -0
- openprocess/model/net.py +701 -0
- openprocess/model/plain.py +192 -0
- openprocess/references.py +280 -0
- openprocess/sim/__init__.py +0 -0
- openprocess/sim/binding.py +620 -0
- openprocess/sim/export.py +66 -0
- openprocess/sim/simulator.py +315 -0
- openprocess/teaching/__init__.py +4 -0
- openprocess/teaching/answers.py +4 -0
- openprocess/teaching/checks.py +5 -0
- openprocess/teaching/pack.py +4 -0
- openprocess/teaching/sheet.py +4 -0
- openprocess-0.7.0.dist-info/METADATA +927 -0
- openprocess-0.7.0.dist-info/RECORD +173 -0
- openprocess-0.7.0.dist-info/WHEEL +5 -0
- openprocess-0.7.0.dist-info/entry_points.txt +6 -0
- openprocess-0.7.0.dist-info/licenses/LICENSE +21 -0
- 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
|