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.
Files changed (94) hide show
  1. lorebase_core/__init__.py +1 -0
  2. lorebase_core/canonical/__init__.py +35 -0
  3. lorebase_core/canonical/hashing.py +128 -0
  4. lorebase_core/canonical/jcs.py +162 -0
  5. lorebase_core/expr/__init__.py +118 -0
  6. lorebase_core/expr/ast.py +443 -0
  7. lorebase_core/expr/checker.py +428 -0
  8. lorebase_core/expr/compiler.py +87 -0
  9. lorebase_core/expr/context.py +82 -0
  10. lorebase_core/expr/derivations.py +306 -0
  11. lorebase_core/expr/environment.py +176 -0
  12. lorebase_core/expr/errors.py +98 -0
  13. lorebase_core/expr/evaluator.py +414 -0
  14. lorebase_core/expr/functions.py +898 -0
  15. lorebase_core/expr/limits.py +52 -0
  16. lorebase_core/expr/operations.py +479 -0
  17. lorebase_core/expr/ops.py +243 -0
  18. lorebase_core/expr/resolver.py +96 -0
  19. lorebase_core/expr/rows.py +148 -0
  20. lorebase_core/expr/sensitivity.py +337 -0
  21. lorebase_core/expr/types.py +227 -0
  22. lorebase_core/expr/validation.py +51 -0
  23. lorebase_core/expr/values.py +169 -0
  24. lorebase_core/expr/visibility.py +363 -0
  25. lorebase_core/infer/__init__.py +36 -0
  26. lorebase_core/infer/columns.py +621 -0
  27. lorebase_core/infer/engine.py +546 -0
  28. lorebase_core/infer/keys.py +164 -0
  29. lorebase_core/infer/names.py +145 -0
  30. lorebase_core/infer/options.py +290 -0
  31. lorebase_core/infer/profile.py +54 -0
  32. lorebase_core/infer/proposal.py +122 -0
  33. lorebase_core/infer/references.py +314 -0
  34. lorebase_core/infer/suggestions.py +731 -0
  35. lorebase_core/infer/tables.py +81 -0
  36. lorebase_core/merge/__init__.py +89 -0
  37. lorebase_core/merge/conflicts.py +262 -0
  38. lorebase_core/merge/consistency.py +440 -0
  39. lorebase_core/merge/data_merge.py +522 -0
  40. lorebase_core/merge/engine.py +476 -0
  41. lorebase_core/merge/index.py +100 -0
  42. lorebase_core/merge/objects.py +204 -0
  43. lorebase_core/merge/policies.py +322 -0
  44. lorebase_core/merge/resolve.py +321 -0
  45. lorebase_core/merge/schema_merge.py +429 -0
  46. lorebase_core/merge/state.py +443 -0
  47. lorebase_core/model/__init__.py +245 -0
  48. lorebase_core/model/attribute.py +72 -0
  49. lorebase_core/model/base.py +61 -0
  50. lorebase_core/model/derivation.py +95 -0
  51. lorebase_core/model/domain.py +71 -0
  52. lorebase_core/model/embedding.py +193 -0
  53. lorebase_core/model/ids.py +78 -0
  54. lorebase_core/model/kind.py +85 -0
  55. lorebase_core/model/lineage.py +114 -0
  56. lorebase_core/model/operation.py +161 -0
  57. lorebase_core/model/paths.py +141 -0
  58. lorebase_core/model/record.py +127 -0
  59. lorebase_core/model/relation.py +83 -0
  60. lorebase_core/model/schema.py +219 -0
  61. lorebase_core/model/source.py +204 -0
  62. lorebase_core/model/trait.py +37 -0
  63. lorebase_core/model/vault.py +43 -0
  64. lorebase_core/model/yaml_schema.py +1058 -0
  65. lorebase_core/query/__init__.py +97 -0
  66. lorebase_core/query/dsl.py +227 -0
  67. lorebase_core/query/resolve.py +794 -0
  68. lorebase_core/revision/__init__.py +59 -0
  69. lorebase_core/revision/ancestry.py +272 -0
  70. lorebase_core/revision/changeset.py +62 -0
  71. lorebase_core/revision/revision.py +121 -0
  72. lorebase_core/tree/__init__.py +76 -0
  73. lorebase_core/tree/diff.py +119 -0
  74. lorebase_core/tree/hamt.py +333 -0
  75. lorebase_core/tree/node.py +280 -0
  76. lorebase_core/tree/params.py +33 -0
  77. lorebase_core/tree/root.py +70 -0
  78. lorebase_core/tree/store.py +98 -0
  79. lorebase_core/types/__init__.py +71 -0
  80. lorebase_core/types/casts.py +271 -0
  81. lorebase_core/types/constraints.py +299 -0
  82. lorebase_core/types/encoding.py +538 -0
  83. lorebase_core/types/errors.py +41 -0
  84. lorebase_core/types/matching.py +40 -0
  85. lorebase_core/types/types.py +187 -0
  86. lorebase_core/validate/__init__.py +49 -0
  87. lorebase_core/validate/changes.py +70 -0
  88. lorebase_core/validate/changeset.py +551 -0
  89. lorebase_core/validate/data_rules.py +291 -0
  90. lorebase_core/validate/issues.py +123 -0
  91. lorebase_core/validate/schema_rules.py +1304 -0
  92. lorebase_core-0.2.3.dist-info/METADATA +9 -0
  93. lorebase_core-0.2.3.dist-info/RECORD +94 -0
  94. 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