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
openprocess/ml/parser.py
ADDED
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
"""Recursive-descent parser for the CPN ML subset.
|
|
2
|
+
|
|
3
|
+
Grammar and precedence
|
|
4
|
+
----------------------
|
|
5
|
+
The infix precedence table follows Standard ML, with the two CPN additions
|
|
6
|
+
(``++``/``--`` for multisets and the backquote coefficient) slotted in where
|
|
7
|
+
CPN Tools puts them. Lowest binding power first:
|
|
8
|
+
|
|
9
|
+
==== ======================== ==============
|
|
10
|
+
Lvl Operators Associativity
|
|
11
|
+
==== ======================== ==============
|
|
12
|
+
1 ``orelse`` left
|
|
13
|
+
2 ``andalso`` left
|
|
14
|
+
3 ``= <> < > <= >=`` left
|
|
15
|
+
4 ``+++ ---`` (timed) left
|
|
16
|
+
5 ``@+`` (time delay) non-associative
|
|
17
|
+
6 ``:: @ ^^`` right
|
|
18
|
+
7 ``+ - ^ ++ --`` left
|
|
19
|
+
8 ``* / div mod`` left
|
|
20
|
+
9 ``~`` ``not`` (prefix) --
|
|
21
|
+
10 `` ` `` (coefficient) non-associative
|
|
22
|
+
11 application ``f x`` left
|
|
23
|
+
12 atoms --
|
|
24
|
+
==== ======================== ==============
|
|
25
|
+
|
|
26
|
+
The timed levels follow CPN Tools' timed multisets:
|
|
27
|
+
``1`x@+5 +++ 1`y@+3`` delays two tokens differently, ``(1`x ++ 1`y)@+5``
|
|
28
|
+
and ``1`x ++ 1`y @+ 5`` delay both, and ``1`3@5 +++ 1`4@0`` gives an
|
|
29
|
+
initial marking with time stamps (``@`` with a number on the right is a time
|
|
30
|
+
stamp; between two lists it is list append).
|
|
31
|
+
|
|
32
|
+
Two consequences are worth internalising, because they explain how ordinary
|
|
33
|
+
CPN inscriptions parse:
|
|
34
|
+
|
|
35
|
+
* ``2`x ++ 3`y`` groups as ``(2`x) ++ (3`y)`` -- the coefficient binds tighter
|
|
36
|
+
than multiset union, so the familiar multiset literal works without brackets.
|
|
37
|
+
* ``1`f(a)`` groups as ``1`(f(a))`` -- application binds tighter still, so a
|
|
38
|
+
computed token value needs no brackets either.
|
|
39
|
+
|
|
40
|
+
Everything else is plain ML. Where ML is ambiguous (a ``case`` nested inside
|
|
41
|
+
another ``case``) we resolve greedily, as every ML compiler does, meaning the
|
|
42
|
+
inner ``case`` swallows following ``|`` rules and you must parenthesise to get
|
|
43
|
+
the other reading.
|
|
44
|
+
|
|
45
|
+
Entry points
|
|
46
|
+
------------
|
|
47
|
+
``parse_expression``
|
|
48
|
+
A general expression -- guards, initial markings, ``let`` bodies.
|
|
49
|
+
``parse_arc_expression``
|
|
50
|
+
Same, but additionally accepts a trailing ``@+ delay`` (only meaningful on
|
|
51
|
+
an arc, hence the separate entry point).
|
|
52
|
+
``parse_declarations``
|
|
53
|
+
A sequence of ``val`` / ``fun`` declarations, as found in a model's
|
|
54
|
+
declaration block.
|
|
55
|
+
``expression_to_pattern``
|
|
56
|
+
Reinterprets an already-parsed expression as a pattern. Input arc
|
|
57
|
+
inscriptions are written as expressions but *used* as patterns during
|
|
58
|
+
binding, and this is the bridge between the two.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
from __future__ import annotations
|
|
62
|
+
|
|
63
|
+
from typing import Any, Callable, Sequence
|
|
64
|
+
|
|
65
|
+
from .ast_nodes import (
|
|
66
|
+
App, BinOp, CaseExpr, Coefficient, Decl, Delay, EmptyMultiset, Expr,
|
|
67
|
+
FnExpr, FunDecl, IfExpr, LetExpr, ListExpr, Literal, PAs, PCons,
|
|
68
|
+
PConstructor, PList, PLiteral, PRecord, PTuple, PVar, PWildcard, Pattern,
|
|
69
|
+
RecordExpr, Selector, TupleExpr, UnOp, ValDecl, Var,
|
|
70
|
+
)
|
|
71
|
+
from .errors import ParseError
|
|
72
|
+
from .lexer import Token, tokenise
|
|
73
|
+
from .values import UNIT
|
|
74
|
+
|
|
75
|
+
# Binary operator levels. Each entry maps a level number to the set of
|
|
76
|
+
# operator spellings at that level; ``_RIGHT_ASSOCIATIVE`` lists the levels that
|
|
77
|
+
# fold to the right instead of the left.
|
|
78
|
+
_BINARY_LEVELS: dict[int, frozenset[str]] = {
|
|
79
|
+
1: frozenset({"orelse"}),
|
|
80
|
+
2: frozenset({"andalso"}),
|
|
81
|
+
3: frozenset({"=", "<>", "<", ">", "<=", ">="}),
|
|
82
|
+
4: frozenset({"+++", "---"}),
|
|
83
|
+
5: frozenset({"@+"}),
|
|
84
|
+
6: frozenset({"::", "@", "^^"}),
|
|
85
|
+
7: frozenset({"+", "-", "^", "++", "--"}),
|
|
86
|
+
8: frozenset({"*", "/", "div", "mod"}),
|
|
87
|
+
}
|
|
88
|
+
_RIGHT_ASSOCIATIVE = frozenset({6})
|
|
89
|
+
_DELAY_LEVEL = 5
|
|
90
|
+
_LOWEST_LEVEL = 1
|
|
91
|
+
_HIGHEST_BINARY_LEVEL = 8
|
|
92
|
+
|
|
93
|
+
# Tokens that can begin an atom. The application parser uses this to decide
|
|
94
|
+
# whether the next token continues an application (``f x``) or ends it.
|
|
95
|
+
_ATOM_START_KEYWORDS = frozenset({"true", "false", "nil", "empty", "op"})
|
|
96
|
+
|
|
97
|
+
# Infix operators that ``op`` turns into ordinary two-argument functions:
|
|
98
|
+
# ``op +`` is ``fn (a, b) => a + b``, as in Standard ML.
|
|
99
|
+
_OP_SECTIONS = frozenset(
|
|
100
|
+
spelling for level in (3, 4, 6, 7, 8) for spelling in _BINARY_LEVELS[level]
|
|
101
|
+
)
|
|
102
|
+
_ATOM_START_OPERATORS = frozenset({"(", "[", "{", "#"})
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Parser:
|
|
106
|
+
"""A cursor over a token list plus one method per grammar production."""
|
|
107
|
+
|
|
108
|
+
def __init__(self, tokens: Sequence[Token], source: str = "") -> None:
|
|
109
|
+
self.tokens = list(tokens)
|
|
110
|
+
self.index = 0
|
|
111
|
+
self.source = source
|
|
112
|
+
|
|
113
|
+
# -- cursor helpers ------------------------------------------------------
|
|
114
|
+
def peek(self, offset: int = 0) -> Token:
|
|
115
|
+
position = min(self.index + offset, len(self.tokens) - 1)
|
|
116
|
+
return self.tokens[position]
|
|
117
|
+
|
|
118
|
+
def next_token(self) -> Token:
|
|
119
|
+
token = self.peek()
|
|
120
|
+
self.index += 1
|
|
121
|
+
return token
|
|
122
|
+
|
|
123
|
+
def at_operator(self, *spellings: str) -> bool:
|
|
124
|
+
token = self.peek()
|
|
125
|
+
return token.kind == "OP" and token.value in spellings
|
|
126
|
+
|
|
127
|
+
def at_keyword(self, *words: str) -> bool:
|
|
128
|
+
token = self.peek()
|
|
129
|
+
return token.kind == "KEYWORD" and token.value in words
|
|
130
|
+
|
|
131
|
+
def at_end(self) -> bool:
|
|
132
|
+
return self.peek().kind == "EOF"
|
|
133
|
+
|
|
134
|
+
def expect_operator(self, spelling: str) -> Token:
|
|
135
|
+
if not self.at_operator(spelling):
|
|
136
|
+
raise ParseError(
|
|
137
|
+
f"expected {spelling!r} but found {self._describe(self.peek())}",
|
|
138
|
+
self.peek().position,
|
|
139
|
+
)
|
|
140
|
+
return self.next_token()
|
|
141
|
+
|
|
142
|
+
def expect_keyword(self, word: str) -> Token:
|
|
143
|
+
if not self.at_keyword(word):
|
|
144
|
+
raise ParseError(
|
|
145
|
+
f"expected {word!r} but found {self._describe(self.peek())}",
|
|
146
|
+
self.peek().position,
|
|
147
|
+
)
|
|
148
|
+
return self.next_token()
|
|
149
|
+
|
|
150
|
+
def expect_identifier(self) -> str:
|
|
151
|
+
if self.peek().kind != "ID":
|
|
152
|
+
raise ParseError(
|
|
153
|
+
f"expected an identifier but found {self._describe(self.peek())}",
|
|
154
|
+
self.peek().position,
|
|
155
|
+
)
|
|
156
|
+
return str(self.next_token().value)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _describe(token: Token) -> str:
|
|
160
|
+
if token.kind == "EOF":
|
|
161
|
+
return "end of input"
|
|
162
|
+
return f"{token.value!r}"
|
|
163
|
+
|
|
164
|
+
# =======================================================================
|
|
165
|
+
# Expressions
|
|
166
|
+
# =======================================================================
|
|
167
|
+
def parse_expression(self) -> Expr:
|
|
168
|
+
"""Top of the expression grammar."""
|
|
169
|
+
token = self.peek()
|
|
170
|
+
|
|
171
|
+
if token.kind == "KEYWORD":
|
|
172
|
+
if token.value == "if":
|
|
173
|
+
return self._parse_if()
|
|
174
|
+
if token.value == "let":
|
|
175
|
+
return self._parse_let()
|
|
176
|
+
if token.value == "case":
|
|
177
|
+
return self._parse_case()
|
|
178
|
+
if token.value == "fn":
|
|
179
|
+
return self._parse_fn()
|
|
180
|
+
if token.value == "raise":
|
|
181
|
+
# `raise Exn` -- we parse and discard the exception name so that
|
|
182
|
+
# models using it load; evaluating one is a runtime error.
|
|
183
|
+
start = self.next_token().position
|
|
184
|
+
self.parse_expression()
|
|
185
|
+
return App(Var("raise", start), Literal(UNIT, start), start)
|
|
186
|
+
|
|
187
|
+
return self._parse_binary(_LOWEST_LEVEL)
|
|
188
|
+
|
|
189
|
+
def _parse_if(self) -> Expr:
|
|
190
|
+
start = self.expect_keyword("if").position
|
|
191
|
+
condition = self.parse_expression()
|
|
192
|
+
self.expect_keyword("then")
|
|
193
|
+
then_branch = self.parse_expression()
|
|
194
|
+
self.expect_keyword("else")
|
|
195
|
+
else_branch = self.parse_expression()
|
|
196
|
+
return IfExpr(condition, then_branch, else_branch, start)
|
|
197
|
+
|
|
198
|
+
def _parse_let(self) -> Expr:
|
|
199
|
+
start = self.expect_keyword("let").position
|
|
200
|
+
declarations: list[Decl] = []
|
|
201
|
+
while not self.at_keyword("in"):
|
|
202
|
+
if self.at_end():
|
|
203
|
+
raise ParseError("unterminated 'let': expected 'in'", start)
|
|
204
|
+
declarations.append(self.parse_declaration())
|
|
205
|
+
# Semicolons between declarations are optional in ML.
|
|
206
|
+
while self.at_operator(";"):
|
|
207
|
+
self.next_token()
|
|
208
|
+
self.expect_keyword("in")
|
|
209
|
+
body = self.parse_expression()
|
|
210
|
+
self.expect_keyword("end")
|
|
211
|
+
return LetExpr(tuple(declarations), body, start)
|
|
212
|
+
|
|
213
|
+
def _parse_case(self) -> Expr:
|
|
214
|
+
start = self.expect_keyword("case").position
|
|
215
|
+
scrutinee = self.parse_expression()
|
|
216
|
+
self.expect_keyword("of")
|
|
217
|
+
return CaseExpr(scrutinee, self._parse_match_rules(), start)
|
|
218
|
+
|
|
219
|
+
def _parse_fn(self) -> Expr:
|
|
220
|
+
start = self.expect_keyword("fn").position
|
|
221
|
+
return FnExpr(self._parse_match_rules(), start)
|
|
222
|
+
|
|
223
|
+
def _parse_match_rules(self) -> tuple[tuple[Pattern, Expr], ...]:
|
|
224
|
+
"""Parse ``p => e | p => e | ...`` -- shared by ``fn`` and ``case``."""
|
|
225
|
+
rules: list[tuple[Pattern, Expr]] = []
|
|
226
|
+
while True:
|
|
227
|
+
pattern = self.parse_pattern()
|
|
228
|
+
self.expect_operator("=>")
|
|
229
|
+
body = self.parse_expression()
|
|
230
|
+
rules.append((pattern, body))
|
|
231
|
+
if self.at_operator("|"):
|
|
232
|
+
self.next_token()
|
|
233
|
+
continue
|
|
234
|
+
return tuple(rules)
|
|
235
|
+
|
|
236
|
+
# -- infix operators -----------------------------------------------------
|
|
237
|
+
def _parse_binary(self, level: int) -> Expr:
|
|
238
|
+
"""Precedence-climbing over :data:`_BINARY_LEVELS`."""
|
|
239
|
+
if level > _HIGHEST_BINARY_LEVEL:
|
|
240
|
+
return self._parse_unary()
|
|
241
|
+
|
|
242
|
+
operators = _BINARY_LEVELS[level]
|
|
243
|
+
left = self._parse_binary(level + 1)
|
|
244
|
+
|
|
245
|
+
if level == _DELAY_LEVEL:
|
|
246
|
+
# `tokens @+ delay`: at most once, the delay being an ordinary
|
|
247
|
+
# arithmetic expression (`x @+ d + 1` delays by d + 1).
|
|
248
|
+
if self.at_operator("@+"):
|
|
249
|
+
token = self.next_token()
|
|
250
|
+
return Delay(left, self._parse_binary(level + 1), token.position)
|
|
251
|
+
return left
|
|
252
|
+
|
|
253
|
+
while True:
|
|
254
|
+
token = self.peek()
|
|
255
|
+
# `div` and `mod` are keywords, every other operator is an OP token.
|
|
256
|
+
spelling = token.value if token.kind in ("OP", "KEYWORD") else None
|
|
257
|
+
if spelling not in operators:
|
|
258
|
+
return left
|
|
259
|
+
self.next_token()
|
|
260
|
+
if level in _RIGHT_ASSOCIATIVE:
|
|
261
|
+
# Right associative: recurse at the *same* level so that
|
|
262
|
+
# `a :: b :: c` becomes `a :: (b :: c)`.
|
|
263
|
+
right = self._parse_binary(level)
|
|
264
|
+
return BinOp(str(spelling), left, right, token.position)
|
|
265
|
+
right = self._parse_binary(level + 1)
|
|
266
|
+
left = BinOp(str(spelling), left, right, token.position)
|
|
267
|
+
|
|
268
|
+
def _parse_unary(self) -> Expr:
|
|
269
|
+
"""Prefix ``~`` (arithmetic negation) and ``not`` (boolean negation)."""
|
|
270
|
+
if self.at_operator("~"):
|
|
271
|
+
token = self.next_token()
|
|
272
|
+
return UnOp("~", self._parse_unary(), token.position)
|
|
273
|
+
if self.at_keyword("not"):
|
|
274
|
+
token = self.next_token()
|
|
275
|
+
return UnOp("not", self._parse_unary(), token.position)
|
|
276
|
+
return self._parse_coefficient()
|
|
277
|
+
|
|
278
|
+
def _parse_coefficient(self) -> Expr:
|
|
279
|
+
"""``n`v`` -- multiset coefficient, binding tighter than ``++``."""
|
|
280
|
+
left = self._parse_application()
|
|
281
|
+
if self.at_operator("`"):
|
|
282
|
+
token = self.next_token()
|
|
283
|
+
value = self._parse_application()
|
|
284
|
+
return Coefficient(left, value, token.position)
|
|
285
|
+
return left
|
|
286
|
+
|
|
287
|
+
def _parse_application(self) -> Expr:
|
|
288
|
+
"""Juxtaposition: ``f x y`` is ``(f x) y``."""
|
|
289
|
+
result = self._parse_atom()
|
|
290
|
+
while self._starts_atom():
|
|
291
|
+
argument = self._parse_atom()
|
|
292
|
+
result = App(result, argument, result.position)
|
|
293
|
+
return result
|
|
294
|
+
|
|
295
|
+
def _starts_atom(self) -> bool:
|
|
296
|
+
token = self.peek()
|
|
297
|
+
if token.kind in ("ID", "INT", "REAL", "STRING", "CHAR"):
|
|
298
|
+
return True
|
|
299
|
+
if token.kind == "KEYWORD":
|
|
300
|
+
return token.value in _ATOM_START_KEYWORDS
|
|
301
|
+
if token.kind == "OP":
|
|
302
|
+
return token.value in _ATOM_START_OPERATORS
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
# -- atoms ---------------------------------------------------------------
|
|
306
|
+
def _parse_atom(self) -> Expr:
|
|
307
|
+
token = self.peek()
|
|
308
|
+
|
|
309
|
+
if token.kind in ("INT", "REAL", "STRING", "CHAR"):
|
|
310
|
+
self.next_token()
|
|
311
|
+
return Literal(token.value, token.position)
|
|
312
|
+
|
|
313
|
+
if token.kind == "ID":
|
|
314
|
+
self.next_token()
|
|
315
|
+
return Var(str(token.value), token.position)
|
|
316
|
+
|
|
317
|
+
if token.kind == "KEYWORD":
|
|
318
|
+
if token.value in ("true", "false"):
|
|
319
|
+
self.next_token()
|
|
320
|
+
return Literal(token.value == "true", token.position)
|
|
321
|
+
if token.value == "nil":
|
|
322
|
+
self.next_token()
|
|
323
|
+
return ListExpr((), token.position)
|
|
324
|
+
if token.value == "empty":
|
|
325
|
+
self.next_token()
|
|
326
|
+
return EmptyMultiset(token.position)
|
|
327
|
+
if token.value == "op":
|
|
328
|
+
return self._parse_op_section()
|
|
329
|
+
# `if`/`let`/`case`/`fn` in argument position must be bracketed in
|
|
330
|
+
# ML, so reaching here means the input really is malformed.
|
|
331
|
+
raise ParseError(
|
|
332
|
+
f"unexpected keyword {token.value!r} in expression", token.position
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if token.kind == "OP":
|
|
336
|
+
if token.value == "(":
|
|
337
|
+
return self._parse_parenthesised()
|
|
338
|
+
if token.value == "[":
|
|
339
|
+
return self._parse_list()
|
|
340
|
+
if token.value == "{":
|
|
341
|
+
return self._parse_record()
|
|
342
|
+
if token.value == "#":
|
|
343
|
+
return self._parse_selector()
|
|
344
|
+
|
|
345
|
+
raise ParseError(
|
|
346
|
+
f"unexpected {self._describe(token)} where an expression was expected",
|
|
347
|
+
token.position,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
def _parse_op_section(self) -> Expr:
|
|
351
|
+
"""``op +``: an infix operator as a function on pairs."""
|
|
352
|
+
start = self.next_token().position # the `op` keyword
|
|
353
|
+
token = self.next_token()
|
|
354
|
+
spelling = token.value if token.kind in ("OP", "KEYWORD") else None
|
|
355
|
+
if spelling not in _OP_SECTIONS:
|
|
356
|
+
raise ParseError(
|
|
357
|
+
f"'op' must be followed by an infix operator, not {self._describe(token)}",
|
|
358
|
+
token.position,
|
|
359
|
+
)
|
|
360
|
+
left, right = "op'left", "op'right" # cannot clash with ML names
|
|
361
|
+
pattern = PTuple((PVar(left, start), PVar(right, start)), start)
|
|
362
|
+
body = BinOp(str(spelling), Var(left, start), Var(right, start), start)
|
|
363
|
+
return FnExpr(((pattern, body),), start)
|
|
364
|
+
|
|
365
|
+
def _parse_parenthesised(self) -> Expr:
|
|
366
|
+
"""``()``, ``(e)`` or ``(e1, e2, ...)`` -- unit, grouping, or a tuple."""
|
|
367
|
+
start = self.expect_operator("(").position
|
|
368
|
+
if self.at_operator(")"):
|
|
369
|
+
self.next_token()
|
|
370
|
+
return Literal(UNIT, start)
|
|
371
|
+
items = [self.parse_expression()]
|
|
372
|
+
while self.at_operator(","):
|
|
373
|
+
self.next_token()
|
|
374
|
+
items.append(self.parse_expression())
|
|
375
|
+
self.expect_operator(")")
|
|
376
|
+
if len(items) == 1:
|
|
377
|
+
return items[0]
|
|
378
|
+
return TupleExpr(tuple(items), start)
|
|
379
|
+
|
|
380
|
+
def _parse_list(self) -> Expr:
|
|
381
|
+
start = self.expect_operator("[").position
|
|
382
|
+
items: list[Expr] = []
|
|
383
|
+
if not self.at_operator("]"):
|
|
384
|
+
items.append(self.parse_expression())
|
|
385
|
+
while self.at_operator(","):
|
|
386
|
+
self.next_token()
|
|
387
|
+
items.append(self.parse_expression())
|
|
388
|
+
self.expect_operator("]")
|
|
389
|
+
return ListExpr(tuple(items), start)
|
|
390
|
+
|
|
391
|
+
def _parse_record(self) -> Expr:
|
|
392
|
+
start = self.expect_operator("{").position
|
|
393
|
+
fields: list[tuple[str, Expr]] = []
|
|
394
|
+
if not self.at_operator("}"):
|
|
395
|
+
while True:
|
|
396
|
+
name = self.expect_identifier()
|
|
397
|
+
self.expect_operator("=")
|
|
398
|
+
fields.append((name, self.parse_expression()))
|
|
399
|
+
if self.at_operator(","):
|
|
400
|
+
self.next_token()
|
|
401
|
+
continue
|
|
402
|
+
break
|
|
403
|
+
self.expect_operator("}")
|
|
404
|
+
return RecordExpr(tuple(fields), start)
|
|
405
|
+
|
|
406
|
+
def _parse_selector(self) -> Expr:
|
|
407
|
+
"""``#field`` (record projection) or ``#2`` (tuple projection)."""
|
|
408
|
+
start = self.expect_operator("#").position
|
|
409
|
+
token = self.next_token()
|
|
410
|
+
if token.kind == "ID":
|
|
411
|
+
return Selector(str(token.value), start)
|
|
412
|
+
if token.kind == "INT":
|
|
413
|
+
return Selector(str(token.value), start)
|
|
414
|
+
raise ParseError("'#' must be followed by a field name or a number", start)
|
|
415
|
+
|
|
416
|
+
# =======================================================================
|
|
417
|
+
# Patterns
|
|
418
|
+
# =======================================================================
|
|
419
|
+
def parse_pattern(self) -> Pattern:
|
|
420
|
+
"""Patterns support ``as`` at the outermost level and ``::`` inside."""
|
|
421
|
+
# `x as p`
|
|
422
|
+
if self.peek().kind == "ID" and self.peek(1).kind == "KEYWORD" and self.peek(1).value == "as":
|
|
423
|
+
name_token = self.next_token()
|
|
424
|
+
self.next_token() # consume `as`
|
|
425
|
+
return PAs(str(name_token.value), self.parse_pattern(), name_token.position)
|
|
426
|
+
return self._parse_cons_pattern()
|
|
427
|
+
|
|
428
|
+
def _parse_cons_pattern(self) -> Pattern:
|
|
429
|
+
"""``h :: t`` -- right associative, like the expression form."""
|
|
430
|
+
head = self._parse_applied_pattern()
|
|
431
|
+
if self.at_operator("::"):
|
|
432
|
+
token = self.next_token()
|
|
433
|
+
return PCons(head, self._parse_cons_pattern(), token.position)
|
|
434
|
+
return head
|
|
435
|
+
|
|
436
|
+
def _parse_applied_pattern(self) -> Pattern:
|
|
437
|
+
"""A constructor applied to an argument: ``Car (a, b)``, ``ph(i)``.
|
|
438
|
+
|
|
439
|
+
This level exists because ML distinguishes two pattern positions. In
|
|
440
|
+
``fun f p1 p2 = e`` the parameters are *atomic* patterns, so ``f x y``
|
|
441
|
+
means two parameters rather than one constructor application. Anywhere
|
|
442
|
+
else -- inside brackets, in a ``case`` rule, on the left of ``=>`` --
|
|
443
|
+
juxtaposition **is** constructor application. :meth:`_parse_fun`
|
|
444
|
+
therefore calls :meth:`_parse_atomic_pattern` directly and everything
|
|
445
|
+
else comes through here.
|
|
446
|
+
|
|
447
|
+
Whether the head really names a constructor is not decided now: the
|
|
448
|
+
matcher resolves it against the environment, so ``ph(i)`` works whether
|
|
449
|
+
``ph`` is an index tag or a union constructor.
|
|
450
|
+
"""
|
|
451
|
+
head = self._parse_atomic_pattern()
|
|
452
|
+
if isinstance(head, PConstructor) and head.argument is None and self._starts_atomic_pattern():
|
|
453
|
+
return PConstructor(head.name, self._parse_atomic_pattern(), head.position)
|
|
454
|
+
if isinstance(head, PVar) and self._starts_atomic_pattern():
|
|
455
|
+
return PConstructor(head.name, self._parse_atomic_pattern(), head.position)
|
|
456
|
+
return head
|
|
457
|
+
|
|
458
|
+
def _parse_atomic_pattern(self) -> Pattern:
|
|
459
|
+
token = self.peek()
|
|
460
|
+
|
|
461
|
+
if token.kind in ("INT", "REAL", "STRING", "CHAR"):
|
|
462
|
+
self.next_token()
|
|
463
|
+
return PLiteral(token.value, token.position)
|
|
464
|
+
|
|
465
|
+
if token.kind == "KEYWORD":
|
|
466
|
+
if token.value in ("true", "false"):
|
|
467
|
+
self.next_token()
|
|
468
|
+
return PLiteral(token.value == "true", token.position)
|
|
469
|
+
if token.value == "nil":
|
|
470
|
+
self.next_token()
|
|
471
|
+
return PList((), token.position)
|
|
472
|
+
raise ParseError(f"unexpected keyword {token.value!r} in pattern", token.position)
|
|
473
|
+
|
|
474
|
+
if token.kind == "ID":
|
|
475
|
+
self.next_token()
|
|
476
|
+
name = str(token.value)
|
|
477
|
+
# A capitalised identifier is *probably* a nullary constructor, but
|
|
478
|
+
# it could equally be a variable the modeller chose to capitalise,
|
|
479
|
+
# so we emit PConstructor and let the matcher resolve it against
|
|
480
|
+
# the environment. Constructor *application* is handled one level
|
|
481
|
+
# up, in :meth:`_parse_applied_pattern`.
|
|
482
|
+
if name[:1].isupper():
|
|
483
|
+
return PConstructor(name, None, token.position)
|
|
484
|
+
return PVar(name, token.position)
|
|
485
|
+
|
|
486
|
+
if token.kind == "OP":
|
|
487
|
+
if token.value == "_":
|
|
488
|
+
self.next_token()
|
|
489
|
+
return PWildcard(token.position)
|
|
490
|
+
if token.value == "(":
|
|
491
|
+
return self._parse_parenthesised_pattern()
|
|
492
|
+
if token.value == "[":
|
|
493
|
+
return self._parse_list_pattern()
|
|
494
|
+
if token.value == "{":
|
|
495
|
+
return self._parse_record_pattern()
|
|
496
|
+
if token.value == "~" and self.peek(1).kind in ("INT", "REAL"):
|
|
497
|
+
# `~1` reaches here only if the lexer split it, which it does
|
|
498
|
+
# not, but handle it defensively.
|
|
499
|
+
self.next_token()
|
|
500
|
+
number = self.next_token()
|
|
501
|
+
return PLiteral(-number.value, token.position) # type: ignore[operator]
|
|
502
|
+
|
|
503
|
+
raise ParseError(
|
|
504
|
+
f"unexpected {self._describe(token)} where a pattern was expected",
|
|
505
|
+
token.position,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
def _starts_atomic_pattern(self) -> bool:
|
|
509
|
+
token = self.peek()
|
|
510
|
+
if token.kind in ("ID", "INT", "REAL", "STRING", "CHAR"):
|
|
511
|
+
return True
|
|
512
|
+
if token.kind == "KEYWORD":
|
|
513
|
+
return token.value in ("true", "false", "nil")
|
|
514
|
+
if token.kind == "OP":
|
|
515
|
+
return token.value in ("(", "[", "{", "_")
|
|
516
|
+
return False
|
|
517
|
+
|
|
518
|
+
def _parse_parenthesised_pattern(self) -> Pattern:
|
|
519
|
+
start = self.expect_operator("(").position
|
|
520
|
+
if self.at_operator(")"):
|
|
521
|
+
self.next_token()
|
|
522
|
+
return PLiteral(UNIT, start)
|
|
523
|
+
items = [self.parse_pattern()]
|
|
524
|
+
while self.at_operator(","):
|
|
525
|
+
self.next_token()
|
|
526
|
+
items.append(self.parse_pattern())
|
|
527
|
+
self.expect_operator(")")
|
|
528
|
+
if len(items) == 1:
|
|
529
|
+
return items[0]
|
|
530
|
+
return PTuple(tuple(items), start)
|
|
531
|
+
|
|
532
|
+
def _parse_list_pattern(self) -> Pattern:
|
|
533
|
+
start = self.expect_operator("[").position
|
|
534
|
+
items: list[Pattern] = []
|
|
535
|
+
if not self.at_operator("]"):
|
|
536
|
+
items.append(self.parse_pattern())
|
|
537
|
+
while self.at_operator(","):
|
|
538
|
+
self.next_token()
|
|
539
|
+
items.append(self.parse_pattern())
|
|
540
|
+
self.expect_operator("]")
|
|
541
|
+
return PList(tuple(items), start)
|
|
542
|
+
|
|
543
|
+
def _parse_record_pattern(self) -> Pattern:
|
|
544
|
+
start = self.expect_operator("{").position
|
|
545
|
+
fields: list[tuple[str, Pattern]] = []
|
|
546
|
+
open_ended = False
|
|
547
|
+
if not self.at_operator("}"):
|
|
548
|
+
while True:
|
|
549
|
+
if self.at_operator("."):
|
|
550
|
+
# `...` ellipsis: three separate '.' tokens, or one '..'
|
|
551
|
+
# plus a '.', depending on how the lexer split them.
|
|
552
|
+
while self.at_operator(".") or self.at_operator(".."):
|
|
553
|
+
self.next_token()
|
|
554
|
+
open_ended = True
|
|
555
|
+
break
|
|
556
|
+
name = self.expect_identifier()
|
|
557
|
+
if self.at_operator("="):
|
|
558
|
+
self.next_token()
|
|
559
|
+
fields.append((name, self.parse_pattern()))
|
|
560
|
+
else:
|
|
561
|
+
# ML shorthand: `{name}` binds the field to a variable of
|
|
562
|
+
# the same name.
|
|
563
|
+
fields.append((name, PVar(name, start)))
|
|
564
|
+
if self.at_operator(","):
|
|
565
|
+
self.next_token()
|
|
566
|
+
continue
|
|
567
|
+
break
|
|
568
|
+
self.expect_operator("}")
|
|
569
|
+
return PRecord(tuple(fields), open_ended, start)
|
|
570
|
+
|
|
571
|
+
# =======================================================================
|
|
572
|
+
# Declarations
|
|
573
|
+
# =======================================================================
|
|
574
|
+
def parse_declaration(self) -> Decl:
|
|
575
|
+
token = self.peek()
|
|
576
|
+
if self.at_keyword("val"):
|
|
577
|
+
start = self.next_token().position
|
|
578
|
+
pattern = self.parse_pattern()
|
|
579
|
+
self.expect_operator("=")
|
|
580
|
+
return ValDecl(pattern, self.parse_expression(), start)
|
|
581
|
+
if self.at_keyword("fun"):
|
|
582
|
+
return self._parse_fun()
|
|
583
|
+
raise ParseError(
|
|
584
|
+
f"expected 'val' or 'fun' but found {self._describe(token)}", token.position
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
def _parse_fun(self) -> Decl:
|
|
588
|
+
"""``fun f p1 p2 = e | f q1 q2 = e2`` -- curried, multi-clause."""
|
|
589
|
+
start = self.expect_keyword("fun").position
|
|
590
|
+
name = self.expect_identifier()
|
|
591
|
+
clauses: list[tuple[tuple[Pattern, ...], Expr]] = []
|
|
592
|
+
while True:
|
|
593
|
+
parameters: list[Pattern] = []
|
|
594
|
+
while not self.at_operator("="):
|
|
595
|
+
parameters.append(self._parse_atomic_pattern())
|
|
596
|
+
if not parameters:
|
|
597
|
+
raise ParseError(f"function '{name}' has no parameters", start)
|
|
598
|
+
self.expect_operator("=")
|
|
599
|
+
clauses.append((tuple(parameters), self.parse_expression()))
|
|
600
|
+
# Another clause? It starts with `|` followed by the same name.
|
|
601
|
+
if self.at_operator("|"):
|
|
602
|
+
save = self.index
|
|
603
|
+
self.next_token()
|
|
604
|
+
if self.peek().kind == "ID" and self.peek().value == name:
|
|
605
|
+
self.next_token()
|
|
606
|
+
continue
|
|
607
|
+
self.index = save
|
|
608
|
+
return FunDecl(name, tuple(clauses), start)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
# ===========================================================================
|
|
612
|
+
# Public entry points
|
|
613
|
+
# ===========================================================================
|
|
614
|
+
def parse_expression(source: str) -> Expr:
|
|
615
|
+
"""Parse a complete expression; error if anything is left over."""
|
|
616
|
+
parser = Parser(tokenise(source), source)
|
|
617
|
+
expression = parser.parse_expression()
|
|
618
|
+
if not parser.at_end():
|
|
619
|
+
raise ParseError(
|
|
620
|
+
f"unexpected {Parser._describe(parser.peek())} after the expression",
|
|
621
|
+
parser.peek().position,
|
|
622
|
+
)
|
|
623
|
+
return expression
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def parse_arc_expression(source: str) -> Expr:
|
|
627
|
+
"""Parse an arc inscription, allowing a trailing ``@+ delay``."""
|
|
628
|
+
parser = Parser(tokenise(source), source)
|
|
629
|
+
expression = parser.parse_expression()
|
|
630
|
+
if parser.at_operator("@+"):
|
|
631
|
+
token = parser.next_token()
|
|
632
|
+
expression = Delay(expression, parser.parse_expression(), token.position)
|
|
633
|
+
if not parser.at_end():
|
|
634
|
+
raise ParseError(
|
|
635
|
+
f"unexpected {Parser._describe(parser.peek())} after the arc expression",
|
|
636
|
+
parser.peek().position,
|
|
637
|
+
)
|
|
638
|
+
return expression
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def parse_declarations(source: str) -> list[Decl]:
|
|
642
|
+
"""Parse a whole declaration block (a sequence of ``val`` / ``fun``)."""
|
|
643
|
+
parser = Parser(tokenise(source), source)
|
|
644
|
+
declarations: list[Decl] = []
|
|
645
|
+
while not parser.at_end():
|
|
646
|
+
if parser.at_operator(";"):
|
|
647
|
+
parser.next_token()
|
|
648
|
+
continue
|
|
649
|
+
declarations.append(parser.parse_declaration())
|
|
650
|
+
return declarations
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
#: How :func:`expression_to_pattern` should read a bare identifier.
|
|
654
|
+
#: ``"constructor"`` -- a union/enumeration tag, matched as a constant;
|
|
655
|
+
#: ``"variable"`` -- a net variable, bound by the match;
|
|
656
|
+
#: ``"other"`` -- anything else (a function, an ML constant), which means
|
|
657
|
+
#: the expression is *not* a pattern and must be evaluated instead.
|
|
658
|
+
IdentifierRole = str
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def default_identifier_role(name: str) -> IdentifierRole:
|
|
662
|
+
"""Fallback classifier: ML's capitalisation convention.
|
|
663
|
+
|
|
664
|
+
Used when no environment is available (tests, tooling). The binder always
|
|
665
|
+
supplies a real classifier built from the model's declarations, because
|
|
666
|
+
the convention is only a convention -- plenty of CPN models capitalise
|
|
667
|
+
their function names.
|
|
668
|
+
"""
|
|
669
|
+
return "constructor" if name[:1].isupper() else "variable"
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def expression_to_pattern(expression: Expr,
|
|
673
|
+
identifier_role: Callable[[str], IdentifierRole] | None = None) -> Pattern:
|
|
674
|
+
"""Reinterpret an expression as a pattern.
|
|
675
|
+
|
|
676
|
+
Why this exists
|
|
677
|
+
---------------
|
|
678
|
+
An input arc carries an inscription such as ``(x, n)`` or ``1`p``. CPN
|
|
679
|
+
semantics say the transition is enabled when the inscription *evaluates* to
|
|
680
|
+
a sub-multiset of the place's marking -- but the variables in it are
|
|
681
|
+
unbound, so we cannot evaluate it yet. The practical algorithm (the same
|
|
682
|
+
one CPN Tools uses for the common case) is to treat the inscription as a
|
|
683
|
+
pattern and match it against the tokens that are actually there, which
|
|
684
|
+
determines the variable values directly instead of guessing them.
|
|
685
|
+
|
|
686
|
+
Not every expression is a pattern: ``x + 1`` is not, because matching it
|
|
687
|
+
would require inverting addition. When that happens we raise
|
|
688
|
+
:class:`ParseError`, and the binder falls back to enumerating the
|
|
689
|
+
variable's colour set and *evaluating* the expression instead. So this
|
|
690
|
+
function failing is a performance question, not a correctness one.
|
|
691
|
+
"""
|
|
692
|
+
role = identifier_role or default_identifier_role
|
|
693
|
+
|
|
694
|
+
def convert(node: Expr) -> Pattern:
|
|
695
|
+
if isinstance(node, Literal):
|
|
696
|
+
return PLiteral(node.value, node.position)
|
|
697
|
+
|
|
698
|
+
if isinstance(node, Var):
|
|
699
|
+
kind = role(node.name)
|
|
700
|
+
if kind == "constructor":
|
|
701
|
+
return PConstructor(node.name, None, node.position)
|
|
702
|
+
if kind == "variable":
|
|
703
|
+
return PVar(node.name, node.position)
|
|
704
|
+
# An ML constant or a function name: not something we can match
|
|
705
|
+
# against, so the whole inscription must be evaluated instead.
|
|
706
|
+
raise ParseError(
|
|
707
|
+
f"'{node.name}' is not a variable or a constructor, so this "
|
|
708
|
+
"expression cannot be used as a pattern",
|
|
709
|
+
node.position,
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
if isinstance(node, TupleExpr):
|
|
713
|
+
return PTuple(tuple(convert(i) for i in node.items), node.position)
|
|
714
|
+
|
|
715
|
+
if isinstance(node, ListExpr):
|
|
716
|
+
return PList(tuple(convert(i) for i in node.items), node.position)
|
|
717
|
+
|
|
718
|
+
if isinstance(node, RecordExpr):
|
|
719
|
+
return PRecord(
|
|
720
|
+
tuple((n, convert(v)) for n, v in node.fields), False, node.position
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
if isinstance(node, BinOp) and node.operator == "::":
|
|
724
|
+
return PCons(convert(node.left), convert(node.right), node.position)
|
|
725
|
+
|
|
726
|
+
if isinstance(node, App) and isinstance(node.function, Var):
|
|
727
|
+
# `Car (a, b)` -- but only if `Car` really is a constructor. A
|
|
728
|
+
# capitalised *function* such as `Chopsticks(p)` must not be read
|
|
729
|
+
# as a pattern, or we would try to match tokens against it.
|
|
730
|
+
if role(node.function.name) == "constructor":
|
|
731
|
+
return PConstructor(
|
|
732
|
+
node.function.name, convert(node.argument), node.position
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
raise ParseError("this expression cannot be used as a pattern", node.position)
|
|
736
|
+
|
|
737
|
+
return convert(expression)
|