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,754 @@
|
|
|
1
|
+
"""The CPN ML evaluator: turns an AST plus an environment into a value.
|
|
2
|
+
|
|
3
|
+
Scope and lookup
|
|
4
|
+
----------------
|
|
5
|
+
Environments form a chain (:class:`Environment`), so a ``let`` or a function
|
|
6
|
+
call pushes a small frame rather than copying the whole global table. Lookup
|
|
7
|
+
walks the chain outwards. The outermost frame holds the standard basis from
|
|
8
|
+
:mod:`openprocess.ml.builtins` plus everything the model declares.
|
|
9
|
+
|
|
10
|
+
Functions
|
|
11
|
+
---------
|
|
12
|
+
User functions are :class:`Closure` objects capturing the environment in which
|
|
13
|
+
they were declared. ``fun`` is made recursive by binding the function's own
|
|
14
|
+
name inside the closure's environment *before* the body ever runs, which is the
|
|
15
|
+
standard trick and avoids a separate ``letrec`` construct.
|
|
16
|
+
|
|
17
|
+
Identifier resolution
|
|
18
|
+
---------------------
|
|
19
|
+
CPN ML has no syntactic marker distinguishing a variable from a nullary
|
|
20
|
+
constructor: ``red`` could be either. We resolve at lookup time -- an
|
|
21
|
+
identifier bound in the environment is whatever it is bound to, and an unbound
|
|
22
|
+
capitalised identifier is treated as a nullary constructor. This matches how
|
|
23
|
+
declarations work in practice, because enumeration constants are injected into
|
|
24
|
+
the global environment when a colour set is compiled.
|
|
25
|
+
|
|
26
|
+
Errors
|
|
27
|
+
------
|
|
28
|
+
Anything the modeller can get wrong raises a subclass of
|
|
29
|
+
:class:`~openprocess.ml.errors.CPNMLError` carrying the source position, so the editor
|
|
30
|
+
can point at the failing token. Pattern-match failure raises
|
|
31
|
+
:class:`~openprocess.ml.errors.MatchError` specifically, because the binder catches
|
|
32
|
+
that one and treats it as "this token does not fit", not as an error.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import random
|
|
38
|
+
from typing import Any, Iterable, Mapping, Sequence
|
|
39
|
+
|
|
40
|
+
from .ast_nodes import (
|
|
41
|
+
App, BinOp, CaseExpr, Coefficient, Decl, Delay, EmptyMultiset, Expr,
|
|
42
|
+
FnExpr, FunDecl, IfExpr, LetExpr, ListExpr, Literal, PAs, PCons,
|
|
43
|
+
PConstructor, PList, PLiteral, PRecord, PTuple, PVar, PWildcard, Pattern,
|
|
44
|
+
RecordExpr, Selector, TupleExpr, UnOp, ValDecl, Var,
|
|
45
|
+
)
|
|
46
|
+
import sys
|
|
47
|
+
|
|
48
|
+
from .builtins import Builtin, PartialApplication, make_builtins
|
|
49
|
+
|
|
50
|
+
# The evaluator is recursive, and ML code recurses over lists: a function such
|
|
51
|
+
# as ``insertB2F`` walking a 120-passenger queue nests a few thousand Python
|
|
52
|
+
# frames. Python's default limit (1000) made that fail with "maximum
|
|
53
|
+
# recursion depth exceeded", so raise it to a level real models need.
|
|
54
|
+
if sys.getrecursionlimit() < 20_000:
|
|
55
|
+
sys.setrecursionlimit(20_000)
|
|
56
|
+
from .errors import EvalError, MatchError
|
|
57
|
+
from .multiset import Multiset, TimedTokens
|
|
58
|
+
from .values import UNIT, Constructor, MLList, Record, Unit, format_value, sort_key
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Environment:
|
|
62
|
+
"""A lexical scope: a small dict plus a pointer to the enclosing scope."""
|
|
63
|
+
|
|
64
|
+
__slots__ = ("bindings", "parent")
|
|
65
|
+
|
|
66
|
+
def __init__(self, bindings: Mapping[str, Any] | None = None,
|
|
67
|
+
parent: "Environment | None" = None) -> None:
|
|
68
|
+
self.bindings: dict[str, Any] = dict(bindings or {})
|
|
69
|
+
self.parent = parent
|
|
70
|
+
|
|
71
|
+
def lookup(self, name: str) -> Any:
|
|
72
|
+
scope: Environment | None = self
|
|
73
|
+
while scope is not None:
|
|
74
|
+
if name in scope.bindings:
|
|
75
|
+
return scope.bindings[name]
|
|
76
|
+
scope = scope.parent
|
|
77
|
+
raise KeyError(name)
|
|
78
|
+
|
|
79
|
+
def contains(self, name: str) -> bool:
|
|
80
|
+
scope: Environment | None = self
|
|
81
|
+
while scope is not None:
|
|
82
|
+
if name in scope.bindings:
|
|
83
|
+
return True
|
|
84
|
+
scope = scope.parent
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
def define(self, name: str, value: Any) -> None:
|
|
88
|
+
self.bindings[name] = value
|
|
89
|
+
|
|
90
|
+
def child(self, bindings: Mapping[str, Any] | None = None) -> "Environment":
|
|
91
|
+
return Environment(bindings, self)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Closure:
|
|
95
|
+
"""A user-defined function value.
|
|
96
|
+
|
|
97
|
+
``rules`` is the list of ``(pattern, body)`` alternatives. A curried
|
|
98
|
+
``fun f x y = ...`` is desugared by the evaluator into nested closures, so
|
|
99
|
+
every closure here is unary, matching ML's actual semantics.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
__slots__ = ("rules", "environment", "name")
|
|
103
|
+
|
|
104
|
+
def __init__(self, rules: Sequence[tuple[Pattern, Expr]],
|
|
105
|
+
environment: Environment, name: str | None = None) -> None:
|
|
106
|
+
self.rules = tuple(rules)
|
|
107
|
+
self.environment = environment
|
|
108
|
+
self.name = name
|
|
109
|
+
|
|
110
|
+
def __repr__(self) -> str:
|
|
111
|
+
return f"fn {self.name}" if self.name else "fn"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class CurriedFunction:
|
|
115
|
+
"""A multi-argument ``fun`` with several clauses, e.g.
|
|
116
|
+
|
|
117
|
+
``fun insert x [] = [x] | insert x (h::t) = ...``
|
|
118
|
+
|
|
119
|
+
ML matches the *whole argument sequence* against each clause in turn, so
|
|
120
|
+
the clause cannot be chosen from the first argument alone -- both clauses
|
|
121
|
+
above accept any ``x``. This value therefore collects arguments one at a
|
|
122
|
+
time (it is still curried: ``insert x`` is a valid partial application),
|
|
123
|
+
and only when all of them have arrived does it try the clauses.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
__slots__ = ("clauses", "environment", "name", "arity", "arguments")
|
|
127
|
+
|
|
128
|
+
def __init__(self, clauses, environment: "Environment", name: str | None,
|
|
129
|
+
arity: int, arguments: tuple = ()) -> None:
|
|
130
|
+
self.clauses = clauses
|
|
131
|
+
self.environment = environment
|
|
132
|
+
self.name = name
|
|
133
|
+
self.arity = arity
|
|
134
|
+
self.arguments = arguments
|
|
135
|
+
|
|
136
|
+
def __repr__(self) -> str:
|
|
137
|
+
return f"fn {self.name}" if self.name else "fn"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ConstructorFunction:
|
|
141
|
+
"""The value of a union constructor that expects an argument.
|
|
142
|
+
|
|
143
|
+
``colset U = union Car:CARS + ...`` makes ``Car`` a function from a ``CARS``
|
|
144
|
+
value to a ``U`` value. Applying it produces a
|
|
145
|
+
:class:`~openprocess.ml.values.Constructor`.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
__slots__ = ("name",)
|
|
149
|
+
|
|
150
|
+
def __init__(self, name: str) -> None:
|
|
151
|
+
self.name = name
|
|
152
|
+
|
|
153
|
+
def __repr__(self) -> str:
|
|
154
|
+
return f"fn {self.name}"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class Evaluator:
|
|
158
|
+
"""Evaluates CPN ML expressions.
|
|
159
|
+
|
|
160
|
+
One evaluator instance serves a whole model. It owns the global
|
|
161
|
+
environment (standard basis + model declarations) and the random number
|
|
162
|
+
generator, so seeding it makes a simulation reproducible.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def __init__(self, seed: int | None = None) -> None:
|
|
166
|
+
self.rng = random.Random(seed)
|
|
167
|
+
# ``make_builtins`` needs to call back into ``apply`` for higher-order
|
|
168
|
+
# functions like List.map, hence passing the bound method.
|
|
169
|
+
self.globals = Environment(make_builtins(self.rng, self.apply))
|
|
170
|
+
# Set by the simulator so that inscriptions can call ``intTime()``.
|
|
171
|
+
self._model_time: int = 0
|
|
172
|
+
self.globals.define("intTime", Builtin("intTime", lambda _v: self._model_time))
|
|
173
|
+
self.globals.define("time", Builtin("time", lambda _v: self._model_time))
|
|
174
|
+
# The option type: `SOME x` / `NONE` (what Int.fromString returns).
|
|
175
|
+
self.globals.define("SOME", ConstructorFunction("SOME"))
|
|
176
|
+
self.globals.define("NONE", Constructor("NONE"))
|
|
177
|
+
|
|
178
|
+
# -- integration points --------------------------------------------------
|
|
179
|
+
def set_model_time(self, value: int) -> None:
|
|
180
|
+
"""Tell inscriptions what the simulator's clock currently reads."""
|
|
181
|
+
self._model_time = value
|
|
182
|
+
|
|
183
|
+
# =======================================================================
|
|
184
|
+
# Expression evaluation
|
|
185
|
+
# =======================================================================
|
|
186
|
+
def evaluate(self, expression: Expr, environment: Environment | None = None) -> Any:
|
|
187
|
+
"""Evaluate ``expression`` in ``environment`` (globals if omitted)."""
|
|
188
|
+
env = environment if environment is not None else self.globals
|
|
189
|
+
|
|
190
|
+
if isinstance(expression, Literal):
|
|
191
|
+
return expression.value
|
|
192
|
+
|
|
193
|
+
if isinstance(expression, Var):
|
|
194
|
+
return self._lookup_identifier(expression, env)
|
|
195
|
+
|
|
196
|
+
if isinstance(expression, TupleExpr):
|
|
197
|
+
return tuple(self.evaluate(item, env) for item in expression.items)
|
|
198
|
+
|
|
199
|
+
if isinstance(expression, ListExpr):
|
|
200
|
+
return MLList(self.evaluate(item, env) for item in expression.items)
|
|
201
|
+
|
|
202
|
+
if isinstance(expression, RecordExpr):
|
|
203
|
+
return Record(tuple((name, self.evaluate(value, env)) for name, value in expression.fields))
|
|
204
|
+
|
|
205
|
+
if isinstance(expression, Selector):
|
|
206
|
+
return Builtin(f"#{expression.field_name}", lambda v, f=expression.field_name: _project(v, f))
|
|
207
|
+
|
|
208
|
+
if isinstance(expression, EmptyMultiset):
|
|
209
|
+
return Multiset.empty()
|
|
210
|
+
|
|
211
|
+
if isinstance(expression, Coefficient):
|
|
212
|
+
return self._evaluate_coefficient(expression, env)
|
|
213
|
+
|
|
214
|
+
if isinstance(expression, Delay):
|
|
215
|
+
# `tokens @+ delay`: tokens that become available `delay` time
|
|
216
|
+
# units after the transition fires (see evaluate_timed_arc).
|
|
217
|
+
base = self.evaluate(expression.base, env)
|
|
218
|
+
delay = _expect_time(self.evaluate(expression.delay, env), expression.position)
|
|
219
|
+
if isinstance(base, TimedTokens):
|
|
220
|
+
return base.delayed(delay)
|
|
221
|
+
return TimedTokens([(to_multiset(base), delay, False)])
|
|
222
|
+
|
|
223
|
+
if isinstance(expression, UnOp):
|
|
224
|
+
return self._evaluate_unary(expression, env)
|
|
225
|
+
|
|
226
|
+
if isinstance(expression, BinOp):
|
|
227
|
+
return self._evaluate_binary(expression, env)
|
|
228
|
+
|
|
229
|
+
if isinstance(expression, IfExpr):
|
|
230
|
+
condition = self.evaluate(expression.condition, env)
|
|
231
|
+
if not isinstance(condition, bool):
|
|
232
|
+
raise EvalError(
|
|
233
|
+
f"'if' condition must be a boolean, got {format_value(condition)}",
|
|
234
|
+
expression.position,
|
|
235
|
+
)
|
|
236
|
+
branch = expression.then_branch if condition else expression.else_branch
|
|
237
|
+
return self.evaluate(branch, env)
|
|
238
|
+
|
|
239
|
+
if isinstance(expression, LetExpr):
|
|
240
|
+
local = env.child()
|
|
241
|
+
for declaration in expression.declarations:
|
|
242
|
+
self.execute_declaration(declaration, local)
|
|
243
|
+
return self.evaluate(expression.body, local)
|
|
244
|
+
|
|
245
|
+
if isinstance(expression, CaseExpr):
|
|
246
|
+
subject = self.evaluate(expression.scrutinee, env)
|
|
247
|
+
for pattern, body in expression.rules:
|
|
248
|
+
bindings: dict[str, Any] = {}
|
|
249
|
+
if self.match(pattern, subject, bindings, env):
|
|
250
|
+
return self.evaluate(body, env.child(bindings))
|
|
251
|
+
raise MatchError(
|
|
252
|
+
f"no case alternative matches {format_value(subject)}", expression.position
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if isinstance(expression, FnExpr):
|
|
256
|
+
return Closure(expression.rules, env)
|
|
257
|
+
|
|
258
|
+
if isinstance(expression, App):
|
|
259
|
+
function = self.evaluate(expression.function, env)
|
|
260
|
+
argument = self.evaluate(expression.argument, env)
|
|
261
|
+
return self.apply(function, argument)
|
|
262
|
+
|
|
263
|
+
raise AssertionError(f"unhandled expression node {type(expression).__name__}")
|
|
264
|
+
|
|
265
|
+
def evaluate_arc(self, expression: Expr, environment: Environment,
|
|
266
|
+
clock: int = 0) -> tuple[Multiset, int]:
|
|
267
|
+
"""Evaluate an arc inscription to ``(tokens, timestamp)``.
|
|
268
|
+
|
|
269
|
+
The result is coerced to a multiset, so a bare ``x`` means ``1`x``
|
|
270
|
+
exactly as CPN Tools reads it. The time stamp is that of the first
|
|
271
|
+
timed term (``x @+ d`` gives ``clock + d``), or the clock itself;
|
|
272
|
+
:meth:`evaluate_timed_arc` keeps the time of every term.
|
|
273
|
+
"""
|
|
274
|
+
groups = self.evaluate_timed_arc(expression, environment, clock)
|
|
275
|
+
tokens = Multiset.empty()
|
|
276
|
+
for part, _stamp in groups:
|
|
277
|
+
tokens = tokens + part
|
|
278
|
+
return tokens, (groups[0][1] if groups else clock)
|
|
279
|
+
|
|
280
|
+
def evaluate_timed_arc(self, expression: Expr, environment: Environment,
|
|
281
|
+
clock: Any = 0) -> list[tuple[Multiset, Any]]:
|
|
282
|
+
"""Evaluate an arc inscription to ``[(tokens, time stamp), ...]``.
|
|
283
|
+
|
|
284
|
+
``1`x@+5 +++ 1`y@+3`` gives two groups, stamped ``clock + 5`` and
|
|
285
|
+
``clock + 3``; an untimed inscription gives one group stamped
|
|
286
|
+
``clock``, meaning the tokens are available immediately. Delays may
|
|
287
|
+
be real (``normal(5.0, 1.0)``); model time is kept as a number of
|
|
288
|
+
either kind, like CPN Tools' real-time setting.
|
|
289
|
+
"""
|
|
290
|
+
value = self.evaluate(expression, environment)
|
|
291
|
+
if isinstance(value, TimedTokens):
|
|
292
|
+
return value.stamped(clock)
|
|
293
|
+
return [(to_multiset(value), clock)]
|
|
294
|
+
|
|
295
|
+
# -- identifier resolution ----------------------------------------------
|
|
296
|
+
def _lookup_identifier(self, node: Var, environment: Environment) -> Any:
|
|
297
|
+
try:
|
|
298
|
+
return environment.lookup(node.name)
|
|
299
|
+
except KeyError:
|
|
300
|
+
pass
|
|
301
|
+
# An unbound capitalised identifier is a nullary constructor. This
|
|
302
|
+
# keeps models loading even when a colour set was declared in a block
|
|
303
|
+
# we could not fully compile.
|
|
304
|
+
if node.name[:1].isupper():
|
|
305
|
+
return Constructor(node.name)
|
|
306
|
+
raise EvalError(f"unbound identifier '{node.name}'", node.position)
|
|
307
|
+
|
|
308
|
+
# -- operators -----------------------------------------------------------
|
|
309
|
+
def _evaluate_coefficient(self, node: Coefficient, environment: Environment) -> Multiset:
|
|
310
|
+
count = self.evaluate(node.count, environment)
|
|
311
|
+
if isinstance(count, bool) or not isinstance(count, int):
|
|
312
|
+
raise EvalError(
|
|
313
|
+
f"multiset coefficient must be an integer, got {format_value(count)}",
|
|
314
|
+
node.position,
|
|
315
|
+
)
|
|
316
|
+
if count < 0:
|
|
317
|
+
raise EvalError("multiset coefficient must not be negative", node.position)
|
|
318
|
+
value = self.evaluate(node.value, environment)
|
|
319
|
+
if isinstance(value, Multiset):
|
|
320
|
+
# `2`(1`x ++ 1`y)` scales an existing multiset.
|
|
321
|
+
return value * count
|
|
322
|
+
if isinstance(value, TimedTokens):
|
|
323
|
+
return value.scaled(count)
|
|
324
|
+
return Multiset.singleton(value, count)
|
|
325
|
+
|
|
326
|
+
def _evaluate_unary(self, node: UnOp, environment: Environment) -> Any:
|
|
327
|
+
operand = self.evaluate(node.operand, environment)
|
|
328
|
+
if node.operator == "~":
|
|
329
|
+
if isinstance(operand, bool) or not isinstance(operand, (int, float)):
|
|
330
|
+
raise EvalError(f"'~' expects a number, got {format_value(operand)}", node.position)
|
|
331
|
+
return -operand
|
|
332
|
+
if node.operator == "not":
|
|
333
|
+
if not isinstance(operand, bool):
|
|
334
|
+
raise EvalError(f"'not' expects a boolean, got {format_value(operand)}", node.position)
|
|
335
|
+
return not operand
|
|
336
|
+
raise AssertionError(f"unhandled unary operator {node.operator!r}")
|
|
337
|
+
|
|
338
|
+
def _evaluate_binary(self, node: BinOp, environment: Environment) -> Any:
|
|
339
|
+
operator = node.operator
|
|
340
|
+
|
|
341
|
+
# Short-circuit operators must not evaluate the right side eagerly.
|
|
342
|
+
if operator in ("andalso", "orelse"):
|
|
343
|
+
left = self.evaluate(node.left, environment)
|
|
344
|
+
if not isinstance(left, bool):
|
|
345
|
+
raise EvalError(
|
|
346
|
+
f"'{operator}' expects booleans, got {format_value(left)}", node.position
|
|
347
|
+
)
|
|
348
|
+
if operator == "andalso" and not left:
|
|
349
|
+
return False
|
|
350
|
+
if operator == "orelse" and left:
|
|
351
|
+
return True
|
|
352
|
+
right = self.evaluate(node.right, environment)
|
|
353
|
+
if not isinstance(right, bool):
|
|
354
|
+
raise EvalError(
|
|
355
|
+
f"'{operator}' expects booleans, got {format_value(right)}", node.position
|
|
356
|
+
)
|
|
357
|
+
return right
|
|
358
|
+
|
|
359
|
+
left = self.evaluate(node.left, environment)
|
|
360
|
+
right = self.evaluate(node.right, environment)
|
|
361
|
+
return self._apply_binary(operator, left, right, node.position)
|
|
362
|
+
|
|
363
|
+
def _apply_binary(self, operator: str, left: Any, right: Any, position: int) -> Any:
|
|
364
|
+
# -- equality and ordering ------------------------------------------
|
|
365
|
+
if operator == "=":
|
|
366
|
+
return left == right
|
|
367
|
+
if operator == "<>":
|
|
368
|
+
return left != right
|
|
369
|
+
if operator in ("<", ">", "<=", ">="):
|
|
370
|
+
if isinstance(left, Multiset) and isinstance(right, Multiset):
|
|
371
|
+
# On multisets these are the inclusion orders, which is what a
|
|
372
|
+
# guard like `m <= M(p)` means.
|
|
373
|
+
return {"<": left < right, ">": left > right,
|
|
374
|
+
"<=": left <= right, ">=": left >= right}[operator]
|
|
375
|
+
try:
|
|
376
|
+
left_key, right_key = sort_key(left), sort_key(right)
|
|
377
|
+
except AssertionError:
|
|
378
|
+
raise EvalError(f"cannot compare {format_value(left)} and {format_value(right)}", position)
|
|
379
|
+
return {"<": left_key < right_key, ">": left_key > right_key,
|
|
380
|
+
"<=": left_key <= right_key, ">=": left_key >= right_key}[operator]
|
|
381
|
+
|
|
382
|
+
# -- timed multisets --------------------------------------------------
|
|
383
|
+
if operator == "+++" or (operator == "++" and (isinstance(left, TimedTokens)
|
|
384
|
+
or isinstance(right, TimedTokens))):
|
|
385
|
+
return _as_timed(left) + _as_timed(right)
|
|
386
|
+
if operator == "---":
|
|
387
|
+
# Removing timed tokens: like `--`, regardless of their times.
|
|
388
|
+
return to_multiset(left) - to_multiset(right)
|
|
389
|
+
|
|
390
|
+
# -- multiset algebra ------------------------------------------------
|
|
391
|
+
if operator in ("++", "--"):
|
|
392
|
+
# In CPN ML a multiset *is* a list ('a ms = 'a list), so `++` on
|
|
393
|
+
# two lists is list concatenation and the result is one list
|
|
394
|
+
# value: `lp ++ [p]` appends p to the queue lp.
|
|
395
|
+
if operator == "++" and isinstance(left, MLList) and isinstance(right, MLList):
|
|
396
|
+
return left.append(right)
|
|
397
|
+
# Otherwise coerce bare values to singletons: writing `x ++ y` on
|
|
398
|
+
# an arc is common shorthand for `1`x ++ 1`y`.
|
|
399
|
+
left_ms, right_ms = to_multiset(left), to_multiset(right)
|
|
400
|
+
return left_ms + right_ms if operator == "++" else left_ms - right_ms
|
|
401
|
+
|
|
402
|
+
# -- lists -----------------------------------------------------------
|
|
403
|
+
if operator == "::":
|
|
404
|
+
if not isinstance(right, MLList):
|
|
405
|
+
raise EvalError(f"'::' expects a list on the right, got {format_value(right)}", position)
|
|
406
|
+
return right.cons(left)
|
|
407
|
+
if operator == "@" and not isinstance(right, bool) and isinstance(right, (int, float)):
|
|
408
|
+
# `1`x@5`: tokens with the time stamp 5 (timed initial markings).
|
|
409
|
+
if isinstance(left, TimedTokens):
|
|
410
|
+
return TimedTokens((tokens, right, True) for tokens, _t, _a in left.groups)
|
|
411
|
+
return TimedTokens([(to_multiset(left), right, True)])
|
|
412
|
+
if operator in ("@", "^^"):
|
|
413
|
+
if isinstance(left, MLList) and isinstance(right, MLList):
|
|
414
|
+
return left.append(right)
|
|
415
|
+
if isinstance(left, str) and isinstance(right, str):
|
|
416
|
+
# CPN Tools also uses ^^ for string concatenation in places.
|
|
417
|
+
return left + right
|
|
418
|
+
hint = (" (join timed tokens with +++, e.g. 1`x@0 +++ 1`y@5)"
|
|
419
|
+
if operator == "@" and isinstance(right, Multiset) else "")
|
|
420
|
+
raise EvalError(f"'{operator}' expects two lists, or a time stamp after '@'{hint}",
|
|
421
|
+
position)
|
|
422
|
+
|
|
423
|
+
# -- strings ----------------------------------------------------------
|
|
424
|
+
if operator == "^":
|
|
425
|
+
if isinstance(left, str) and isinstance(right, str):
|
|
426
|
+
return left + right
|
|
427
|
+
raise EvalError(f"'^' expects two strings, got {format_value(left)} and {format_value(right)}", position)
|
|
428
|
+
|
|
429
|
+
# -- arithmetic --------------------------------------------------------
|
|
430
|
+
if operator in ("+", "-", "*", "/", "div", "mod"):
|
|
431
|
+
return self._arithmetic(operator, left, right, position)
|
|
432
|
+
|
|
433
|
+
raise AssertionError(f"unhandled binary operator {operator!r}")
|
|
434
|
+
|
|
435
|
+
@staticmethod
|
|
436
|
+
def _arithmetic(operator: str, left: Any, right: Any, position: int) -> Any:
|
|
437
|
+
for operand in (left, right):
|
|
438
|
+
if isinstance(operand, bool) or not isinstance(operand, (int, float)):
|
|
439
|
+
raise EvalError(
|
|
440
|
+
f"'{operator}' expects numbers, got {format_value(left)} and {format_value(right)}",
|
|
441
|
+
position,
|
|
442
|
+
)
|
|
443
|
+
if operator == "+":
|
|
444
|
+
return left + right
|
|
445
|
+
if operator == "-":
|
|
446
|
+
return left - right
|
|
447
|
+
if operator == "*":
|
|
448
|
+
return left * right
|
|
449
|
+
if operator == "/":
|
|
450
|
+
if float(right) == 0.0:
|
|
451
|
+
raise EvalError("division by zero", position)
|
|
452
|
+
return float(left) / float(right)
|
|
453
|
+
if right == 0:
|
|
454
|
+
raise EvalError(f"'{operator}' by zero", position)
|
|
455
|
+
if operator == "div":
|
|
456
|
+
# ML's `div` truncates towards negative infinity, like Python's //.
|
|
457
|
+
return left // right
|
|
458
|
+
return left % right
|
|
459
|
+
|
|
460
|
+
# -- application ---------------------------------------------------------
|
|
461
|
+
def apply(self, function: Any, argument: Any) -> Any:
|
|
462
|
+
"""Apply a function value to one argument."""
|
|
463
|
+
if isinstance(function, Closure):
|
|
464
|
+
for pattern, body in function.rules:
|
|
465
|
+
bindings: dict[str, Any] = {}
|
|
466
|
+
if self.match(pattern, argument, bindings, function.environment):
|
|
467
|
+
return self.evaluate(body, function.environment.child(bindings))
|
|
468
|
+
raise MatchError(
|
|
469
|
+
f"no clause of {function.name or 'this function'} matches "
|
|
470
|
+
f"{format_value(argument)}"
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
if isinstance(function, CurriedFunction):
|
|
474
|
+
arguments = function.arguments + (argument,)
|
|
475
|
+
if len(arguments) < function.arity:
|
|
476
|
+
return CurriedFunction(function.clauses, function.environment, function.name,
|
|
477
|
+
function.arity, arguments)
|
|
478
|
+
for parameters, body in function.clauses:
|
|
479
|
+
bindings: dict[str, Any] = {}
|
|
480
|
+
if all(self.match(pattern, value, bindings, function.environment)
|
|
481
|
+
for pattern, value in zip(parameters, arguments)):
|
|
482
|
+
return self.evaluate(body, function.environment.child(bindings))
|
|
483
|
+
shown = " ".join(format_value(a) for a in arguments)
|
|
484
|
+
raise MatchError(f"no clause of {function.name or 'this function'} matches {shown}")
|
|
485
|
+
|
|
486
|
+
if isinstance(function, ConstructorFunction):
|
|
487
|
+
return Constructor(function.name, argument)
|
|
488
|
+
|
|
489
|
+
if isinstance(function, Builtin):
|
|
490
|
+
if function.arity == 1:
|
|
491
|
+
return function.function(argument)
|
|
492
|
+
return self.apply(PartialApplication(function, ()), argument)
|
|
493
|
+
|
|
494
|
+
if isinstance(function, PartialApplication):
|
|
495
|
+
arguments = function.arguments + (argument,)
|
|
496
|
+
if len(arguments) == function.builtin.arity:
|
|
497
|
+
return function.builtin.function(*arguments)
|
|
498
|
+
return PartialApplication(function.builtin, arguments)
|
|
499
|
+
|
|
500
|
+
raise EvalError(f"{format_value(function) if not callable(function) else function} is not a function")
|
|
501
|
+
|
|
502
|
+
# =======================================================================
|
|
503
|
+
# Pattern matching
|
|
504
|
+
# =======================================================================
|
|
505
|
+
def match(self, pattern: Pattern, value: Any, bindings: dict[str, Any],
|
|
506
|
+
environment: Environment) -> bool:
|
|
507
|
+
"""Try to match ``value`` against ``pattern``.
|
|
508
|
+
|
|
509
|
+
On success, fills ``bindings`` with the variables the pattern binds and
|
|
510
|
+
returns ``True``. On failure returns ``False`` -- ``bindings`` may have
|
|
511
|
+
been partially written, so callers must not reuse a failed dictionary.
|
|
512
|
+
|
|
513
|
+
A variable that is *already* in ``bindings`` acts as an equality test
|
|
514
|
+
rather than a fresh binding. This is what makes a non-linear arc
|
|
515
|
+
inscription such as ``(x, x)`` behave correctly, and it is also how the
|
|
516
|
+
binder enforces consistency of one variable across several input arcs.
|
|
517
|
+
"""
|
|
518
|
+
if isinstance(pattern, PWildcard):
|
|
519
|
+
return True
|
|
520
|
+
|
|
521
|
+
if isinstance(pattern, PVar):
|
|
522
|
+
existing = bindings.get(pattern.name, _MISSING)
|
|
523
|
+
if existing is _MISSING:
|
|
524
|
+
bindings[pattern.name] = value
|
|
525
|
+
return True
|
|
526
|
+
return bool(existing == value)
|
|
527
|
+
|
|
528
|
+
if isinstance(pattern, PLiteral):
|
|
529
|
+
return bool(pattern.value == value)
|
|
530
|
+
|
|
531
|
+
if isinstance(pattern, PAs):
|
|
532
|
+
if not self.match(pattern.pattern, value, bindings, environment):
|
|
533
|
+
return False
|
|
534
|
+
existing = bindings.get(pattern.name, _MISSING)
|
|
535
|
+
if existing is _MISSING:
|
|
536
|
+
bindings[pattern.name] = value
|
|
537
|
+
return True
|
|
538
|
+
return bool(existing == value)
|
|
539
|
+
|
|
540
|
+
if isinstance(pattern, PTuple):
|
|
541
|
+
if not isinstance(value, tuple) or len(value) != len(pattern.items):
|
|
542
|
+
return False
|
|
543
|
+
return all(
|
|
544
|
+
self.match(sub, item, bindings, environment)
|
|
545
|
+
for sub, item in zip(pattern.items, value)
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
if isinstance(pattern, PList):
|
|
549
|
+
if not isinstance(value, MLList) or len(value) != len(pattern.items):
|
|
550
|
+
return False
|
|
551
|
+
return all(
|
|
552
|
+
self.match(sub, item, bindings, environment)
|
|
553
|
+
for sub, item in zip(pattern.items, value)
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
if isinstance(pattern, PCons):
|
|
557
|
+
if not isinstance(value, MLList) or len(value) == 0:
|
|
558
|
+
return False
|
|
559
|
+
if not self.match(pattern.head, value[0], bindings, environment):
|
|
560
|
+
return False
|
|
561
|
+
return self.match(pattern.tail, MLList(value.items[1:]), bindings, environment)
|
|
562
|
+
|
|
563
|
+
if isinstance(pattern, PRecord):
|
|
564
|
+
if not isinstance(value, Record):
|
|
565
|
+
return False
|
|
566
|
+
if not pattern.open_ended and len(pattern.fields) != len(value.fields):
|
|
567
|
+
return False
|
|
568
|
+
for name, sub in pattern.fields:
|
|
569
|
+
try:
|
|
570
|
+
field_value = value.get(name)
|
|
571
|
+
except KeyError:
|
|
572
|
+
return False
|
|
573
|
+
if not self.match(sub, field_value, bindings, environment):
|
|
574
|
+
return False
|
|
575
|
+
return True
|
|
576
|
+
|
|
577
|
+
if isinstance(pattern, PConstructor):
|
|
578
|
+
return self._match_constructor(pattern, value, bindings, environment)
|
|
579
|
+
|
|
580
|
+
raise AssertionError(f"unhandled pattern node {type(pattern).__name__}")
|
|
581
|
+
|
|
582
|
+
def _match_constructor(self, pattern: PConstructor, value: Any,
|
|
583
|
+
bindings: dict[str, Any], environment: Environment) -> bool:
|
|
584
|
+
"""Match ``Tag`` or ``Tag p``.
|
|
585
|
+
|
|
586
|
+
A bare capitalised identifier is genuinely ambiguous in CPN ML: it can
|
|
587
|
+
be a nullary constructor *or* a variable that the modeller chose to
|
|
588
|
+
capitalise. We resolve it the only way that is always safe: if the
|
|
589
|
+
name is bound in the environment to a constructor, treat it as a
|
|
590
|
+
constant; otherwise treat it as a variable binding.
|
|
591
|
+
"""
|
|
592
|
+
if pattern.argument is None:
|
|
593
|
+
if environment.contains(pattern.name):
|
|
594
|
+
bound = environment.lookup(pattern.name)
|
|
595
|
+
if isinstance(bound, Constructor):
|
|
596
|
+
return bool(bound == value)
|
|
597
|
+
if isinstance(value, Constructor) and not value.has_argument \
|
|
598
|
+
and value.name == pattern.name:
|
|
599
|
+
return True
|
|
600
|
+
# Fall back to variable semantics.
|
|
601
|
+
existing = bindings.get(pattern.name, _MISSING)
|
|
602
|
+
if existing is _MISSING:
|
|
603
|
+
bindings[pattern.name] = value
|
|
604
|
+
return True
|
|
605
|
+
return bool(existing == value)
|
|
606
|
+
|
|
607
|
+
if not isinstance(value, Constructor) or value.name != pattern.name:
|
|
608
|
+
return False
|
|
609
|
+
if not value.has_argument:
|
|
610
|
+
return False
|
|
611
|
+
return self.match(pattern.argument, value.argument, bindings, environment)
|
|
612
|
+
|
|
613
|
+
# =======================================================================
|
|
614
|
+
# Declarations
|
|
615
|
+
# =======================================================================
|
|
616
|
+
def execute_declaration(self, declaration: Decl, environment: Environment) -> None:
|
|
617
|
+
"""Run a ``val`` or ``fun`` declaration, updating ``environment``."""
|
|
618
|
+
if isinstance(declaration, ValDecl):
|
|
619
|
+
value = self.evaluate(declaration.expression, environment)
|
|
620
|
+
bindings: dict[str, Any] = {}
|
|
621
|
+
if not self.match(declaration.pattern, value, bindings, environment):
|
|
622
|
+
raise MatchError(
|
|
623
|
+
f"value {format_value(value)} does not match the pattern in this "
|
|
624
|
+
"'val' declaration",
|
|
625
|
+
declaration.position,
|
|
626
|
+
)
|
|
627
|
+
environment.bindings.update(bindings)
|
|
628
|
+
return
|
|
629
|
+
|
|
630
|
+
if isinstance(declaration, FunDecl):
|
|
631
|
+
self._define_function(declaration, environment)
|
|
632
|
+
return
|
|
633
|
+
|
|
634
|
+
raise AssertionError(f"unhandled declaration node {type(declaration).__name__}")
|
|
635
|
+
|
|
636
|
+
def _define_function(self, declaration: FunDecl, environment: Environment) -> None:
|
|
637
|
+
"""Bind a ``fun`` declaration, desugaring currying and enabling recursion.
|
|
638
|
+
|
|
639
|
+
``fun f x y = body`` becomes a closure over ``x`` whose body is another
|
|
640
|
+
closure over ``y``. We build the outer closure first, bind the name,
|
|
641
|
+
then let the closure's captured environment see that binding -- which
|
|
642
|
+
is what makes ``f`` visible inside its own body.
|
|
643
|
+
"""
|
|
644
|
+
arity = len(declaration.clauses[0][0])
|
|
645
|
+
if any(len(parameters) != arity for parameters, _ in declaration.clauses):
|
|
646
|
+
raise EvalError(
|
|
647
|
+
f"all clauses of '{declaration.name}' must take the same number of arguments",
|
|
648
|
+
declaration.position,
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
if arity == 1:
|
|
652
|
+
rules = tuple((parameters[0], body) for parameters, body in declaration.clauses)
|
|
653
|
+
closure = Closure(rules, environment, declaration.name)
|
|
654
|
+
elif len(declaration.clauses) == 1:
|
|
655
|
+
# A single clause: nested ``fn``s are exact and cheapest.
|
|
656
|
+
parameters, body = declaration.clauses[0]
|
|
657
|
+
closure = Closure(((parameters[0], _nest_fn(parameters[1:], body)),),
|
|
658
|
+
environment, declaration.name)
|
|
659
|
+
else:
|
|
660
|
+
# Several clauses: match all arguments together (see CurriedFunction).
|
|
661
|
+
closure = CurriedFunction(tuple(declaration.clauses), environment,
|
|
662
|
+
declaration.name, arity)
|
|
663
|
+
|
|
664
|
+
# Bind before evaluation so the closure can call itself.
|
|
665
|
+
environment.define(declaration.name, closure)
|
|
666
|
+
|
|
667
|
+
# =======================================================================
|
|
668
|
+
# Convenience
|
|
669
|
+
# =======================================================================
|
|
670
|
+
def run_declarations(self, declarations: Iterable[Decl],
|
|
671
|
+
environment: Environment | None = None) -> None:
|
|
672
|
+
env = environment if environment is not None else self.globals
|
|
673
|
+
for declaration in declarations:
|
|
674
|
+
self.execute_declaration(declaration, env)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
# Sentinel for "no such binding", so that ``None`` remains a legal bound value.
|
|
678
|
+
_MISSING = object()
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _nest_fn(parameters: Sequence[Pattern], body: Expr) -> Expr:
|
|
682
|
+
"""Build ``fn p1 => fn p2 => ... => body`` for a curried function."""
|
|
683
|
+
result = body
|
|
684
|
+
for pattern in reversed(parameters):
|
|
685
|
+
result = FnExpr(((pattern, result),), getattr(pattern, "position", 0))
|
|
686
|
+
return result
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _project(value: Any, field_name: str) -> Any:
|
|
690
|
+
"""Implement ``#field`` / ``#n`` projection for records and tuples."""
|
|
691
|
+
if isinstance(value, Record):
|
|
692
|
+
try:
|
|
693
|
+
return value.get(field_name)
|
|
694
|
+
except KeyError:
|
|
695
|
+
raise EvalError(f"record has no field '{field_name}'")
|
|
696
|
+
if isinstance(value, tuple) and field_name.isdigit():
|
|
697
|
+
index = int(field_name) - 1 # ML tuple components are 1-based.
|
|
698
|
+
if 0 <= index < len(value):
|
|
699
|
+
return value[index]
|
|
700
|
+
raise EvalError(f"tuple has no component #{field_name}")
|
|
701
|
+
raise EvalError(f"cannot select #{field_name} from {format_value(value)}")
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def expand_lists(tokens: Multiset, colour_set) -> Multiset:
|
|
705
|
+
"""CPN Tools' list-to-multiset coercion.
|
|
706
|
+
|
|
707
|
+
When an initial marking or arc expression evaluates to a *list* but the
|
|
708
|
+
place's colour set is not a list colour set, CPN Tools reads the list as
|
|
709
|
+
a multiset: one token per element. ``PLANE_INIT`` (a list of seats) on a
|
|
710
|
+
``SEAT`` place therefore means "one token per seat", not one list token.
|
|
711
|
+
Tokens that already belong to the colour set are left alone.
|
|
712
|
+
"""
|
|
713
|
+
if colour_set is None:
|
|
714
|
+
return tokens
|
|
715
|
+
from .values import MLList
|
|
716
|
+
result = Multiset.empty()
|
|
717
|
+
changed = False
|
|
718
|
+
for token, count in tokens.items():
|
|
719
|
+
if isinstance(token, MLList) and not colour_set.contains(token):
|
|
720
|
+
for item in token.items:
|
|
721
|
+
result = result + Multiset.singleton(item, count)
|
|
722
|
+
changed = True
|
|
723
|
+
else:
|
|
724
|
+
result = result + Multiset.singleton(token, count)
|
|
725
|
+
return result if changed else tokens
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def to_multiset(value: Any) -> Multiset:
|
|
729
|
+
"""Coerce an arc-expression result to a multiset.
|
|
730
|
+
|
|
731
|
+
CPN Tools treats a bare colour expression on an arc as one token of that
|
|
732
|
+
colour, so ``x`` and ``1`x`` mean the same thing. We implement that
|
|
733
|
+
coercion here, in one place, rather than scattering it through the
|
|
734
|
+
simulator. Timed tokens lose their times (an input arc, for example,
|
|
735
|
+
takes tokens regardless of their stamps).
|
|
736
|
+
"""
|
|
737
|
+
if isinstance(value, Multiset):
|
|
738
|
+
return value
|
|
739
|
+
if isinstance(value, TimedTokens):
|
|
740
|
+
return value.untimed()
|
|
741
|
+
return Multiset.singleton(value, 1)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def _as_timed(value: Any) -> TimedTokens:
|
|
745
|
+
"""An operand of ``+++``: untimed tokens are available at once (``@+0``)."""
|
|
746
|
+
if isinstance(value, TimedTokens):
|
|
747
|
+
return value
|
|
748
|
+
return TimedTokens([(to_multiset(value), 0, False)])
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def _expect_time(value: Any, position: int) -> Any:
|
|
752
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
753
|
+
raise EvalError(f"time delay must be a number, got {format_value(value)}", position)
|
|
754
|
+
return value
|