lorebase-core 0.2.3__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.
- lorebase_core/__init__.py +1 -0
- lorebase_core/canonical/__init__.py +35 -0
- lorebase_core/canonical/hashing.py +128 -0
- lorebase_core/canonical/jcs.py +162 -0
- lorebase_core/expr/__init__.py +118 -0
- lorebase_core/expr/ast.py +443 -0
- lorebase_core/expr/checker.py +428 -0
- lorebase_core/expr/compiler.py +87 -0
- lorebase_core/expr/context.py +82 -0
- lorebase_core/expr/derivations.py +306 -0
- lorebase_core/expr/environment.py +176 -0
- lorebase_core/expr/errors.py +98 -0
- lorebase_core/expr/evaluator.py +414 -0
- lorebase_core/expr/functions.py +898 -0
- lorebase_core/expr/limits.py +52 -0
- lorebase_core/expr/operations.py +479 -0
- lorebase_core/expr/ops.py +243 -0
- lorebase_core/expr/resolver.py +96 -0
- lorebase_core/expr/rows.py +148 -0
- lorebase_core/expr/sensitivity.py +337 -0
- lorebase_core/expr/types.py +227 -0
- lorebase_core/expr/validation.py +51 -0
- lorebase_core/expr/values.py +169 -0
- lorebase_core/expr/visibility.py +363 -0
- lorebase_core/infer/__init__.py +36 -0
- lorebase_core/infer/columns.py +621 -0
- lorebase_core/infer/engine.py +546 -0
- lorebase_core/infer/keys.py +164 -0
- lorebase_core/infer/names.py +145 -0
- lorebase_core/infer/options.py +290 -0
- lorebase_core/infer/profile.py +54 -0
- lorebase_core/infer/proposal.py +122 -0
- lorebase_core/infer/references.py +314 -0
- lorebase_core/infer/suggestions.py +731 -0
- lorebase_core/infer/tables.py +81 -0
- lorebase_core/merge/__init__.py +89 -0
- lorebase_core/merge/conflicts.py +262 -0
- lorebase_core/merge/consistency.py +440 -0
- lorebase_core/merge/data_merge.py +522 -0
- lorebase_core/merge/engine.py +476 -0
- lorebase_core/merge/index.py +100 -0
- lorebase_core/merge/objects.py +204 -0
- lorebase_core/merge/policies.py +322 -0
- lorebase_core/merge/resolve.py +321 -0
- lorebase_core/merge/schema_merge.py +429 -0
- lorebase_core/merge/state.py +443 -0
- lorebase_core/model/__init__.py +245 -0
- lorebase_core/model/attribute.py +72 -0
- lorebase_core/model/base.py +61 -0
- lorebase_core/model/derivation.py +95 -0
- lorebase_core/model/domain.py +71 -0
- lorebase_core/model/embedding.py +193 -0
- lorebase_core/model/ids.py +78 -0
- lorebase_core/model/kind.py +85 -0
- lorebase_core/model/lineage.py +114 -0
- lorebase_core/model/operation.py +161 -0
- lorebase_core/model/paths.py +141 -0
- lorebase_core/model/record.py +127 -0
- lorebase_core/model/relation.py +83 -0
- lorebase_core/model/schema.py +219 -0
- lorebase_core/model/source.py +204 -0
- lorebase_core/model/trait.py +37 -0
- lorebase_core/model/vault.py +43 -0
- lorebase_core/model/yaml_schema.py +1058 -0
- lorebase_core/query/__init__.py +97 -0
- lorebase_core/query/dsl.py +227 -0
- lorebase_core/query/resolve.py +794 -0
- lorebase_core/revision/__init__.py +59 -0
- lorebase_core/revision/ancestry.py +272 -0
- lorebase_core/revision/changeset.py +62 -0
- lorebase_core/revision/revision.py +121 -0
- lorebase_core/tree/__init__.py +76 -0
- lorebase_core/tree/diff.py +119 -0
- lorebase_core/tree/hamt.py +333 -0
- lorebase_core/tree/node.py +280 -0
- lorebase_core/tree/params.py +33 -0
- lorebase_core/tree/root.py +70 -0
- lorebase_core/tree/store.py +98 -0
- lorebase_core/types/__init__.py +71 -0
- lorebase_core/types/casts.py +271 -0
- lorebase_core/types/constraints.py +299 -0
- lorebase_core/types/encoding.py +538 -0
- lorebase_core/types/errors.py +41 -0
- lorebase_core/types/matching.py +40 -0
- lorebase_core/types/types.py +187 -0
- lorebase_core/validate/__init__.py +49 -0
- lorebase_core/validate/changes.py +70 -0
- lorebase_core/validate/changeset.py +551 -0
- lorebase_core/validate/data_rules.py +291 -0
- lorebase_core/validate/issues.py +123 -0
- lorebase_core/validate/schema_rules.py +1304 -0
- lorebase_core-0.2.3.dist-info/METADATA +9 -0
- lorebase_core-0.2.3.dist-info/RECORD +94 -0
- lorebase_core-0.2.3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""Static type checking of an AST against an Environment.
|
|
2
|
+
|
|
3
|
+
Every node gets a type; the first problem raises ``ExprError`` (code ``expression.type``)
|
|
4
|
+
positioned at the offending node. ``dyn`` (json Attributes, map values of unknown type)
|
|
5
|
+
defers checks to evaluation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import difflib
|
|
11
|
+
|
|
12
|
+
from lorebase_core.expr import functions
|
|
13
|
+
from lorebase_core.expr.ast import (
|
|
14
|
+
STEP_SYNTAX,
|
|
15
|
+
STEP_VARIABLE,
|
|
16
|
+
Binary,
|
|
17
|
+
Call,
|
|
18
|
+
Comprehension,
|
|
19
|
+
Conditional,
|
|
20
|
+
Ident,
|
|
21
|
+
Index,
|
|
22
|
+
ListLit,
|
|
23
|
+
Literal,
|
|
24
|
+
MapLit,
|
|
25
|
+
Node,
|
|
26
|
+
Select,
|
|
27
|
+
Unary,
|
|
28
|
+
)
|
|
29
|
+
from lorebase_core.expr.environment import Environment
|
|
30
|
+
from lorebase_core.expr.errors import Position, type_error
|
|
31
|
+
from lorebase_core.expr.types import (
|
|
32
|
+
BOOL,
|
|
33
|
+
DATE,
|
|
34
|
+
DECIMAL,
|
|
35
|
+
DURATION,
|
|
36
|
+
DYN,
|
|
37
|
+
INT,
|
|
38
|
+
NUMERIC,
|
|
39
|
+
ORDERED,
|
|
40
|
+
STRING,
|
|
41
|
+
TIMESTAMP,
|
|
42
|
+
ExprType,
|
|
43
|
+
TypeKind,
|
|
44
|
+
join,
|
|
45
|
+
list_of,
|
|
46
|
+
map_of,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
K = TypeKind
|
|
50
|
+
_MAP_KEYS = frozenset({K.STRING, K.INT, K.BOOL, K.DYN})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Checker:
|
|
54
|
+
def __init__(self, environment: Environment) -> None:
|
|
55
|
+
self.env = environment
|
|
56
|
+
self.types: dict[Node, ExprType] = {}
|
|
57
|
+
self.scopes: list[dict[str, ExprType]] = []
|
|
58
|
+
|
|
59
|
+
def describe(self, expr_type: ExprType) -> str:
|
|
60
|
+
return self.env.describe(expr_type)
|
|
61
|
+
|
|
62
|
+
def check(self, node: Node) -> ExprType:
|
|
63
|
+
result = self._check(node)
|
|
64
|
+
self.types[node] = result
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
def _check(self, node: Node) -> ExprType:
|
|
68
|
+
if isinstance(node, Literal):
|
|
69
|
+
return node.type
|
|
70
|
+
if isinstance(node, Ident):
|
|
71
|
+
return self._ident(node)
|
|
72
|
+
if isinstance(node, Select):
|
|
73
|
+
return self._select(node)
|
|
74
|
+
if isinstance(node, Index):
|
|
75
|
+
return self._index(node)
|
|
76
|
+
if isinstance(node, Call):
|
|
77
|
+
return self._call(node)
|
|
78
|
+
if isinstance(node, Comprehension):
|
|
79
|
+
return self._comprehension(node)
|
|
80
|
+
if isinstance(node, ListLit):
|
|
81
|
+
items = [self.check(item) for item in node.items]
|
|
82
|
+
element = DYN
|
|
83
|
+
if items:
|
|
84
|
+
element = items[0]
|
|
85
|
+
for item in items[1:]:
|
|
86
|
+
element = join(element, item)
|
|
87
|
+
return list_of(element)
|
|
88
|
+
if isinstance(node, MapLit):
|
|
89
|
+
return self._map(node)
|
|
90
|
+
if isinstance(node, Unary):
|
|
91
|
+
return self._unary(node)
|
|
92
|
+
if isinstance(node, Binary):
|
|
93
|
+
return self._binary(node)
|
|
94
|
+
if isinstance(node, Conditional):
|
|
95
|
+
return self._conditional(node)
|
|
96
|
+
raise type_error(f"unsupported expression {type(node).__name__}", node.pos)
|
|
97
|
+
|
|
98
|
+
# Names and fields -------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def _ident(self, node: Ident) -> ExprType:
|
|
101
|
+
for scope in reversed(self.scopes):
|
|
102
|
+
if node.name in scope:
|
|
103
|
+
return scope[node.name]
|
|
104
|
+
found = self.env.variables.get(node.name)
|
|
105
|
+
if found is not None:
|
|
106
|
+
return found
|
|
107
|
+
if node.name == STEP_VARIABLE:
|
|
108
|
+
if self.env.step_types is None:
|
|
109
|
+
raise type_error(f"{STEP_SYNTAX}[n] is only available in Operation steps", node.pos)
|
|
110
|
+
raise type_error(f"use {STEP_SYNTAX}[n] with a literal step number", node.pos)
|
|
111
|
+
names = [*self.env.variables, *(n for scope in self.scopes for n in scope)]
|
|
112
|
+
raise type_error(f"unknown name {node.name!r}{_suggest(node.name, names)}", node.pos)
|
|
113
|
+
|
|
114
|
+
def _select(self, node: Select) -> ExprType:
|
|
115
|
+
operand = self.check(node.operand)
|
|
116
|
+
result = self._field(operand, node.field, node.pos)
|
|
117
|
+
return BOOL if node.test else result
|
|
118
|
+
|
|
119
|
+
def _field(self, operand: ExprType, name: str, pos: Position) -> ExprType:
|
|
120
|
+
if operand.kind is K.DYN:
|
|
121
|
+
return DYN
|
|
122
|
+
if operand.kind is K.MAP:
|
|
123
|
+
if operand.args[0].kind not in (K.STRING, K.DYN):
|
|
124
|
+
raise type_error(f"cannot select {name!r} from {self.describe(operand)}", pos)
|
|
125
|
+
return operand.args[1]
|
|
126
|
+
if operand.kind is K.REF:
|
|
127
|
+
if name == "key":
|
|
128
|
+
return STRING
|
|
129
|
+
raise type_error(
|
|
130
|
+
f"{self.describe(operand)} is a reference; read its attributes with "
|
|
131
|
+
f"record(...).{name}",
|
|
132
|
+
pos,
|
|
133
|
+
)
|
|
134
|
+
fields = self.env.fields_of(operand)
|
|
135
|
+
if fields is None:
|
|
136
|
+
raise type_error(f"{self.describe(operand)} has no fields (selecting {name!r})", pos)
|
|
137
|
+
found = fields.get(name)
|
|
138
|
+
if found is None:
|
|
139
|
+
owner = self.describe(operand)
|
|
140
|
+
if operand.kind is K.RECORD and operand.name is not None:
|
|
141
|
+
owner = f"Kind {self.env.kind_name(operand.name)}"
|
|
142
|
+
raise type_error(f"{owner} has no field {name!r}{_suggest(name, list(fields))}", pos)
|
|
143
|
+
return found
|
|
144
|
+
|
|
145
|
+
def _index(self, node: Index) -> ExprType:
|
|
146
|
+
if (
|
|
147
|
+
isinstance(node.operand, Ident)
|
|
148
|
+
and node.operand.name == STEP_VARIABLE
|
|
149
|
+
and STEP_VARIABLE not in self.env.variables
|
|
150
|
+
and not any(STEP_VARIABLE in scope for scope in self.scopes)
|
|
151
|
+
):
|
|
152
|
+
return self._step(node)
|
|
153
|
+
operand = self.check(node.operand)
|
|
154
|
+
index = self.check(node.index)
|
|
155
|
+
if operand.kind is K.DYN:
|
|
156
|
+
return DYN
|
|
157
|
+
if operand.kind is K.LIST:
|
|
158
|
+
if index.kind not in (K.INT, K.DYN):
|
|
159
|
+
raise type_error(f"list index must be an int, got {self.describe(index)}", node.pos)
|
|
160
|
+
return operand.element
|
|
161
|
+
if operand.kind is K.MAP:
|
|
162
|
+
if not _comparable(index, operand.args[0]):
|
|
163
|
+
raise type_error(
|
|
164
|
+
f"map key must be {self.describe(operand.args[0])}, got {self.describe(index)}",
|
|
165
|
+
node.pos,
|
|
166
|
+
)
|
|
167
|
+
return operand.args[1]
|
|
168
|
+
raise type_error(f"cannot index {self.describe(operand)}", node.pos)
|
|
169
|
+
|
|
170
|
+
def _step(self, node: Index) -> ExprType:
|
|
171
|
+
steps = self.env.step_types
|
|
172
|
+
if steps is None:
|
|
173
|
+
raise type_error(f"{STEP_SYNTAX}[n] is only available in Operation steps", node.pos)
|
|
174
|
+
if not isinstance(node.index, Literal) or not isinstance(node.index.value, int):
|
|
175
|
+
raise type_error(f"{STEP_SYNTAX}[n] needs a literal step number", node.pos)
|
|
176
|
+
number = node.index.value
|
|
177
|
+
if isinstance(number, bool) or not 0 <= number < len(steps):
|
|
178
|
+
raise type_error(
|
|
179
|
+
f"{STEP_SYNTAX}[{number}] does not name an earlier step "
|
|
180
|
+
f"({len(steps)} step(s) before this one)",
|
|
181
|
+
node.pos,
|
|
182
|
+
)
|
|
183
|
+
found = steps[number]
|
|
184
|
+
if found is None:
|
|
185
|
+
raise type_error(f"step {number} edits a Link, not a Record", node.pos)
|
|
186
|
+
self.types[node.operand] = list_of(DYN)
|
|
187
|
+
self.types[node.index] = INT
|
|
188
|
+
return found
|
|
189
|
+
|
|
190
|
+
# Calls ----------------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
def _call(self, node: Call) -> ExprType:
|
|
193
|
+
member = node.target is not None
|
|
194
|
+
function = functions.lookup(node.function, member)
|
|
195
|
+
if function is None:
|
|
196
|
+
other = functions.lookup(node.function, not member)
|
|
197
|
+
hint = ""
|
|
198
|
+
if other is not None:
|
|
199
|
+
hint = (
|
|
200
|
+
f"; call it as {node.function}(x)"
|
|
201
|
+
if member
|
|
202
|
+
else f"; call it as x.{node.function}()"
|
|
203
|
+
)
|
|
204
|
+
raise type_error(f"unknown function {node.function!r}{hint}", node.pos)
|
|
205
|
+
if function.records and not self.env.records:
|
|
206
|
+
raise type_error(
|
|
207
|
+
f"{node.function}() is not available here: Derivations read only their own "
|
|
208
|
+
"Record and links.<relation>",
|
|
209
|
+
node.pos,
|
|
210
|
+
)
|
|
211
|
+
args = tuple(self.check(arg) for arg in node.args)
|
|
212
|
+
if node.target is not None:
|
|
213
|
+
args = (self.check(node.target), *args)
|
|
214
|
+
return function.check(functions.CallSite(node, args, self.env))
|
|
215
|
+
|
|
216
|
+
def _comprehension(self, node: Comprehension) -> ExprType:
|
|
217
|
+
target = self.check(node.target)
|
|
218
|
+
if target.kind is K.LIST:
|
|
219
|
+
variable = target.element
|
|
220
|
+
elif target.kind is K.MAP:
|
|
221
|
+
variable = target.args[0]
|
|
222
|
+
elif target.kind is K.DYN:
|
|
223
|
+
variable = DYN
|
|
224
|
+
else:
|
|
225
|
+
raise type_error(
|
|
226
|
+
f"{node.macro}() needs a list or a map, got {self.describe(target)}", node.pos
|
|
227
|
+
)
|
|
228
|
+
self.scopes.append({node.variable: variable})
|
|
229
|
+
try:
|
|
230
|
+
body = self.check(node.body)
|
|
231
|
+
finally:
|
|
232
|
+
self.scopes.pop()
|
|
233
|
+
if node.macro == "map":
|
|
234
|
+
return list_of(body)
|
|
235
|
+
if body.kind not in (K.BOOL, K.DYN):
|
|
236
|
+
raise type_error(
|
|
237
|
+
f"{node.macro}() needs a bool condition, got {self.describe(body)}", node.body.pos
|
|
238
|
+
)
|
|
239
|
+
if node.macro == "filter":
|
|
240
|
+
return target if target.kind is K.LIST else list_of(variable)
|
|
241
|
+
if node.macro == "count":
|
|
242
|
+
return INT
|
|
243
|
+
return BOOL
|
|
244
|
+
|
|
245
|
+
def _map(self, node: MapLit) -> ExprType:
|
|
246
|
+
key_type: ExprType | None = None
|
|
247
|
+
value_type: ExprType | None = None
|
|
248
|
+
for key_node, value_node in node.entries:
|
|
249
|
+
key = self.check(key_node)
|
|
250
|
+
value = self.check(value_node)
|
|
251
|
+
if key.kind not in _MAP_KEYS:
|
|
252
|
+
raise type_error(
|
|
253
|
+
f"map keys must be strings, ints or bools, got {self.describe(key)}",
|
|
254
|
+
key_node.pos,
|
|
255
|
+
)
|
|
256
|
+
key_type = key if key_type is None else join(key_type, key)
|
|
257
|
+
value_type = value if value_type is None else join(value_type, value)
|
|
258
|
+
return map_of((key_type or DYN).plain(), value_type or DYN)
|
|
259
|
+
|
|
260
|
+
# Operators ------------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
def _unary(self, node: Unary) -> ExprType:
|
|
263
|
+
operand = self.check(node.operand)
|
|
264
|
+
if operand.kind is K.DYN:
|
|
265
|
+
return BOOL if node.op == "!" else DYN
|
|
266
|
+
if node.op == "!":
|
|
267
|
+
if operand.kind is not K.BOOL:
|
|
268
|
+
raise type_error(f"! needs a bool, got {self.describe(operand)}", node.pos)
|
|
269
|
+
return BOOL
|
|
270
|
+
if operand.kind not in NUMERIC | {K.DURATION}:
|
|
271
|
+
raise type_error(f"cannot negate {self.describe(operand)}", node.pos)
|
|
272
|
+
return operand
|
|
273
|
+
|
|
274
|
+
def _binary(self, node: Binary) -> ExprType:
|
|
275
|
+
left = self.check(node.left)
|
|
276
|
+
right = self.check(node.right)
|
|
277
|
+
op = node.op
|
|
278
|
+
if op in ("&&", "||"):
|
|
279
|
+
for side, side_type in ((node.left, left), (node.right, right)):
|
|
280
|
+
if side_type.kind not in (K.BOOL, K.DYN):
|
|
281
|
+
raise type_error(
|
|
282
|
+
f"{op} needs bool operands, got {self.describe(side_type)}", side.pos
|
|
283
|
+
)
|
|
284
|
+
return BOOL
|
|
285
|
+
if op in ("==", "!="):
|
|
286
|
+
self._require_comparable(left, right, node)
|
|
287
|
+
return BOOL
|
|
288
|
+
if op in ("<", "<=", ">", ">="):
|
|
289
|
+
if not _orderable(left, right):
|
|
290
|
+
raise type_error(
|
|
291
|
+
f"cannot order {self.describe(left)} and {self.describe(right)}", node.pos
|
|
292
|
+
)
|
|
293
|
+
return BOOL
|
|
294
|
+
if op == "in":
|
|
295
|
+
return self._in(left, right, node)
|
|
296
|
+
result = _arithmetic(op, left, right)
|
|
297
|
+
if result is None:
|
|
298
|
+
hint = ""
|
|
299
|
+
kinds = {left.kind, right.kind}
|
|
300
|
+
if kinds == {K.INT, K.DOUBLE}:
|
|
301
|
+
hint = "; convert with double() or int()"
|
|
302
|
+
elif kinds == {K.DECIMAL, K.DOUBLE}:
|
|
303
|
+
hint = "; convert with decimal()"
|
|
304
|
+
raise type_error(
|
|
305
|
+
f"operator {op} is not defined for {self.describe(left)} and "
|
|
306
|
+
f"{self.describe(right)}{hint}",
|
|
307
|
+
node.pos,
|
|
308
|
+
)
|
|
309
|
+
return result
|
|
310
|
+
|
|
311
|
+
def _require_comparable(self, left: ExprType, right: ExprType, node: Node) -> None:
|
|
312
|
+
if not _comparable(left, right):
|
|
313
|
+
hint = ""
|
|
314
|
+
if {left.kind, right.kind} == {K.REF, K.STRING}:
|
|
315
|
+
hint = "; compare the reference's .key with a string"
|
|
316
|
+
raise type_error(
|
|
317
|
+
f"cannot compare {self.describe(left)} with {self.describe(right)}{hint}", node.pos
|
|
318
|
+
)
|
|
319
|
+
if (
|
|
320
|
+
left.kind is K.STRING
|
|
321
|
+
and right.kind is K.STRING
|
|
322
|
+
and (left.enum or right.enum)
|
|
323
|
+
and left.values is not None
|
|
324
|
+
and right.values is not None
|
|
325
|
+
and not left.values & right.values
|
|
326
|
+
):
|
|
327
|
+
enum, other = (left, right) if left.enum else (right, left)
|
|
328
|
+
assert other.values is not None
|
|
329
|
+
assert enum.values is not None
|
|
330
|
+
raise type_error(
|
|
331
|
+
f"{', '.join(repr(v) for v in sorted(other.values))} is not a value of "
|
|
332
|
+
f"{self.describe(enum)}: the comparison is always the same",
|
|
333
|
+
node.pos,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def _in(self, left: ExprType, right: ExprType, node: Binary) -> ExprType:
|
|
337
|
+
if right.kind is K.DYN:
|
|
338
|
+
return BOOL
|
|
339
|
+
if right.kind not in (K.LIST, K.MAP):
|
|
340
|
+
raise type_error(f"'in' needs a list or a map, got {self.describe(right)}", node.pos)
|
|
341
|
+
self._require_comparable(left, right.element, node)
|
|
342
|
+
return BOOL
|
|
343
|
+
|
|
344
|
+
def _conditional(self, node: Conditional) -> ExprType:
|
|
345
|
+
condition = self.check(node.condition)
|
|
346
|
+
if condition.kind not in (K.BOOL, K.DYN):
|
|
347
|
+
raise type_error(
|
|
348
|
+
f"condition must be a bool, got {self.describe(condition)}", node.condition.pos
|
|
349
|
+
)
|
|
350
|
+
then = self.check(node.then)
|
|
351
|
+
otherwise = self.check(node.otherwise)
|
|
352
|
+
result = join(then, otherwise)
|
|
353
|
+
if result.kind is K.DYN and K.DYN not in (then.kind, otherwise.kind):
|
|
354
|
+
raise type_error(
|
|
355
|
+
f"branches have different types: {self.describe(then)} and "
|
|
356
|
+
f"{self.describe(otherwise)}",
|
|
357
|
+
node.pos,
|
|
358
|
+
)
|
|
359
|
+
return result
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _suggest(name: str, candidates: list[str]) -> str:
|
|
363
|
+
close = difflib.get_close_matches(name, candidates, n=1)
|
|
364
|
+
return f" (did you mean {close[0]!r}?)" if close else ""
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _comparable(left: ExprType, right: ExprType) -> bool:
|
|
368
|
+
kinds = {left.kind, right.kind}
|
|
369
|
+
if K.DYN in kinds or K.NULL in kinds:
|
|
370
|
+
return True
|
|
371
|
+
if left.is_numeric and right.is_numeric:
|
|
372
|
+
return True
|
|
373
|
+
if left.kind is not right.kind:
|
|
374
|
+
return False
|
|
375
|
+
if left.kind in (K.REF, K.RECORD, K.OBJECT):
|
|
376
|
+
return left.name == right.name
|
|
377
|
+
if left.kind in (K.LIST, K.MAP):
|
|
378
|
+
return all(_comparable(a, b) for a, b in zip(left.args, right.args, strict=True))
|
|
379
|
+
return True
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _orderable(left: ExprType, right: ExprType) -> bool:
|
|
383
|
+
if K.DYN in (left.kind, right.kind):
|
|
384
|
+
return True
|
|
385
|
+
if left.is_numeric and right.is_numeric:
|
|
386
|
+
return True
|
|
387
|
+
return left.kind is right.kind and left.kind in ORDERED
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
_TEMPORAL: dict[tuple[str, TypeKind, TypeKind], ExprType] = {
|
|
391
|
+
("+", K.TIMESTAMP, K.DURATION): TIMESTAMP,
|
|
392
|
+
("+", K.DURATION, K.TIMESTAMP): TIMESTAMP,
|
|
393
|
+
("+", K.DATE, K.DURATION): DATE,
|
|
394
|
+
("+", K.DURATION, K.DATE): DATE,
|
|
395
|
+
("+", K.DURATION, K.DURATION): DURATION,
|
|
396
|
+
("-", K.TIMESTAMP, K.TIMESTAMP): DURATION,
|
|
397
|
+
("-", K.TIMESTAMP, K.DURATION): TIMESTAMP,
|
|
398
|
+
("-", K.DATE, K.DATE): DURATION,
|
|
399
|
+
("-", K.DATE, K.DURATION): DATE,
|
|
400
|
+
("-", K.DURATION, K.DURATION): DURATION,
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _arithmetic(op: str, left: ExprType, right: ExprType) -> ExprType | None:
|
|
405
|
+
"""Result type of ``left op right``, ``None`` when the operator does not apply."""
|
|
406
|
+
if K.DYN in (left.kind, right.kind):
|
|
407
|
+
other = right if left.kind is K.DYN else left
|
|
408
|
+
if other.kind in (K.BOOL, K.NULL):
|
|
409
|
+
return None
|
|
410
|
+
return DYN
|
|
411
|
+
if left.is_numeric and right.is_numeric:
|
|
412
|
+
if op == "%":
|
|
413
|
+
return (
|
|
414
|
+
INT
|
|
415
|
+
if left.kind is right.kind is K.INT
|
|
416
|
+
else (DECIMAL if K.DOUBLE not in (left.kind, right.kind) else None)
|
|
417
|
+
)
|
|
418
|
+
if left.kind is right.kind:
|
|
419
|
+
return left.plain()
|
|
420
|
+
if K.DOUBLE in (left.kind, right.kind):
|
|
421
|
+
return None
|
|
422
|
+
return DECIMAL
|
|
423
|
+
if op == "+" and left.kind is right.kind:
|
|
424
|
+
if left.kind in (K.STRING, K.BYTES):
|
|
425
|
+
return left.plain()
|
|
426
|
+
if left.kind is K.LIST:
|
|
427
|
+
return list_of(join(left.element, right.element))
|
|
428
|
+
return _TEMPORAL.get((op, left.kind, right.kind))
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Compile an expression (parse and type check) and run a compiled expression."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from lorebase_core.expr.ast import Node, parse
|
|
10
|
+
from lorebase_core.expr.checker import Checker
|
|
11
|
+
from lorebase_core.expr.environment import Environment
|
|
12
|
+
from lorebase_core.expr.errors import ExprError, type_error
|
|
13
|
+
from lorebase_core.expr.evaluator import Evaluator, Overlay
|
|
14
|
+
from lorebase_core.expr.limits import ExprLimits
|
|
15
|
+
from lorebase_core.expr.resolver import RecordResolver
|
|
16
|
+
from lorebase_core.expr.types import ExprType, assignable
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, eq=False)
|
|
20
|
+
class CompiledExpression:
|
|
21
|
+
"""A parsed and type-checked expression, ready to run any number of times."""
|
|
22
|
+
|
|
23
|
+
source: str
|
|
24
|
+
root: Node
|
|
25
|
+
type: ExprType
|
|
26
|
+
environment: Environment
|
|
27
|
+
limits: ExprLimits
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def compile(
|
|
31
|
+
expression: str,
|
|
32
|
+
environment: Environment,
|
|
33
|
+
*,
|
|
34
|
+
expected: ExprType | None = None,
|
|
35
|
+
nullable: bool = False,
|
|
36
|
+
limits: ExprLimits | None = None,
|
|
37
|
+
) -> CompiledExpression:
|
|
38
|
+
"""Parse and type check ``expression`` in ``environment``.
|
|
39
|
+
|
|
40
|
+
With ``expected``, the result type must be assignable to it (``nullable`` also accepts
|
|
41
|
+
``null``). Raises ``ExprError`` with code ``expression.syntax``, ``expression.type`` or
|
|
42
|
+
``expression.limit_exceeded`` and the position of the problem.
|
|
43
|
+
"""
|
|
44
|
+
limits = limits or ExprLimits()
|
|
45
|
+
root = parse(expression, limits)
|
|
46
|
+
try:
|
|
47
|
+
result = Checker(environment).check(root)
|
|
48
|
+
if expected is not None and not assignable(result, expected, nullable=nullable):
|
|
49
|
+
raise type_error(
|
|
50
|
+
f"expression has type {environment.describe(result)}, "
|
|
51
|
+
f"expected {environment.describe(expected)}",
|
|
52
|
+
root.pos,
|
|
53
|
+
)
|
|
54
|
+
except ExprError as error:
|
|
55
|
+
raise error.located(source=expression) from None
|
|
56
|
+
except RecursionError:
|
|
57
|
+
raise type_error("expression nests too deeply to check").located(
|
|
58
|
+
source=expression
|
|
59
|
+
) from None
|
|
60
|
+
return CompiledExpression(expression, root, result, environment, limits)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run(
|
|
64
|
+
compiled: CompiledExpression,
|
|
65
|
+
variables: Mapping[str, object],
|
|
66
|
+
*,
|
|
67
|
+
now: datetime,
|
|
68
|
+
resolver: RecordResolver | None = None,
|
|
69
|
+
limits: ExprLimits | None = None,
|
|
70
|
+
overlay: Overlay | None = None,
|
|
71
|
+
) -> object:
|
|
72
|
+
"""Evaluate ``compiled`` with runtime ``variables``; returns the runtime value.
|
|
73
|
+
|
|
74
|
+
Raises ``ExprError`` for evaluation problems and lets ``RecordNotLoaded`` through.
|
|
75
|
+
"""
|
|
76
|
+
evaluator = Evaluator(
|
|
77
|
+
compiled.environment,
|
|
78
|
+
variables,
|
|
79
|
+
now=now,
|
|
80
|
+
limits=limits or compiled.limits,
|
|
81
|
+
resolver=resolver,
|
|
82
|
+
overlay=overlay,
|
|
83
|
+
)
|
|
84
|
+
try:
|
|
85
|
+
return evaluator.evaluate(compiled.root)
|
|
86
|
+
except ExprError as error:
|
|
87
|
+
raise error.located(source=compiled.source) from None
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""What a caller supplies to evaluate: the clock, Records, the principal, the budget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from pydantic import JsonValue
|
|
10
|
+
|
|
11
|
+
from lorebase_core.expr.ast import STEP_VARIABLE
|
|
12
|
+
from lorebase_core.expr.environment import KEY, LINKS, PARAMS, PRINCIPAL, Environment
|
|
13
|
+
from lorebase_core.expr.errors import evaluation_error
|
|
14
|
+
from lorebase_core.expr.limits import ExprLimits
|
|
15
|
+
from lorebase_core.expr.resolver import RecordResolver
|
|
16
|
+
from lorebase_core.expr.values import LinksValue, ObjectValue, RecordValue, Ref, to_runtime
|
|
17
|
+
from lorebase_core.model.record import Record
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class Principal:
|
|
22
|
+
"""The caller, as expressions see it: ``principal.id`` and ``principal.subjects``."""
|
|
23
|
+
|
|
24
|
+
id: str
|
|
25
|
+
subjects: tuple[str, ...] = ()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class EvaluationContext:
|
|
30
|
+
"""Inputs of one evaluation besides the expression's own variables.
|
|
31
|
+
|
|
32
|
+
``now`` is the clock (timezone-aware); ``resolver`` answers ``record()`` and ``links``;
|
|
33
|
+
``limits`` overrides the limits the expression was compiled with; ``mint_key`` mints
|
|
34
|
+
keys of Records created by steps of Kinds without a natural key.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
now: datetime
|
|
38
|
+
resolver: RecordResolver | None = None
|
|
39
|
+
principal: Principal | None = None
|
|
40
|
+
limits: ExprLimits | None = None
|
|
41
|
+
mint_key: Callable[[], str] | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def activation(
|
|
45
|
+
environment: Environment,
|
|
46
|
+
*,
|
|
47
|
+
record: Record | None = None,
|
|
48
|
+
parameters: Mapping[str, JsonValue] | None = None,
|
|
49
|
+
principal: Principal | None = None,
|
|
50
|
+
steps: Sequence[Ref | None] | None = None,
|
|
51
|
+
) -> dict[str, object]:
|
|
52
|
+
"""Runtime variables of ``environment`` from canonical inputs.
|
|
53
|
+
|
|
54
|
+
``record`` is required when the environment has a subject Kind (its Attributes are
|
|
55
|
+
variables); ``parameters`` are canonical parameter values by name.
|
|
56
|
+
"""
|
|
57
|
+
variables: dict[str, object] = {}
|
|
58
|
+
if environment.subject is not None:
|
|
59
|
+
if record is None or record.kind != environment.subject:
|
|
60
|
+
name = environment.kind_name(environment.subject)
|
|
61
|
+
raise evaluation_error(f"this expression needs a Record of Kind {name}")
|
|
62
|
+
kind = environment.schema.kinds[record.kind]
|
|
63
|
+
subject = RecordValue(kind, record.key, record.values)
|
|
64
|
+
for attribute in kind.attributes.values():
|
|
65
|
+
variables[attribute.api_name] = subject.get(attribute.api_name)
|
|
66
|
+
variables[KEY] = record.key
|
|
67
|
+
variables[LINKS] = LinksValue(kind, record.key)
|
|
68
|
+
if PARAMS in environment.objects:
|
|
69
|
+
given = parameters or {}
|
|
70
|
+
values = {
|
|
71
|
+
name: to_runtime(spec, given.get(name)) for name, spec in environment.parameters.items()
|
|
72
|
+
}
|
|
73
|
+
variables.update(values)
|
|
74
|
+
variables[PARAMS] = ObjectValue(PARAMS, values)
|
|
75
|
+
variables[PRINCIPAL] = (
|
|
76
|
+
None
|
|
77
|
+
if principal is None
|
|
78
|
+
else ObjectValue(PRINCIPAL, {"id": principal.id, "subjects": list(principal.subjects)})
|
|
79
|
+
)
|
|
80
|
+
if steps is not None:
|
|
81
|
+
variables[STEP_VARIABLE] = list(steps)
|
|
82
|
+
return variables
|