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,414 @@
|
|
|
1
|
+
"""Bounded, deterministic evaluation of a checked AST.
|
|
2
|
+
|
|
3
|
+
The budget comes from ``ExprLimits``: steps (one per node visited), wall-clock time (checked
|
|
4
|
+
every few steps, a safety net only), sizes of built strings and lists, and Record lookups.
|
|
5
|
+
The clock is the ``now`` the caller passes: ``now()`` and ``today()`` never read the system
|
|
6
|
+
time, so the same inputs give the same result. Nothing here performs I/O; Records come from
|
|
7
|
+
the caller's ``RecordResolver``.
|
|
8
|
+
|
|
9
|
+
``&&`` and ``||`` follow CEL: an error on one side is absorbed when the other side decides
|
|
10
|
+
the result. Comprehensions stop at the first deciding element and propagate errors.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import time
|
|
16
|
+
from collections.abc import Callable, Mapping
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from decimal import ROUND_HALF_EVEN, Context
|
|
19
|
+
|
|
20
|
+
from pydantic import JsonValue
|
|
21
|
+
|
|
22
|
+
from lorebase_core.expr import functions, ops
|
|
23
|
+
from lorebase_core.expr.ast import (
|
|
24
|
+
Binary,
|
|
25
|
+
Call,
|
|
26
|
+
Comprehension,
|
|
27
|
+
Conditional,
|
|
28
|
+
Ident,
|
|
29
|
+
Index,
|
|
30
|
+
ListLit,
|
|
31
|
+
Literal,
|
|
32
|
+
MapLit,
|
|
33
|
+
Node,
|
|
34
|
+
Select,
|
|
35
|
+
Unary,
|
|
36
|
+
)
|
|
37
|
+
from lorebase_core.expr.environment import Environment
|
|
38
|
+
from lorebase_core.expr.errors import (
|
|
39
|
+
ExprError,
|
|
40
|
+
ExprErrorCode,
|
|
41
|
+
evaluation_error,
|
|
42
|
+
limit_error,
|
|
43
|
+
)
|
|
44
|
+
from lorebase_core.expr.limits import ExprLimits
|
|
45
|
+
from lorebase_core.expr.resolver import RecordResolver
|
|
46
|
+
from lorebase_core.expr.values import (
|
|
47
|
+
LinksValue,
|
|
48
|
+
ObjectValue,
|
|
49
|
+
RecordValue,
|
|
50
|
+
Ref,
|
|
51
|
+
type_name,
|
|
52
|
+
)
|
|
53
|
+
from lorebase_core.model.record import format_ref
|
|
54
|
+
from lorebase_core.types import GeoPoint
|
|
55
|
+
|
|
56
|
+
#: Steps between two wall-clock checks.
|
|
57
|
+
_CLOCK_EVERY = 64
|
|
58
|
+
|
|
59
|
+
Overlay = Mapping[tuple[str, str], Mapping[str, JsonValue] | None]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Evaluator:
|
|
63
|
+
"""Evaluates one expression; implements ``functions.Runtime``."""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
environment: Environment,
|
|
68
|
+
variables: Mapping[str, object],
|
|
69
|
+
*,
|
|
70
|
+
now: datetime,
|
|
71
|
+
limits: ExprLimits,
|
|
72
|
+
resolver: RecordResolver | None = None,
|
|
73
|
+
overlay: Overlay | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
if now.tzinfo is None or now.utcoffset() is None:
|
|
76
|
+
raise ValueError("the evaluation clock must be timezone-aware")
|
|
77
|
+
self.env = environment
|
|
78
|
+
self.variables = variables
|
|
79
|
+
self._now = now
|
|
80
|
+
self._limits = limits
|
|
81
|
+
self.resolver = resolver
|
|
82
|
+
self.overlay = overlay or {}
|
|
83
|
+
self._decimals = Context(prec=limits.decimal_precision, rounding=ROUND_HALF_EVEN)
|
|
84
|
+
self.scopes: list[dict[str, object]] = []
|
|
85
|
+
self.steps = 0
|
|
86
|
+
self.lookups = 0
|
|
87
|
+
self.deadline = time.monotonic() + limits.max_duration_ms / 1000
|
|
88
|
+
self._patterns: dict[str, Callable[[str], bool]] = {}
|
|
89
|
+
|
|
90
|
+
# Runtime protocol ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def now(self) -> datetime:
|
|
94
|
+
return self._now
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def limits(self) -> ExprLimits:
|
|
98
|
+
return self._limits
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def decimals(self) -> Context:
|
|
102
|
+
return self._decimals
|
|
103
|
+
|
|
104
|
+
def text(self, value: str) -> str:
|
|
105
|
+
if len(value) > self._limits.max_string_length:
|
|
106
|
+
raise limit_error(f"string longer than {self._limits.max_string_length} characters")
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
def items(self, value: list[object]) -> list[object]:
|
|
110
|
+
if len(value) > self._limits.max_list_size:
|
|
111
|
+
raise limit_error(f"list longer than {self._limits.max_list_size} elements")
|
|
112
|
+
return value
|
|
113
|
+
|
|
114
|
+
def regex(self, pattern: str) -> Callable[[str], bool]:
|
|
115
|
+
compiled = self._patterns.get(pattern)
|
|
116
|
+
if compiled is None:
|
|
117
|
+
try:
|
|
118
|
+
compiled = functions.compile_regex(pattern)
|
|
119
|
+
except ValueError as error:
|
|
120
|
+
raise evaluation_error(str(error)) from None
|
|
121
|
+
self._patterns[pattern] = compiled
|
|
122
|
+
return compiled
|
|
123
|
+
|
|
124
|
+
def _count_lookup(self) -> RecordResolver:
|
|
125
|
+
self.lookups += 1
|
|
126
|
+
if self.lookups > self._limits.max_record_lookups:
|
|
127
|
+
raise limit_error(f"more than {self._limits.max_record_lookups} Record lookups")
|
|
128
|
+
if self.resolver is None:
|
|
129
|
+
raise evaluation_error("Records cannot be read in this evaluation")
|
|
130
|
+
return self.resolver
|
|
131
|
+
|
|
132
|
+
def load_record(self, ref: Ref) -> RecordValue:
|
|
133
|
+
kind = self.env.schema.kinds.get(ref.kind)
|
|
134
|
+
if kind is None:
|
|
135
|
+
raise evaluation_error(f"unknown Kind {ref.kind}")
|
|
136
|
+
if (ref.kind, ref.key) in self.overlay:
|
|
137
|
+
self._count_lookup()
|
|
138
|
+
values = self.overlay[(ref.kind, ref.key)]
|
|
139
|
+
else:
|
|
140
|
+
values = self._count_lookup().record(ref.kind, ref.key)
|
|
141
|
+
if values is None:
|
|
142
|
+
raise ExprError(
|
|
143
|
+
ExprErrorCode.RECORD_NOT_FOUND,
|
|
144
|
+
f"Record {format_ref(kind.api_name, ref.key)} does not exist",
|
|
145
|
+
)
|
|
146
|
+
return RecordValue(kind, ref.key, values)
|
|
147
|
+
|
|
148
|
+
def load_links(self, ref: Ref, name: str) -> list[RecordValue]:
|
|
149
|
+
found = self.env.schema.link_by_api_name(name)
|
|
150
|
+
if found is None:
|
|
151
|
+
raise evaluation_error(f"unknown Relation {name!r}")
|
|
152
|
+
relation, direction = found
|
|
153
|
+
if relation.endpoint(direction) != ref.kind:
|
|
154
|
+
raise evaluation_error(f"Relation {name!r} does not start at this Record's Kind")
|
|
155
|
+
far = self.env.schema.kinds.get(relation.far_endpoint(direction))
|
|
156
|
+
if far is None:
|
|
157
|
+
raise evaluation_error(f"Relation {name!r} ends at an unknown Kind")
|
|
158
|
+
linked = self._count_lookup().linked(relation.id, direction, ref.key)
|
|
159
|
+
self.items(list(linked))
|
|
160
|
+
return [RecordValue(far, item.key, item.values) for item in linked]
|
|
161
|
+
|
|
162
|
+
# Evaluation -----------------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
def evaluate(self, node: Node) -> object:
|
|
165
|
+
try:
|
|
166
|
+
return self.eval(node)
|
|
167
|
+
except RecursionError:
|
|
168
|
+
raise limit_error("expression nests too deeply to evaluate") from None
|
|
169
|
+
|
|
170
|
+
def eval(self, node: Node) -> object:
|
|
171
|
+
self.steps += 1
|
|
172
|
+
if self.steps > self._limits.max_steps:
|
|
173
|
+
raise limit_error(f"evaluation took more than {self._limits.max_steps} steps", node.pos)
|
|
174
|
+
if self.steps % _CLOCK_EVERY == 0 and time.monotonic() > self.deadline:
|
|
175
|
+
raise limit_error(
|
|
176
|
+
f"evaluation took longer than {self._limits.max_duration_ms} ms", node.pos
|
|
177
|
+
)
|
|
178
|
+
try:
|
|
179
|
+
return self._eval(node)
|
|
180
|
+
except ExprError as error:
|
|
181
|
+
if error.position is None:
|
|
182
|
+
error.position = node.pos
|
|
183
|
+
raise
|
|
184
|
+
|
|
185
|
+
def _eval(self, node: Node) -> object:
|
|
186
|
+
if isinstance(node, Literal):
|
|
187
|
+
return node.value
|
|
188
|
+
if isinstance(node, Ident):
|
|
189
|
+
return self._ident(node)
|
|
190
|
+
if isinstance(node, Select):
|
|
191
|
+
operand = self.eval(node.operand)
|
|
192
|
+
if node.test:
|
|
193
|
+
return self._has(operand, node.field)
|
|
194
|
+
return self._field(operand, node.field)
|
|
195
|
+
if isinstance(node, Index):
|
|
196
|
+
return self._index(self.eval(node.operand), self.eval(node.index))
|
|
197
|
+
if isinstance(node, Call):
|
|
198
|
+
return self._call(node)
|
|
199
|
+
if isinstance(node, Comprehension):
|
|
200
|
+
return self._comprehension(node)
|
|
201
|
+
if isinstance(node, ListLit):
|
|
202
|
+
return self.items([self.eval(item) for item in node.items])
|
|
203
|
+
if isinstance(node, MapLit):
|
|
204
|
+
return self._map(node)
|
|
205
|
+
if isinstance(node, Unary):
|
|
206
|
+
operand = self.eval(node.operand)
|
|
207
|
+
if node.op == "!":
|
|
208
|
+
return not ops.require_bool(operand, "! operand")
|
|
209
|
+
return ops.negate(operand, self._decimals)
|
|
210
|
+
if isinstance(node, Binary):
|
|
211
|
+
return self._binary(node)
|
|
212
|
+
if isinstance(node, Conditional):
|
|
213
|
+
condition = ops.require_bool(self.eval(node.condition), "condition")
|
|
214
|
+
return self.eval(node.then if condition else node.otherwise)
|
|
215
|
+
raise evaluation_error(f"unsupported expression {type(node).__name__}")
|
|
216
|
+
|
|
217
|
+
def _ident(self, node: Ident) -> object:
|
|
218
|
+
for scope in reversed(self.scopes):
|
|
219
|
+
if node.name in scope:
|
|
220
|
+
return scope[node.name]
|
|
221
|
+
if node.name in self.variables:
|
|
222
|
+
return self.variables[node.name]
|
|
223
|
+
raise evaluation_error(f"no value for {node.name!r}")
|
|
224
|
+
|
|
225
|
+
def _field(self, operand: object, name: str) -> object:
|
|
226
|
+
if isinstance(operand, RecordValue):
|
|
227
|
+
if name == "key":
|
|
228
|
+
return operand.key
|
|
229
|
+
if name == "ref":
|
|
230
|
+
return operand.ref
|
|
231
|
+
return operand.get(name)
|
|
232
|
+
if isinstance(operand, LinksValue):
|
|
233
|
+
return self._links_field(operand, name)
|
|
234
|
+
if isinstance(operand, ObjectValue):
|
|
235
|
+
if name not in operand.fields:
|
|
236
|
+
raise evaluation_error(f"{operand.name} has no field {name!r}")
|
|
237
|
+
return operand.fields[name]
|
|
238
|
+
if isinstance(operand, GeoPoint) and name in ("lat", "lon"):
|
|
239
|
+
return getattr(operand, name)
|
|
240
|
+
if isinstance(operand, dict):
|
|
241
|
+
if name not in operand:
|
|
242
|
+
raise evaluation_error(f"no key {name!r} in map")
|
|
243
|
+
return operand[name]
|
|
244
|
+
if isinstance(operand, Ref) and name == "key":
|
|
245
|
+
return operand.key
|
|
246
|
+
if operand is None:
|
|
247
|
+
raise evaluation_error(
|
|
248
|
+
f"cannot read {name!r} of an absent value (null); test presence with has()"
|
|
249
|
+
)
|
|
250
|
+
raise evaluation_error(f"{type_name(operand)} has no field {name!r}")
|
|
251
|
+
|
|
252
|
+
def _has(self, operand: object, name: str) -> bool:
|
|
253
|
+
if isinstance(operand, RecordValue):
|
|
254
|
+
return name in ("key", "ref") or operand.has(name)
|
|
255
|
+
if isinstance(operand, ObjectValue):
|
|
256
|
+
return operand.fields.get(name) is not None
|
|
257
|
+
if isinstance(operand, dict):
|
|
258
|
+
return name in operand
|
|
259
|
+
if isinstance(operand, LinksValue | GeoPoint):
|
|
260
|
+
return True
|
|
261
|
+
if operand is None:
|
|
262
|
+
raise evaluation_error(f"has() on an absent value (null) selecting {name!r}")
|
|
263
|
+
raise evaluation_error(f"has() is not defined on {type_name(operand)}")
|
|
264
|
+
|
|
265
|
+
def _links_field(self, links: LinksValue, name: str) -> list[RecordValue]:
|
|
266
|
+
field = self.env.link_fields.get(name)
|
|
267
|
+
if field is None:
|
|
268
|
+
raise evaluation_error(f"no Relation {name!r} at Kind {links.kind.api_name}")
|
|
269
|
+
far = self.env.schema.kinds.get(field.relation.far_endpoint(field.direction))
|
|
270
|
+
if far is None:
|
|
271
|
+
raise evaluation_error(f"Relation {name!r} ends at an unknown Kind")
|
|
272
|
+
linked = self._count_lookup().linked(field.relation.id, field.direction, links.key)
|
|
273
|
+
self.items(list(linked))
|
|
274
|
+
return [RecordValue(far, item.key, item.values) for item in linked]
|
|
275
|
+
|
|
276
|
+
def _index(self, operand: object, index: object) -> object:
|
|
277
|
+
if isinstance(operand, list):
|
|
278
|
+
if isinstance(index, bool) or not isinstance(index, int):
|
|
279
|
+
raise evaluation_error(f"list index must be an int, got {type_name(index)}")
|
|
280
|
+
if not 0 <= index < len(operand):
|
|
281
|
+
raise evaluation_error(f"index {index} out of range for a list of {len(operand)}")
|
|
282
|
+
return operand[index]
|
|
283
|
+
if isinstance(operand, dict):
|
|
284
|
+
for key, value in operand.items():
|
|
285
|
+
if ops.equals(key, index):
|
|
286
|
+
return value
|
|
287
|
+
raise evaluation_error(f"no key {index!r} in map")
|
|
288
|
+
if operand is None:
|
|
289
|
+
raise evaluation_error("cannot index an absent value (null)")
|
|
290
|
+
raise evaluation_error(f"cannot index {type_name(operand)}")
|
|
291
|
+
|
|
292
|
+
def _call(self, node: Call) -> object:
|
|
293
|
+
function = functions.lookup(node.function, node.target is not None)
|
|
294
|
+
if function is None:
|
|
295
|
+
raise evaluation_error(f"unknown function {node.function!r}")
|
|
296
|
+
args = [self.eval(arg) for arg in node.args]
|
|
297
|
+
if node.target is not None:
|
|
298
|
+
args.insert(0, self.eval(node.target))
|
|
299
|
+
try:
|
|
300
|
+
result = function.run(self, args)
|
|
301
|
+
except (ArithmeticError, ValueError) as error:
|
|
302
|
+
raise evaluation_error(f"{node.function}() failed: {error}") from None
|
|
303
|
+
if isinstance(result, str):
|
|
304
|
+
self.text(result)
|
|
305
|
+
return result
|
|
306
|
+
|
|
307
|
+
def _comprehension(self, node: Comprehension) -> object:
|
|
308
|
+
target = self.eval(node.target)
|
|
309
|
+
if isinstance(target, list):
|
|
310
|
+
elements: list[object] = target
|
|
311
|
+
elif isinstance(target, dict):
|
|
312
|
+
elements = list(target)
|
|
313
|
+
else:
|
|
314
|
+
raise evaluation_error(f"{node.macro}() needs a list or a map, got {type_name(target)}")
|
|
315
|
+
scope: dict[str, object] = {}
|
|
316
|
+
self.scopes.append(scope)
|
|
317
|
+
try:
|
|
318
|
+
return self._iterate(node, elements, scope)
|
|
319
|
+
finally:
|
|
320
|
+
self.scopes.pop()
|
|
321
|
+
|
|
322
|
+
def _iterate(
|
|
323
|
+
self, node: Comprehension, elements: list[object], scope: dict[str, object]
|
|
324
|
+
) -> object:
|
|
325
|
+
macro = node.macro
|
|
326
|
+
results: list[object] = []
|
|
327
|
+
matches = 0
|
|
328
|
+
for element in elements:
|
|
329
|
+
scope[node.variable] = element
|
|
330
|
+
value = self.eval(node.body)
|
|
331
|
+
if macro == "map":
|
|
332
|
+
results.append(value)
|
|
333
|
+
self.items(results)
|
|
334
|
+
continue
|
|
335
|
+
accepted = ops.require_bool(value, f"{macro}() condition")
|
|
336
|
+
if macro == "all" and not accepted:
|
|
337
|
+
return False
|
|
338
|
+
if macro == "exists" and accepted:
|
|
339
|
+
return True
|
|
340
|
+
if accepted:
|
|
341
|
+
matches += 1
|
|
342
|
+
if macro == "filter":
|
|
343
|
+
results.append(element)
|
|
344
|
+
if macro == "all":
|
|
345
|
+
return True
|
|
346
|
+
if macro == "exists":
|
|
347
|
+
return False
|
|
348
|
+
if macro == "exists_one":
|
|
349
|
+
return matches == 1
|
|
350
|
+
if macro == "count":
|
|
351
|
+
return matches
|
|
352
|
+
return results
|
|
353
|
+
|
|
354
|
+
def _map(self, node: MapLit) -> dict[object, object]:
|
|
355
|
+
result: dict[object, object] = {}
|
|
356
|
+
for key_node, value_node in node.entries:
|
|
357
|
+
key = self.eval(key_node)
|
|
358
|
+
if not isinstance(key, str | int):
|
|
359
|
+
raise evaluation_error(
|
|
360
|
+
f"map keys must be strings, ints or bools, got {type_name(key)}"
|
|
361
|
+
)
|
|
362
|
+
if any(ops.equals(key, existing) for existing in result):
|
|
363
|
+
raise evaluation_error(f"duplicate map key {key!r}")
|
|
364
|
+
result[key] = self.eval(value_node)
|
|
365
|
+
self.items(list(result))
|
|
366
|
+
return result
|
|
367
|
+
|
|
368
|
+
def _binary(self, node: Binary) -> object:
|
|
369
|
+
op = node.op
|
|
370
|
+
if op in ("&&", "||"):
|
|
371
|
+
return self._logical(node)
|
|
372
|
+
left = self.eval(node.left)
|
|
373
|
+
right = self.eval(node.right)
|
|
374
|
+
if op == "==":
|
|
375
|
+
return ops.equals(left, right)
|
|
376
|
+
if op == "!=":
|
|
377
|
+
return not ops.equals(left, right)
|
|
378
|
+
if op == "<":
|
|
379
|
+
return ops.compare(left, right) < 0
|
|
380
|
+
if op == "<=":
|
|
381
|
+
return ops.compare(left, right) <= 0
|
|
382
|
+
if op == ">":
|
|
383
|
+
return ops.compare(left, right) > 0
|
|
384
|
+
if op == ">=":
|
|
385
|
+
return ops.compare(left, right) >= 0
|
|
386
|
+
if op == "in":
|
|
387
|
+
return ops.contains(right, left)
|
|
388
|
+
result = ops.arithmetic(op, left, right, self._decimals)
|
|
389
|
+
if isinstance(result, str):
|
|
390
|
+
self.text(result)
|
|
391
|
+
elif isinstance(result, list):
|
|
392
|
+
self.items(result)
|
|
393
|
+
elif isinstance(result, bytes) and len(result) > self._limits.max_string_length:
|
|
394
|
+
raise limit_error(f"bytes longer than {self._limits.max_string_length}")
|
|
395
|
+
return result
|
|
396
|
+
|
|
397
|
+
def _logical(self, node: Binary) -> bool:
|
|
398
|
+
deciding = node.op == "||" # the value that decides the result on its own
|
|
399
|
+
failure: ExprError | None = None
|
|
400
|
+
try:
|
|
401
|
+
left = ops.require_bool(self.eval(node.left), f"{node.op} operand")
|
|
402
|
+
except ExprError as error:
|
|
403
|
+
if error.code is ExprErrorCode.LIMIT_EXCEEDED:
|
|
404
|
+
raise
|
|
405
|
+
failure = error
|
|
406
|
+
else:
|
|
407
|
+
if left is deciding:
|
|
408
|
+
return deciding
|
|
409
|
+
right = ops.require_bool(self.eval(node.right), f"{node.op} operand")
|
|
410
|
+
if right is deciding:
|
|
411
|
+
return deciding
|
|
412
|
+
if failure is not None:
|
|
413
|
+
raise failure
|
|
414
|
+
return not deciding
|