cpncheck 0.2.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.
- cpncheck/__init__.py +62 -0
- cpncheck/askctl.py +352 -0
- cpncheck/askctl_input.py +1120 -0
- cpncheck/checker.py +992 -0
- cpncheck/cli.py +325 -0
- cpncheck/coloured.py +1633 -0
- cpncheck/colourset.py +1000 -0
- cpncheck/combined.py +505 -0
- cpncheck/conformance.py +132 -0
- cpncheck/counterexample.py +333 -0
- cpncheck/cpnml.py +2249 -0
- cpncheck/ctl.py +210 -0
- cpncheck/equivalence.py +455 -0
- cpncheck/formula.py +1646 -0
- cpncheck/mllib.py +953 -0
- cpncheck/mutation.py +329 -0
- cpncheck/net.py +212 -0
- cpncheck/oracle.py +643 -0
- cpncheck/profile.py +350 -0
- cpncheck/profiles/__init__.py +12 -0
- cpncheck/profiles/statespace_report.py +439 -0
- cpncheck/properties.py +97 -0
- cpncheck/query.py +600 -0
- cpncheck/reduction.py +1237 -0
- cpncheck/result.py +466 -0
- cpncheck/selectors.py +338 -0
- cpncheck/smlvalue.py +355 -0
- cpncheck/statespace.py +271 -0
- cpncheck/structural.py +268 -0
- cpncheck/sweepline.py +483 -0
- cpncheck/symmetry.py +398 -0
- cpncheck-0.2.0.dist-info/METADATA +556 -0
- cpncheck-0.2.0.dist-info/RECORD +37 -0
- cpncheck-0.2.0.dist-info/WHEEL +5 -0
- cpncheck-0.2.0.dist-info/entry_points.txt +2 -0
- cpncheck-0.2.0.dist-info/licenses/LICENSE +21 -0
- cpncheck-0.2.0.dist-info/top_level.txt +1 -0
cpncheck/__init__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""CPNCheck -- a model checker for Coloured Petri Nets.
|
|
2
|
+
|
|
3
|
+
CPN Tools answers verification questions interactively: enter the state-space
|
|
4
|
+
tool, calculate the occurrence graph and the SCC graph, paste an ASK-CTL script
|
|
5
|
+
into an auxiliary box and evaluate it. CPNCheck answers the same questions from
|
|
6
|
+
Python, on the very ``.cpn`` file CPN Tools would open, and cross-checks its
|
|
7
|
+
answers against CPN Tools itself.
|
|
8
|
+
|
|
9
|
+
>>> from cpncheck import check_cpn
|
|
10
|
+
>>> result = check_cpn("model.cpn", properties="AG !dead")
|
|
11
|
+
>>> print(result.report())
|
|
12
|
+
|
|
13
|
+
Layers, bottom up:
|
|
14
|
+
|
|
15
|
+
============================ ====================================================
|
|
16
|
+
:mod:`cpncheck.net` parse the ``.cpn`` XML
|
|
17
|
+
:mod:`cpncheck.coloured` compile the CPN ML fragment; flatten; fire
|
|
18
|
+
:mod:`cpncheck.statespace` occurrence graph, SCCs, home markings
|
|
19
|
+
:mod:`cpncheck.reduction` stubborn-set reduced graphs (partial-order reduction)
|
|
20
|
+
:mod:`cpncheck.sweepline` the generalised sweep-line method
|
|
21
|
+
:mod:`cpncheck.equivalence` quotient graphs; time equivalence
|
|
22
|
+
:mod:`cpncheck.symmetry` symmetry reduction
|
|
23
|
+
:mod:`cpncheck.combined` several reductions at once
|
|
24
|
+
:mod:`cpncheck.query` user properties written as text
|
|
25
|
+
:mod:`cpncheck.askctl_input` ASK-CTL query files, with ASKCTL.sml's semantics
|
|
26
|
+
:mod:`cpncheck.ctl` CTL fixpoints over the occurrence graph
|
|
27
|
+
:mod:`cpncheck.properties` the property catalog
|
|
28
|
+
:mod:`cpncheck.result` verdicts and reports (text, JSON, LaTeX)
|
|
29
|
+
:mod:`cpncheck.checker` the four techniques, and the public entry points
|
|
30
|
+
:mod:`cpncheck.oracle` run CPN Tools 4.0.1 headless and compare
|
|
31
|
+
:mod:`cpncheck.mutation` mutants: every property must be able to fail
|
|
32
|
+
============================ ====================================================
|
|
33
|
+
|
|
34
|
+
Examples, one feature per script: ``examples/`` in the repository.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from .checker import CPNModelChecker, check_cpn
|
|
38
|
+
from .coloured import ColouredNet, UnsupportedNet
|
|
39
|
+
from .net import CPNNet
|
|
40
|
+
from .properties import METHODS, Property, catalog
|
|
41
|
+
from .result import ModelCheckResult, PropertyResult, compare_results
|
|
42
|
+
from .statespace import DEFAULT_MAX_NODES, StateSpace, build_state_space
|
|
43
|
+
|
|
44
|
+
__version__ = "0.2.0"
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"CPNModelChecker",
|
|
48
|
+
"CPNNet",
|
|
49
|
+
"ColouredNet",
|
|
50
|
+
"DEFAULT_MAX_NODES",
|
|
51
|
+
"METHODS",
|
|
52
|
+
"ModelCheckResult",
|
|
53
|
+
"catalog",
|
|
54
|
+
"Property",
|
|
55
|
+
"PropertyResult",
|
|
56
|
+
"StateSpace",
|
|
57
|
+
"UnsupportedNet",
|
|
58
|
+
"__version__",
|
|
59
|
+
"build_state_space",
|
|
60
|
+
"check_cpn",
|
|
61
|
+
"compare_results",
|
|
62
|
+
]
|
cpncheck/askctl.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""Rendering a formula as an ASK-CTL query for CPN Tools.
|
|
2
|
+
|
|
3
|
+
The same :mod:`cpncheck.formula` tree that :mod:`cpncheck.checker` evaluates in
|
|
4
|
+
Python is rendered here as CPN ML, so the two engines are asked *the same*
|
|
5
|
+
question by construction. Before this existed the Python check and the ASK-CTL
|
|
6
|
+
script were written by hand side by side, and nothing but care kept them in
|
|
7
|
+
agreement.
|
|
8
|
+
|
|
9
|
+
Correspondence with ASK-CTL (Cheng, Christensen & Mortensen; the ``ASKCTL``
|
|
10
|
+
library shipped with CPN Tools):
|
|
11
|
+
|
|
12
|
+
========================= ====================================================
|
|
13
|
+
``Dead`` ``DEAD``, declared as ``NOT(MODAL(TT))``
|
|
14
|
+
``Not`` / ``And`` / ``Or`` ``NOT`` / ``AND`` / ``OR`` (binary, folded right)
|
|
15
|
+
``AG`` ``INV``
|
|
16
|
+
``EF`` ``POS``
|
|
17
|
+
``EU`` ``EXIST_UNTIL``
|
|
18
|
+
``AF`` (textbook) ``AFmax``, declared below
|
|
19
|
+
``AF`` (ASK-CTL reading) ``EV``
|
|
20
|
+
atomic propositions ``NF("name", fn cpnq_n => ...)`` over the node's marking
|
|
21
|
+
``ForEach`` folded into ``AND``, one conjunct per member
|
|
22
|
+
========================= ====================================================
|
|
23
|
+
|
|
24
|
+
``AF`` needs the declared helper because ASK-CTL's ``EV`` is vacuously true at a
|
|
25
|
+
dead marking: ``EV(phi)`` means ``AF(phi or dead)``, so a net that stops before
|
|
26
|
+
``phi`` would satisfy it. ``AFmax phi`` adds the missing conjunct.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import collections
|
|
30
|
+
import re
|
|
31
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
32
|
+
|
|
33
|
+
from . import formula as F
|
|
34
|
+
|
|
35
|
+
__all__ = ["MLProgram", "UnsupportedFormula"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Exception raised when a formula has no ASK-CTL rendering.
|
|
39
|
+
class UnsupportedFormula(Exception):
|
|
40
|
+
"""The formula uses a construct this renderer cannot express in CPN ML."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Class rendering formulas as ASK-CTL and collecting the declarations they need.
|
|
44
|
+
class MLProgram:
|
|
45
|
+
"""Builds the declarations and the queries of one CPN Tools script.
|
|
46
|
+
|
|
47
|
+
Rendering a formula may require declarations -- a predicate over arcs for a
|
|
48
|
+
transition group, the ``AFmax`` helper -- which are emitted once and in
|
|
49
|
+
dependency order. Render every formula first, then read
|
|
50
|
+
:meth:`declarations`.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
net (ColouredNet): The net being rendered, for page and element names.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
# Method to build an empty program.
|
|
57
|
+
def __init__(self, net):
|
|
58
|
+
"""Create the renderer.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
net (ColouredNet): The flattened net.
|
|
62
|
+
"""
|
|
63
|
+
self.net = net
|
|
64
|
+
self._decls: List[str] = []
|
|
65
|
+
self._seen = set()
|
|
66
|
+
self._groups: Dict[tuple, str] = {}
|
|
67
|
+
|
|
68
|
+
# Method to emit a declaration once.
|
|
69
|
+
def declare(self, text: str) -> None:
|
|
70
|
+
"""Add a declaration, ignoring repeats.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
text (str): The CPN ML declaration.
|
|
74
|
+
"""
|
|
75
|
+
if text not in self._seen:
|
|
76
|
+
self._seen.add(text)
|
|
77
|
+
self._decls.append(text)
|
|
78
|
+
|
|
79
|
+
# Method to return every declaration emitted so far.
|
|
80
|
+
def declarations(self) -> str:
|
|
81
|
+
"""The declarations the rendered queries depend on.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
str: One declaration per line, in dependency order.
|
|
85
|
+
"""
|
|
86
|
+
return "\n".join(self._decls)
|
|
87
|
+
|
|
88
|
+
# ------------------------------------------------------------------
|
|
89
|
+
# Names
|
|
90
|
+
# ------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
# Method to render the token count of one place as an ML function.
|
|
93
|
+
def mark(self, pidx: int) -> str:
|
|
94
|
+
"""A function from a node to the number of tokens of a place.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
pidx (int): Place index.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
str: ``(fn cpnq_n => size (Mark.Page'Place 1 cpnq_n))``.
|
|
101
|
+
"""
|
|
102
|
+
return f"(fn cpnq_n => size ({self.multiset(pidx)}))"
|
|
103
|
+
|
|
104
|
+
# Method to render a list of token counters.
|
|
105
|
+
def marks(self, places: Sequence[int]) -> str:
|
|
106
|
+
"""An ML list of the counting functions of several places.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
places (sequence): Place indices.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
str: ``[f1, f2, ...]``.
|
|
113
|
+
"""
|
|
114
|
+
return "[" + ", ".join(self.mark(p) for p in places) + "]"
|
|
115
|
+
|
|
116
|
+
# Method to render the marking of one place as an ML multiset.
|
|
117
|
+
def multiset(self, pidx: int) -> str:
|
|
118
|
+
"""The marking of a place, as the ML multiset expression.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
pidx (int): Place index.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
str: ``Mark.Page'Place 1 cpnq_n``.
|
|
125
|
+
"""
|
|
126
|
+
p = self.net.places[pidx]
|
|
127
|
+
return f"Mark.{_ml_of(p)} {p.get('instance', 1)} cpnq_n"
|
|
128
|
+
|
|
129
|
+
# Method to declare and name an arc predicate for a transition group.
|
|
130
|
+
def group(self, transitions: Sequence[int]) -> str:
|
|
131
|
+
"""A predicate telling whether an arc belongs to a transition group.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
transitions (sequence): Transition indices.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
str: The name of the declared predicate.
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
UnsupportedFormula: When the group is empty.
|
|
141
|
+
"""
|
|
142
|
+
key = tuple(sorted(transitions))
|
|
143
|
+
if not key:
|
|
144
|
+
raise UnsupportedFormula("an empty transition group has no "
|
|
145
|
+
"ASK-CTL rendering")
|
|
146
|
+
if key in self._groups:
|
|
147
|
+
return self._groups[key]
|
|
148
|
+
name = f"isG{len(self._groups)}"
|
|
149
|
+
cases = " | ".join(
|
|
150
|
+
f"Bind.{_ml_of(self.net.transitions[ti])} "
|
|
151
|
+
f"({self.net.transitions[ti].get('instance', 1)}, _) => true"
|
|
152
|
+
for ti in key)
|
|
153
|
+
self.declare("fun enCount cpnq_isT cpnq_n = length "
|
|
154
|
+
"(List.filter cpnq_isT (OutArcs cpnq_n));")
|
|
155
|
+
self.declare(f"fun {name} cpnq_a = case ArcToBE cpnq_a of {cases} "
|
|
156
|
+
"| _ => false;")
|
|
157
|
+
self._groups[key] = name
|
|
158
|
+
return name
|
|
159
|
+
|
|
160
|
+
# ------------------------------------------------------------------
|
|
161
|
+
# Rendering
|
|
162
|
+
# ------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
# Method to render one formula as an ASK-CTL expression.
|
|
165
|
+
def render(self, phi, ctx, semantics: str = "textbook") -> str:
|
|
166
|
+
"""Translate a formula into an ASK-CTL expression.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
phi (Formula): The formula.
|
|
170
|
+
ctx (Context): The context the selectors resolve against.
|
|
171
|
+
semantics (str): ``"textbook"`` or ``"askctl"``, for ``AF``.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
str: The ASK-CTL expression.
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
UnsupportedFormula: For a formula with no rendering.
|
|
178
|
+
"""
|
|
179
|
+
render = lambda x: self.render(x, ctx, semantics) # noqa: E731
|
|
180
|
+
|
|
181
|
+
if getattr(phi, "script", None) is not None:
|
|
182
|
+
# An ASK-CTL query as the user wrote it, after its file.
|
|
183
|
+
if phi.script.declarations:
|
|
184
|
+
self.declare(phi.script.declarations)
|
|
185
|
+
return phi.text
|
|
186
|
+
|
|
187
|
+
if isinstance(phi, F.Dead):
|
|
188
|
+
self.declare("val DEAD = NOT(MODAL(TT));")
|
|
189
|
+
return "DEAD"
|
|
190
|
+
|
|
191
|
+
if isinstance(phi, F.Marked):
|
|
192
|
+
places = ctx.select(phi.selector)
|
|
193
|
+
body = (f"size ({self.multiset(places[0])}) > 0" if len(places) == 1
|
|
194
|
+
else f"List.exists (fn cpnq_f => cpnq_f cpnq_n > 0) "
|
|
195
|
+
f"{self.marks(places)}")
|
|
196
|
+
return f'NF("marked", fn cpnq_n => {body})'
|
|
197
|
+
|
|
198
|
+
if isinstance(phi, F.AllMarked):
|
|
199
|
+
places = ctx.select(phi.selector)
|
|
200
|
+
return ('NF("all marked", fn cpnq_n => List.all '
|
|
201
|
+
'(fn cpnq_f => cpnq_f cpnq_n > 0) '
|
|
202
|
+
f'{self.marks(places)})')
|
|
203
|
+
|
|
204
|
+
if isinstance(phi, F.MaxTokens):
|
|
205
|
+
places = ctx.select(phi.selector)
|
|
206
|
+
return (f'NF("at most {phi.limit}", fn cpnq_n => List.all '
|
|
207
|
+
f'(fn cpnq_f => cpnq_f cpnq_n <= {phi.limit}) '
|
|
208
|
+
f'{self.marks(places)})')
|
|
209
|
+
|
|
210
|
+
if isinstance(phi, F.TokensIn):
|
|
211
|
+
places = ctx.select(phi.selector)
|
|
212
|
+
domain = "[" + ", ".join(_ml_literal(v)
|
|
213
|
+
for v in sorted(phi.domain)) + "]"
|
|
214
|
+
parts = [f"(let val cpnq_m = {self.multiset(p)} in size cpnq_m = "
|
|
215
|
+
f"List.foldl (fn (cpnq_k, cpnq_acc) => cpnq_acc + "
|
|
216
|
+
f"cf (cpnq_k, cpnq_m)) 0 {domain} end)"
|
|
217
|
+
for p in places]
|
|
218
|
+
return ('NF("in domain", fn cpnq_n => ' + " andalso ".join(parts) + ")")
|
|
219
|
+
|
|
220
|
+
if isinstance(phi, F.TokenCount):
|
|
221
|
+
places = ctx.select(phi.selector)
|
|
222
|
+
total = " + ".join(f"{self.mark(p)} cpnq_n" for p in places) or "0"
|
|
223
|
+
return (f'NF("tokens", fn cpnq_n => ({total}) {phi.op} {phi.limit})')
|
|
224
|
+
|
|
225
|
+
if isinstance(phi, F.Holds):
|
|
226
|
+
places = ctx.select(phi.selector)
|
|
227
|
+
parts = " orelse ".join(f"cf ({phi.ml_text}, {self.multiset(p)}) > 0"
|
|
228
|
+
for p in places) or "false"
|
|
229
|
+
return f'NF("has", fn cpnq_n => {parts})'
|
|
230
|
+
|
|
231
|
+
if isinstance(phi, F.EnabledTally):
|
|
232
|
+
name = self.group(ctx.select(phi.selector))
|
|
233
|
+
return (f'NF("enabled count", fn cpnq_n => enCount {name} cpnq_n '
|
|
234
|
+
f'{phi.op} {phi.limit})')
|
|
235
|
+
|
|
236
|
+
if isinstance(phi, F.Enabled):
|
|
237
|
+
name = self.group(ctx.select(phi.selector))
|
|
238
|
+
return f'NF("enabled", fn cpnq_n => enCount {name} cpnq_n > 0)'
|
|
239
|
+
|
|
240
|
+
if isinstance(phi, F.EnabledCount):
|
|
241
|
+
name = self.group(ctx.select(phi.selector))
|
|
242
|
+
return (f'NF("at most {phi.limit} enabled", fn cpnq_n => enCount '
|
|
243
|
+
f'{name} cpnq_n <= {phi.limit})')
|
|
244
|
+
|
|
245
|
+
if isinstance(phi, F.Not):
|
|
246
|
+
return f"NOT({render(phi.operand)})"
|
|
247
|
+
|
|
248
|
+
if isinstance(phi, F.And):
|
|
249
|
+
return _fold("AND", [render(o) for o in phi.operands])
|
|
250
|
+
|
|
251
|
+
if isinstance(phi, F.Or):
|
|
252
|
+
return _fold("OR", [render(o) for o in phi.operands])
|
|
253
|
+
|
|
254
|
+
if isinstance(phi, F.AG):
|
|
255
|
+
return f"INV({render(phi.operand)})"
|
|
256
|
+
|
|
257
|
+
if isinstance(phi, F.EF):
|
|
258
|
+
return f"POS({render(phi.operand)})"
|
|
259
|
+
|
|
260
|
+
if isinstance(phi, F.EU):
|
|
261
|
+
return f"EXIST_UNTIL({render(phi.hold)}, {render(phi.target)})"
|
|
262
|
+
|
|
263
|
+
if isinstance(phi, F.AF):
|
|
264
|
+
inner = render(phi.operand)
|
|
265
|
+
if (phi.semantics or semantics) == "askctl":
|
|
266
|
+
return f"EV({inner})"
|
|
267
|
+
self.declare("val DEAD = NOT(MODAL(TT));")
|
|
268
|
+
self.declare("fun AFmax phi = AND(EV(phi), "
|
|
269
|
+
"NOT(EXIST_UNTIL(NOT(phi), AND(DEAD, NOT(phi)))));")
|
|
270
|
+
return f"AFmax ({inner})"
|
|
271
|
+
|
|
272
|
+
if isinstance(phi, F.ForEach):
|
|
273
|
+
parts = [render(f) for _label, f in phi.instances(ctx)]
|
|
274
|
+
if not parts:
|
|
275
|
+
return "TT"
|
|
276
|
+
return _fold("AND", parts)
|
|
277
|
+
|
|
278
|
+
raise UnsupportedFormula(
|
|
279
|
+
f"{type(phi).__name__} has no ASK-CTL rendering")
|
|
280
|
+
|
|
281
|
+
# Method to render a formula as a complete, string-valued query.
|
|
282
|
+
def query(self, phi, ctx, semantics: str = "textbook") -> str:
|
|
283
|
+
"""A query evaluating the formula at the initial marking.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
phi (Formula): The formula.
|
|
287
|
+
ctx (Context): The context the selectors resolve against.
|
|
288
|
+
semantics (str): ``"textbook"`` or ``"askctl"``.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
str: A string-valued CPN ML expression.
|
|
292
|
+
"""
|
|
293
|
+
return (f"Bool.toString (eval_node ({self.render(phi, ctx, semantics)})"
|
|
294
|
+
" InitNode)")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# Function to fold a binary ASK-CTL connective over a list.
|
|
298
|
+
def _fold(op: str, parts: List[str]) -> str:
|
|
299
|
+
"""Combine expressions with a binary connective.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
op (str): ``"AND"`` or ``"OR"``.
|
|
303
|
+
parts (list): The rendered operands.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
str: The folded expression.
|
|
307
|
+
"""
|
|
308
|
+
if not parts:
|
|
309
|
+
return "TT" if op == "AND" else "NOT(TT)"
|
|
310
|
+
out = parts[-1]
|
|
311
|
+
for part in reversed(parts[:-1]):
|
|
312
|
+
out = f"{op}({part}, {out})"
|
|
313
|
+
return out
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# Function to give the CPN ML path of a flattened place or transition.
|
|
317
|
+
def _ml_of(element) -> str:
|
|
318
|
+
"""``Page'Name`` as CPN Tools spells it in ``Mark.`` and ``Bind.``.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
element (dict): A place or transition of the flattened net.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
str: The path; built from the names when the record has none.
|
|
325
|
+
"""
|
|
326
|
+
if element.get("ml"):
|
|
327
|
+
return element["ml"]
|
|
328
|
+
clean = lambda s: re.sub(r"[^A-Za-z0-9_']", "_", s.strip()) # noqa: E731
|
|
329
|
+
return f"{clean(element['page'])}'{clean(element['name'])}"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# Function to render a Python token value as a CPN ML literal.
|
|
333
|
+
def _ml_literal(value) -> str:
|
|
334
|
+
"""Write a token value the way CPN ML spells it.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
value: The token value.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
str: The literal.
|
|
341
|
+
|
|
342
|
+
Raises:
|
|
343
|
+
UnsupportedFormula: For a value with no literal form.
|
|
344
|
+
"""
|
|
345
|
+
if isinstance(value, bool):
|
|
346
|
+
return "true" if value else "false"
|
|
347
|
+
if isinstance(value, int):
|
|
348
|
+
return str(value) if value >= 0 else f"~{-value}"
|
|
349
|
+
if isinstance(value, float):
|
|
350
|
+
text = repr(value)
|
|
351
|
+
return text.replace("-", "~") if text.startswith("-") else text
|
|
352
|
+
raise UnsupportedFormula(f"no CPN ML literal for {value!r}")
|