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,617 @@
|
|
|
1
|
+
"""Parsing and compiling a model's declaration block.
|
|
2
|
+
|
|
3
|
+
A CPN model's declarations live in what CPN Tools calls the *global box*
|
|
4
|
+
(``<globbox>`` in the file format). They come in four kinds:
|
|
5
|
+
|
|
6
|
+
``colset`` -- a colour set (a type)
|
|
7
|
+
``colset PACKET = product INT * STRING timed;``
|
|
8
|
+
``var`` -- a typed variable usable in inscriptions
|
|
9
|
+
``var p : PACKET; var n, m : INT;``
|
|
10
|
+
``val`` / ``fun`` -- ordinary ML value and function declarations
|
|
11
|
+
``val limit = 10; fun next n = n + 1;``
|
|
12
|
+
``globref`` -- a mutable global reference
|
|
13
|
+
``globref counter = 0;``
|
|
14
|
+
|
|
15
|
+
This module turns that text into:
|
|
16
|
+
|
|
17
|
+
* a **colour set registry**: name -> :class:`~openprocess.ml.colorsets.ColourSet`;
|
|
18
|
+
* a **variable table**: name -> the colour set it ranges over (the binder needs
|
|
19
|
+
this to know what to enumerate);
|
|
20
|
+
* **global environment entries**: enumeration constants, union constructors,
|
|
21
|
+
and any ``val``/``fun`` bindings, all installed into the evaluator.
|
|
22
|
+
|
|
23
|
+
Order matters -- a colour set may refer to earlier ones -- so declarations are
|
|
24
|
+
compiled in the order they appear, and a forward reference is an error with a
|
|
25
|
+
message naming the missing colour set.
|
|
26
|
+
|
|
27
|
+
Why a separate parser
|
|
28
|
+
---------------------
|
|
29
|
+
Declaration syntax is *not* ML expression syntax: ``product A * B`` and
|
|
30
|
+
``union C1:A + C2`` are CPN Tools' own notation. We reuse the ML
|
|
31
|
+
:func:`~openprocess.ml.lexer.tokenise` for the token stream, then apply a small
|
|
32
|
+
purpose-built grammar here.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from dataclasses import dataclass, field
|
|
38
|
+
from typing import Any, Sequence
|
|
39
|
+
|
|
40
|
+
from ..ml.colorsets import (
|
|
41
|
+
AliasColourSet, BoolColourSet, ColourSet, EnumColourSet, IndexColourSet,
|
|
42
|
+
IntColourSet, IntInfColourSet, ListColourSet, ProductColourSet,
|
|
43
|
+
RealColourSet, RecordColourSet, StringColourSet, SubsetColourSet,
|
|
44
|
+
UnionColourSet, UnitColourSet, standard_colour_sets,
|
|
45
|
+
)
|
|
46
|
+
from ..ml.errors import CPNMLError, ParseError
|
|
47
|
+
from ..ml.evaluator import ConstructorFunction, Evaluator
|
|
48
|
+
from ..ml.lexer import Token, tokenise
|
|
49
|
+
from ..ml.parser import Parser, parse_declarations
|
|
50
|
+
from ..ml.values import Constructor
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class VariableDeclaration:
|
|
55
|
+
"""``var name : COLOURSET;`` -- one variable usable in inscriptions."""
|
|
56
|
+
|
|
57
|
+
name: str
|
|
58
|
+
colour_set_name: str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class DeclarationBlock:
|
|
63
|
+
"""The parsed contents of a model's global box.
|
|
64
|
+
|
|
65
|
+
Kept as ordered lists of *source text* alongside the compiled results, so
|
|
66
|
+
that saving a model reproduces exactly what the modeller wrote rather than
|
|
67
|
+
a pretty-printed approximation.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
#: ``(name, source_text)`` for every ``colset`` declaration, in order.
|
|
71
|
+
colour_set_sources: list[tuple[str, str]] = field(default_factory=list)
|
|
72
|
+
#: ``(name, colour_set_name, source_text)`` for every ``var`` declaration.
|
|
73
|
+
variable_sources: list[tuple[str, str, str]] = field(default_factory=list)
|
|
74
|
+
#: Raw ``val`` / ``fun`` declaration text, in order.
|
|
75
|
+
ml_sources: list[str] = field(default_factory=list)
|
|
76
|
+
#: ``(name, initial_expression_text)`` for every ``globref``.
|
|
77
|
+
globref_sources: list[tuple[str, str]] = field(default_factory=list)
|
|
78
|
+
|
|
79
|
+
# Filled in by :meth:`compile`.
|
|
80
|
+
colour_sets: dict[str, ColourSet] = field(default_factory=dict)
|
|
81
|
+
variables: dict[str, str] = field(default_factory=dict)
|
|
82
|
+
|
|
83
|
+
# -- compilation ---------------------------------------------------------
|
|
84
|
+
def compile(self, evaluator: Evaluator) -> None:
|
|
85
|
+
"""Build the colour sets and install everything into ``evaluator``.
|
|
86
|
+
|
|
87
|
+
Called once when a model is loaded and again whenever the modeller
|
|
88
|
+
edits a declaration. It is idempotent: the registry is rebuilt from
|
|
89
|
+
the standard colour sets each time, so removing a declaration really
|
|
90
|
+
removes it.
|
|
91
|
+
"""
|
|
92
|
+
from ..ml.parser import parse_expression
|
|
93
|
+
self.colour_sets = standard_colour_sets()
|
|
94
|
+
self.variables = {}
|
|
95
|
+
|
|
96
|
+
for colour_set in self.colour_sets.values(): # INT.all () ...
|
|
97
|
+
_install_colour_set_functions(colour_set, evaluator)
|
|
98
|
+
|
|
99
|
+
# Colour sets and ML declarations may depend on each other in either
|
|
100
|
+
# direction: `val n = 5; colset N = int with 1..n;`, a subset by a
|
|
101
|
+
# predicate function, or `val phs = PH.all ();`. The three lists do
|
|
102
|
+
# not record how the modeller interleaved them, so compile in passes:
|
|
103
|
+
# each pass compiles whatever it can, and a declaration that fails
|
|
104
|
+
# waits for the next pass. Only when a whole pass makes no progress
|
|
105
|
+
# is the first remaining problem reported.
|
|
106
|
+
pending_sets = list(self.colour_sets_in_order())
|
|
107
|
+
pending_ml = list(self.ml_sources)
|
|
108
|
+
pending_refs = list(self.globref_sources)
|
|
109
|
+
while pending_sets or pending_ml or pending_refs:
|
|
110
|
+
problems: list[CPNMLError] = []
|
|
111
|
+
progress = False
|
|
112
|
+
|
|
113
|
+
waiting_sets = []
|
|
114
|
+
for name, source in pending_sets:
|
|
115
|
+
try:
|
|
116
|
+
colour_set = parse_colour_set_declaration(source, self.colour_sets, evaluator)
|
|
117
|
+
except CPNMLError as problem:
|
|
118
|
+
waiting_sets.append((name, source))
|
|
119
|
+
# Say which declaration failed: positions are relative to it.
|
|
120
|
+
problems.append(type(problem)(f"colset {name}: {problem.message}",
|
|
121
|
+
problem.position))
|
|
122
|
+
continue
|
|
123
|
+
self.colour_sets[colour_set.name] = colour_set
|
|
124
|
+
_install_constructors(colour_set, evaluator)
|
|
125
|
+
_install_colour_set_functions(colour_set, evaluator)
|
|
126
|
+
progress = True
|
|
127
|
+
pending_sets = waiting_sets
|
|
128
|
+
|
|
129
|
+
waiting_ml = []
|
|
130
|
+
for source in pending_ml:
|
|
131
|
+
try:
|
|
132
|
+
evaluator.run_declarations(parse_declarations(source))
|
|
133
|
+
except CPNMLError as problem:
|
|
134
|
+
waiting_ml.append(source)
|
|
135
|
+
problems.append(problem)
|
|
136
|
+
continue
|
|
137
|
+
progress = True
|
|
138
|
+
pending_ml = waiting_ml
|
|
139
|
+
|
|
140
|
+
waiting_refs = []
|
|
141
|
+
for name, initial in pending_refs:
|
|
142
|
+
try:
|
|
143
|
+
evaluator.globals.define(name, evaluator.evaluate(parse_expression(initial)))
|
|
144
|
+
except CPNMLError as problem:
|
|
145
|
+
waiting_refs.append((name, initial))
|
|
146
|
+
problems.append(type(problem)(f"globref {name}: {problem.message}",
|
|
147
|
+
problem.position))
|
|
148
|
+
continue
|
|
149
|
+
progress = True
|
|
150
|
+
pending_refs = waiting_refs
|
|
151
|
+
|
|
152
|
+
if not progress:
|
|
153
|
+
raise problems[0]
|
|
154
|
+
|
|
155
|
+
for name, colour_set_name, _source in self.variable_sources:
|
|
156
|
+
if colour_set_name not in self.colour_sets:
|
|
157
|
+
raise CPNMLError(
|
|
158
|
+
f"variable '{name}' is declared with unknown colour set "
|
|
159
|
+
f"'{colour_set_name}'"
|
|
160
|
+
)
|
|
161
|
+
self.variables[name] = colour_set_name
|
|
162
|
+
|
|
163
|
+
def colour_sets_in_order(self) -> list[tuple[str, str]]:
|
|
164
|
+
return list(self.colour_set_sources)
|
|
165
|
+
|
|
166
|
+
def variable_colour_set(self, name: str) -> ColourSet | None:
|
|
167
|
+
"""The colour set a variable ranges over, or ``None`` if undeclared."""
|
|
168
|
+
colour_set_name = self.variables.get(name)
|
|
169
|
+
if colour_set_name is None:
|
|
170
|
+
return None
|
|
171
|
+
return self.colour_sets.get(colour_set_name)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _install_constructors(colour_set: ColourSet, evaluator: Evaluator) -> None:
|
|
175
|
+
"""Make a colour set's constants and constructors usable in inscriptions.
|
|
176
|
+
|
|
177
|
+
* enumeration constants (``red``) become bound values;
|
|
178
|
+
* index tags (``proc``) and union tags with a payload become functions;
|
|
179
|
+
* nullary union tags become bound values, like enumeration constants.
|
|
180
|
+
|
|
181
|
+
Without this step an inscription mentioning ``red`` would fail with
|
|
182
|
+
"unbound identifier".
|
|
183
|
+
"""
|
|
184
|
+
if isinstance(colour_set, EnumColourSet):
|
|
185
|
+
for constant in colour_set.constants:
|
|
186
|
+
evaluator.globals.define(constant, Constructor(constant))
|
|
187
|
+
elif isinstance(colour_set, IndexColourSet):
|
|
188
|
+
evaluator.globals.define(colour_set.tag, ConstructorFunction(colour_set.tag))
|
|
189
|
+
elif isinstance(colour_set, UnionColourSet):
|
|
190
|
+
for tag, payload in colour_set.variants:
|
|
191
|
+
if payload is None:
|
|
192
|
+
evaluator.globals.define(tag, Constructor(tag))
|
|
193
|
+
else:
|
|
194
|
+
evaluator.globals.define(tag, ConstructorFunction(tag))
|
|
195
|
+
elif isinstance(colour_set, UnitColourSet) and colour_set.alias:
|
|
196
|
+
from ..ml.values import UNIT
|
|
197
|
+
evaluator.globals.define(colour_set.alias, UNIT)
|
|
198
|
+
elif isinstance(colour_set, BoolColourSet):
|
|
199
|
+
if colour_set.false_alias:
|
|
200
|
+
evaluator.globals.define(colour_set.false_alias, False)
|
|
201
|
+
if colour_set.true_alias:
|
|
202
|
+
evaluator.globals.define(colour_set.true_alias, True)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _install_colour_set_functions(colour_set: ColourSet, evaluator: Evaluator) -> None:
|
|
206
|
+
"""CPN Tools' colour set functions: ``PH.all()``, ``PH.ran()`` and friends.
|
|
207
|
+
|
|
208
|
+
Every colour set ``CS`` comes with
|
|
209
|
+
|
|
210
|
+
``CS.all ()`` the multiset with one of each colour (finite sets only)
|
|
211
|
+
``CS.size ()`` the number of colours
|
|
212
|
+
``CS.ran ()`` a colour drawn at random
|
|
213
|
+
``CS.ord c`` the position of ``c``, counting from 0
|
|
214
|
+
``CS.col i`` the colour at position ``i``
|
|
215
|
+
``CS.legal c`` whether ``c`` is a member
|
|
216
|
+
``CS.mkstr c`` ``c`` as a string
|
|
217
|
+
|
|
218
|
+
The colours are listed on first use only, so declaring a large colour set
|
|
219
|
+
costs nothing until one of these is called.
|
|
220
|
+
"""
|
|
221
|
+
from ..ml.builtins import Builtin
|
|
222
|
+
from ..ml.errors import EvalError
|
|
223
|
+
from ..ml.multiset import Multiset
|
|
224
|
+
from ..ml.values import format_value
|
|
225
|
+
|
|
226
|
+
name = colour_set.name
|
|
227
|
+
cache: list[list[Any]] = []
|
|
228
|
+
|
|
229
|
+
def members() -> list[Any]:
|
|
230
|
+
if not cache:
|
|
231
|
+
if not colour_set.is_finite():
|
|
232
|
+
raise EvalError(f"{name} is infinite, so it has no {name}.all/ran/ord/col")
|
|
233
|
+
cache.append(list(colour_set.members()))
|
|
234
|
+
return cache[0]
|
|
235
|
+
|
|
236
|
+
def ordinal(colour: Any) -> int:
|
|
237
|
+
try:
|
|
238
|
+
return members().index(colour)
|
|
239
|
+
except ValueError:
|
|
240
|
+
raise EvalError(f"{format_value(colour)} is not a colour of {name}") from None
|
|
241
|
+
|
|
242
|
+
def column(index: Any) -> Any:
|
|
243
|
+
colours = members()
|
|
244
|
+
if isinstance(index, bool) or not isinstance(index, int) or not 0 <= index < len(colours):
|
|
245
|
+
raise EvalError(f"{name}.col expects 0..{len(colours) - 1}, got {format_value(index)}")
|
|
246
|
+
return colours[index]
|
|
247
|
+
|
|
248
|
+
def random_colour(_unit: Any) -> Any:
|
|
249
|
+
colours = members()
|
|
250
|
+
if not colours:
|
|
251
|
+
raise EvalError(f"{name} is empty, so {name}.ran has nothing to choose")
|
|
252
|
+
return evaluator.rng.choice(colours)
|
|
253
|
+
|
|
254
|
+
functions = {
|
|
255
|
+
"all": lambda _unit: Multiset.from_values(members()),
|
|
256
|
+
"size": lambda _unit: len(members()),
|
|
257
|
+
"ran": random_colour,
|
|
258
|
+
"ord": ordinal,
|
|
259
|
+
"col": column,
|
|
260
|
+
"legal": lambda colour: bool(colour_set.contains(colour)),
|
|
261
|
+
"mkstr": lambda colour: format_value(colour),
|
|
262
|
+
}
|
|
263
|
+
for function_name, function in functions.items():
|
|
264
|
+
qualified = f"{name}.{function_name}"
|
|
265
|
+
evaluator.globals.define(qualified, Builtin(qualified, function))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ===========================================================================
|
|
269
|
+
# The colour set declaration parser
|
|
270
|
+
# ===========================================================================
|
|
271
|
+
class _ColourSetParser(Parser):
|
|
272
|
+
"""Extends the ML parser cursor with the ``colset`` grammar."""
|
|
273
|
+
|
|
274
|
+
def __init__(self, tokens: Sequence[Token], registry: dict[str, ColourSet], source: str,
|
|
275
|
+
evaluator: Evaluator | None = None) -> None:
|
|
276
|
+
super().__init__(tokens, source)
|
|
277
|
+
self.registry = registry
|
|
278
|
+
#: Evaluates named range bounds and subset predicates; without it only
|
|
279
|
+
#: literals are accepted and subsets keep every value.
|
|
280
|
+
self.evaluator = evaluator
|
|
281
|
+
|
|
282
|
+
# -- helpers -------------------------------------------------------------
|
|
283
|
+
def _resolve(self, name: str) -> ColourSet:
|
|
284
|
+
if name not in self.registry:
|
|
285
|
+
raise ParseError(
|
|
286
|
+
f"unknown colour set '{name}' -- declare it before it is used",
|
|
287
|
+
self.peek().position,
|
|
288
|
+
)
|
|
289
|
+
return self.registry[name]
|
|
290
|
+
|
|
291
|
+
def _constant_int(self) -> int:
|
|
292
|
+
"""Read an integer bound: a literal, or an expression such as ``n``
|
|
293
|
+
or ``n * 2`` over values declared with ``val`` (as CPN Tools allows).
|
|
294
|
+
"""
|
|
295
|
+
token = self.peek()
|
|
296
|
+
if token.kind == "INT" and (self.peek(1).kind == "EOF" or self._bound_ends(1)):
|
|
297
|
+
self.next_token()
|
|
298
|
+
return int(token.value) # type: ignore[arg-type]
|
|
299
|
+
value = self._evaluate_until_bound_end("a colour set range")
|
|
300
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
301
|
+
raise ParseError(f"a colour set range must be an integer, got {value!r}",
|
|
302
|
+
token.position)
|
|
303
|
+
return value
|
|
304
|
+
|
|
305
|
+
def _bound_ends(self, offset: int = 0) -> bool:
|
|
306
|
+
"""Does the token at ``offset`` end a range bound?"""
|
|
307
|
+
token = self.peek(offset)
|
|
308
|
+
if token.kind == "EOF":
|
|
309
|
+
return True
|
|
310
|
+
if token.kind == "OP" and token.value in ("..", ";"):
|
|
311
|
+
return True
|
|
312
|
+
return token.kind in ("ID", "KEYWORD") and token.value in ("timed", "and")
|
|
313
|
+
|
|
314
|
+
def _evaluate_until_bound_end(self, what: str) -> Any:
|
|
315
|
+
"""Evaluate the tokens up to the end of a bound (``..``, ``;``, ...)."""
|
|
316
|
+
from ..ml.parser import parse_expression
|
|
317
|
+
start = self.peek().position
|
|
318
|
+
while not self._bound_ends():
|
|
319
|
+
self.next_token()
|
|
320
|
+
end = self.peek().position if not self.at_end() else len(self.source)
|
|
321
|
+
text = self.source[start:end].strip()
|
|
322
|
+
if not text:
|
|
323
|
+
raise ParseError(f"expected {what}", start)
|
|
324
|
+
if self.evaluator is None:
|
|
325
|
+
raise ParseError(f"{what} must be an integer literal here (found {text!r})", start)
|
|
326
|
+
return self.evaluator.evaluate(parse_expression(text))
|
|
327
|
+
|
|
328
|
+
def _string_literal(self) -> str:
|
|
329
|
+
token = self.next_token()
|
|
330
|
+
if token.kind not in ("STRING", "CHAR"):
|
|
331
|
+
raise ParseError("expected a string literal", token.position)
|
|
332
|
+
return str(token.value)
|
|
333
|
+
|
|
334
|
+
# -- the grammar ---------------------------------------------------------
|
|
335
|
+
def parse(self) -> ColourSet:
|
|
336
|
+
"""``colset NAME = <definition> [timed];``"""
|
|
337
|
+
if self.peek().kind == "ID" and self.peek().value == "colset":
|
|
338
|
+
self.next_token()
|
|
339
|
+
name = self.expect_identifier()
|
|
340
|
+
self.expect_operator("=")
|
|
341
|
+
colour_set = self._parse_definition(name)
|
|
342
|
+
# The optional `timed` keyword follows the definition.
|
|
343
|
+
if self.peek().kind == "ID" and self.peek().value == "timed":
|
|
344
|
+
self.next_token()
|
|
345
|
+
colour_set.timed = True
|
|
346
|
+
if self.at_operator(";"):
|
|
347
|
+
self.next_token()
|
|
348
|
+
return colour_set
|
|
349
|
+
|
|
350
|
+
def _parse_definition(self, name: str) -> ColourSet:
|
|
351
|
+
token = self.peek()
|
|
352
|
+
|
|
353
|
+
# `with a | b | c` -- an enumeration.
|
|
354
|
+
if self._at_word("with"):
|
|
355
|
+
self.next_token()
|
|
356
|
+
constants = [self.expect_identifier()]
|
|
357
|
+
while self.at_operator("|"):
|
|
358
|
+
self.next_token()
|
|
359
|
+
constants.append(self.expect_identifier())
|
|
360
|
+
return EnumColourSet(name, constants)
|
|
361
|
+
|
|
362
|
+
if token.kind != "ID":
|
|
363
|
+
raise ParseError(
|
|
364
|
+
f"expected a colour set definition, found {token.value!r}", token.position
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
word = str(token.value)
|
|
368
|
+
|
|
369
|
+
if word == "unit":
|
|
370
|
+
self.next_token()
|
|
371
|
+
alias = None
|
|
372
|
+
if self._at_word("with"):
|
|
373
|
+
self.next_token()
|
|
374
|
+
alias = self.expect_identifier()
|
|
375
|
+
return UnitColourSet(name, alias=alias)
|
|
376
|
+
|
|
377
|
+
if word == "bool":
|
|
378
|
+
self.next_token()
|
|
379
|
+
false_alias = true_alias = None
|
|
380
|
+
if self._at_word("with"):
|
|
381
|
+
self.next_token()
|
|
382
|
+
self.expect_operator("(")
|
|
383
|
+
false_alias = self.expect_identifier()
|
|
384
|
+
self.expect_operator(",")
|
|
385
|
+
true_alias = self.expect_identifier()
|
|
386
|
+
self.expect_operator(")")
|
|
387
|
+
return BoolColourSet(name, false_alias=false_alias, true_alias=true_alias)
|
|
388
|
+
|
|
389
|
+
if word in ("int", "intinf"):
|
|
390
|
+
self.next_token()
|
|
391
|
+
low = high = None
|
|
392
|
+
if self._at_word("with"):
|
|
393
|
+
self.next_token()
|
|
394
|
+
low = self._constant_int()
|
|
395
|
+
self.expect_operator("..")
|
|
396
|
+
high = self._constant_int()
|
|
397
|
+
cls = IntColourSet if word == "int" else IntInfColourSet
|
|
398
|
+
return cls(name, low=low, high=high)
|
|
399
|
+
|
|
400
|
+
if word == "time":
|
|
401
|
+
# CPN Tools' built-in model-time type. Model time is a number
|
|
402
|
+
# (integer or real, depending on the simulator setting), so it
|
|
403
|
+
# behaves like an unbounded real colour set.
|
|
404
|
+
self.next_token()
|
|
405
|
+
return RealColourSet(name)
|
|
406
|
+
|
|
407
|
+
if word == "real":
|
|
408
|
+
self.next_token()
|
|
409
|
+
low = high = None
|
|
410
|
+
if self._at_word("with"):
|
|
411
|
+
self.next_token()
|
|
412
|
+
low = float(self.next_token().value) # type: ignore[arg-type]
|
|
413
|
+
self.expect_operator("..")
|
|
414
|
+
high = float(self.next_token().value) # type: ignore[arg-type]
|
|
415
|
+
return RealColourSet(name, low=low, high=high)
|
|
416
|
+
|
|
417
|
+
if word == "string":
|
|
418
|
+
self.next_token()
|
|
419
|
+
char_low = char_high = None
|
|
420
|
+
length_low = length_high = None
|
|
421
|
+
if self._at_word("with"):
|
|
422
|
+
self.next_token()
|
|
423
|
+
char_low = self._string_literal()
|
|
424
|
+
self.expect_operator("..")
|
|
425
|
+
char_high = self._string_literal()
|
|
426
|
+
if self._at_word("and"):
|
|
427
|
+
self.next_token()
|
|
428
|
+
length_low = self._constant_int()
|
|
429
|
+
self.expect_operator("..")
|
|
430
|
+
length_high = self._constant_int()
|
|
431
|
+
return StringColourSet(
|
|
432
|
+
name, char_low=char_low, char_high=char_high,
|
|
433
|
+
length_low=length_low, length_high=length_high,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
if word == "index":
|
|
437
|
+
self.next_token()
|
|
438
|
+
tag = self.expect_identifier()
|
|
439
|
+
self._expect_word("with")
|
|
440
|
+
low = self._constant_int()
|
|
441
|
+
self.expect_operator("..")
|
|
442
|
+
high = self._constant_int()
|
|
443
|
+
return IndexColourSet(name, tag, low, high)
|
|
444
|
+
|
|
445
|
+
if word == "product":
|
|
446
|
+
self.next_token()
|
|
447
|
+
components = [self._resolve(self.expect_identifier())]
|
|
448
|
+
while self.at_operator("*"):
|
|
449
|
+
self.next_token()
|
|
450
|
+
components.append(self._resolve(self.expect_identifier()))
|
|
451
|
+
return ProductColourSet(name, components)
|
|
452
|
+
|
|
453
|
+
if word == "record":
|
|
454
|
+
self.next_token()
|
|
455
|
+
fields = [self._parse_record_field()]
|
|
456
|
+
while self.at_operator("*"):
|
|
457
|
+
self.next_token()
|
|
458
|
+
fields.append(self._parse_record_field())
|
|
459
|
+
return RecordColourSet(name, fields)
|
|
460
|
+
|
|
461
|
+
if word == "list":
|
|
462
|
+
self.next_token()
|
|
463
|
+
element = self._resolve(self.expect_identifier())
|
|
464
|
+
length_low = length_high = None
|
|
465
|
+
if self._at_word("with"):
|
|
466
|
+
self.next_token()
|
|
467
|
+
length_low = self._constant_int()
|
|
468
|
+
self.expect_operator("..")
|
|
469
|
+
length_high = self._constant_int()
|
|
470
|
+
return ListColourSet(name, element, length_low=length_low, length_high=length_high)
|
|
471
|
+
|
|
472
|
+
if word == "union":
|
|
473
|
+
self.next_token()
|
|
474
|
+
variants = [self._parse_union_variant()]
|
|
475
|
+
while self.at_operator("+"):
|
|
476
|
+
self.next_token()
|
|
477
|
+
variants.append(self._parse_union_variant())
|
|
478
|
+
return UnionColourSet(name, variants)
|
|
479
|
+
|
|
480
|
+
if word == "subset":
|
|
481
|
+
self.next_token()
|
|
482
|
+
base = self._resolve(self.expect_identifier())
|
|
483
|
+
return self._parse_subset(name, base)
|
|
484
|
+
|
|
485
|
+
# Anything else is an alias for an existing colour set.
|
|
486
|
+
self.next_token()
|
|
487
|
+
return AliasColourSet(name, self._resolve_named(word, token.position))
|
|
488
|
+
|
|
489
|
+
# -- pieces --------------------------------------------------------------
|
|
490
|
+
def _resolve_named(self, name: str, position: int) -> ColourSet:
|
|
491
|
+
if name not in self.registry:
|
|
492
|
+
raise ParseError(f"unknown colour set '{name}'", position)
|
|
493
|
+
return self.registry[name]
|
|
494
|
+
|
|
495
|
+
def _parse_record_field(self) -> tuple[str, ColourSet]:
|
|
496
|
+
field_name = self.expect_identifier()
|
|
497
|
+
self.expect_operator(":")
|
|
498
|
+
return field_name, self._resolve(self.expect_identifier())
|
|
499
|
+
|
|
500
|
+
def _parse_union_variant(self) -> tuple[str, ColourSet | None]:
|
|
501
|
+
tag = self.expect_identifier()
|
|
502
|
+
if self.at_operator(":"):
|
|
503
|
+
self.next_token()
|
|
504
|
+
return tag, self._resolve(self.expect_identifier())
|
|
505
|
+
return tag, None
|
|
506
|
+
|
|
507
|
+
def _parse_subset(self, name: str, base: ColourSet) -> ColourSet:
|
|
508
|
+
"""``subset A by pred`` or ``subset A with [v1, v2]``.
|
|
509
|
+
|
|
510
|
+
``by`` takes a predicate function, ``with`` an explicit member list.
|
|
511
|
+
The predicate is stored as source text and compiled lazily by the
|
|
512
|
+
model, because it may refer to functions declared further down.
|
|
513
|
+
"""
|
|
514
|
+
if self._at_word("by"):
|
|
515
|
+
self.next_token()
|
|
516
|
+
start = self.peek().position
|
|
517
|
+
# The predicate runs to the end of the declaration.
|
|
518
|
+
while not self.at_end() and not self.at_operator(";") and not self._at_word("timed"):
|
|
519
|
+
self.next_token()
|
|
520
|
+
end = self.peek().position if not self.at_end() else len(self.source)
|
|
521
|
+
source = self.source[start:end].strip()
|
|
522
|
+
if self.evaluator is None:
|
|
523
|
+
# Parsed only for its shape (no evaluator): keep every value.
|
|
524
|
+
return SubsetColourSet(name, base, lambda _v: True, predicate_source=source)
|
|
525
|
+
from ..ml.parser import parse_expression
|
|
526
|
+
evaluator = self.evaluator
|
|
527
|
+
# Evaluated now, so an undeclared predicate makes this colour set
|
|
528
|
+
# wait until the ML declarations have defined it.
|
|
529
|
+
function = evaluator.evaluate(parse_expression(source))
|
|
530
|
+
|
|
531
|
+
def predicate(value: Any) -> bool:
|
|
532
|
+
try:
|
|
533
|
+
return evaluator.apply(function, value) is True
|
|
534
|
+
except CPNMLError:
|
|
535
|
+
return False
|
|
536
|
+
|
|
537
|
+
return SubsetColourSet(name, base, predicate, predicate_source=source)
|
|
538
|
+
|
|
539
|
+
if self._at_word("with"):
|
|
540
|
+
self.next_token()
|
|
541
|
+
self.expect_operator("[")
|
|
542
|
+
start = self.peek().position
|
|
543
|
+
depth = 1
|
|
544
|
+
while depth:
|
|
545
|
+
if self.at_operator("["):
|
|
546
|
+
depth += 1
|
|
547
|
+
elif self.at_operator("]"):
|
|
548
|
+
depth -= 1
|
|
549
|
+
if depth == 0:
|
|
550
|
+
break
|
|
551
|
+
self.next_token()
|
|
552
|
+
end = self.peek().position
|
|
553
|
+
source = self.source[start:end]
|
|
554
|
+
self.expect_operator("]")
|
|
555
|
+
if self.evaluator is None:
|
|
556
|
+
return SubsetColourSet(name, base, lambda _v: True,
|
|
557
|
+
predicate_source=f"MEMBERS[{source}]")
|
|
558
|
+
from ..ml.parser import parse_expression
|
|
559
|
+
listed = list(self.evaluator.evaluate(parse_expression(f"[{source}]")))
|
|
560
|
+
for value in listed:
|
|
561
|
+
if not base.contains(value):
|
|
562
|
+
raise ParseError(f"{value!r} in subset '{name}' is not a member of "
|
|
563
|
+
f"'{base.name}'", start)
|
|
564
|
+
return SubsetColourSet(name, base, lambda value: value in listed,
|
|
565
|
+
predicate_source=f"MEMBERS[{source}]",
|
|
566
|
+
members=listed)
|
|
567
|
+
|
|
568
|
+
raise ParseError("'subset' must be followed by 'by' or 'with'", self.peek().position)
|
|
569
|
+
|
|
570
|
+
# -- word helpers (CPN keywords are plain identifiers to the ML lexer) ---
|
|
571
|
+
def _at_word(self, word: str) -> bool:
|
|
572
|
+
token = self.peek()
|
|
573
|
+
return token.kind == "ID" and token.value == word
|
|
574
|
+
|
|
575
|
+
def _expect_word(self, word: str) -> None:
|
|
576
|
+
if not self._at_word(word):
|
|
577
|
+
raise ParseError(f"expected '{word}'", self.peek().position)
|
|
578
|
+
self.next_token()
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def parse_colour_set_declaration(source: str, registry: dict[str, ColourSet],
|
|
582
|
+
evaluator: Evaluator | None = None) -> ColourSet:
|
|
583
|
+
"""Parse one ``colset ... = ...;`` declaration against ``registry``.
|
|
584
|
+
|
|
585
|
+
With an ``evaluator``, range bounds may name declared values and subset
|
|
586
|
+
predicates are compiled; without one, only literal bounds are accepted.
|
|
587
|
+
"""
|
|
588
|
+
return _ColourSetParser(tokenise(source), registry, source, evaluator).parse()
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def parse_variable_declaration(source: str) -> list[VariableDeclaration]:
|
|
592
|
+
"""Parse ``var a, b : COLOURSET;`` into one entry per name."""
|
|
593
|
+
parser = Parser(tokenise(source), source)
|
|
594
|
+
if parser.peek().kind == "ID" and parser.peek().value == "var":
|
|
595
|
+
parser.next_token()
|
|
596
|
+
names = [parser.expect_identifier()]
|
|
597
|
+
while parser.at_operator(","):
|
|
598
|
+
parser.next_token()
|
|
599
|
+
names.append(parser.expect_identifier())
|
|
600
|
+
parser.expect_operator(":")
|
|
601
|
+
colour_set_name = parser.expect_identifier()
|
|
602
|
+
return [VariableDeclaration(name, colour_set_name) for name in names]
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def classify_declaration(source: str) -> str:
|
|
606
|
+
"""Return ``'colset'``, ``'var'``, ``'globref'`` or ``'ml'`` for a snippet.
|
|
607
|
+
|
|
608
|
+
Used by the ``.cpn`` reader, which sometimes receives a declaration block
|
|
609
|
+
as one lump of text rather than as individually tagged elements.
|
|
610
|
+
"""
|
|
611
|
+
stripped = source.lstrip()
|
|
612
|
+
for keyword in ("colset", "var", "globref"):
|
|
613
|
+
if stripped.startswith(keyword) and (
|
|
614
|
+
len(stripped) == len(keyword) or not stripped[len(keyword)].isalnum()
|
|
615
|
+
):
|
|
616
|
+
return keyword
|
|
617
|
+
return "ml"
|