avrae-ls 0.6.0__py3-none-any.whl → 0.6.2__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.
@@ -0,0 +1,1091 @@
1
+ import abc
2
+ import ast
3
+ import contextlib
4
+ from collections.abc import Mapping, Sequence
5
+ from functools import cached_property
6
+
7
+ from .exceptions import *
8
+ from .helpers import DraconicConfig, OperatorMixin, zip_star
9
+ from .string import check_format_spec
10
+ from .versions import PY_310
11
+ from .types import approx_len_of
12
+
13
+ __all__ = ("SimpleInterpreter", "DraconicInterpreter")
14
+
15
+
16
+ # ===== single-line evaluator, noncompound types, etc =====
17
+ class SimpleInterpreter(OperatorMixin):
18
+ """A simple interpreter capable of evaluating expressions. No compound types or assignments."""
19
+
20
+ def __init__(self, builtins=None, config=None):
21
+ if config is None:
22
+ config = DraconicConfig()
23
+ if builtins is None:
24
+ builtins = {}
25
+
26
+ super().__init__(config)
27
+
28
+ if config.builtins_extend_default:
29
+ builtins = {**config.default_names, **builtins}
30
+
31
+ self.builtins = builtins
32
+
33
+ self.nodes = {
34
+ ast.Expr: self._eval_expr,
35
+ # literals:
36
+ ast.Num: self._eval_num,
37
+ ast.Str: self._eval_str,
38
+ ast.Constant: self._eval_constant,
39
+ ast.FormattedValue: self._eval_formattedvalue, # formatted value in f-string
40
+ ast.JoinedStr: self._eval_joinedstr, # f-string
41
+ ast.NameConstant: self._eval_constant, # True/False/None up to py3.7
42
+ # names:
43
+ ast.Name: self._eval_name,
44
+ # ops:
45
+ ast.UnaryOp: self._eval_unaryop,
46
+ ast.BinOp: self._eval_binop,
47
+ ast.BoolOp: self._eval_boolop,
48
+ ast.Compare: self._eval_compare,
49
+ ast.IfExp: self._eval_ifexp,
50
+ # function call:
51
+ ast.Call: self._eval_call,
52
+ ast.keyword: self._eval_keyword, # foo(x=y), kwargs (not supported)
53
+ # container[key]:
54
+ ast.Subscript: self._eval_subscript,
55
+ ast.Index: self._eval_index, # deprecated in py3.9 (bpo-34822)
56
+ ast.Slice: self._eval_slice, # deprecated in py3.9 (bpo-34822)
57
+ # container.key:
58
+ ast.Attribute: self._eval_attribute,
59
+ }
60
+
61
+ self._str = self._config.str
62
+ self._expr = None # save the expression for error handling
63
+
64
+ def parse(self, expr: str):
65
+ """
66
+ Parses an expression.
67
+
68
+ :type expr: str
69
+ :rtype: list[ast.AST]
70
+ """
71
+ self._expr = expr
72
+ try:
73
+ return ast.parse(expr).body
74
+ except SyntaxError as e:
75
+ raise DraconicSyntaxError(e, expr) from e
76
+
77
+ def eval(self, expr: str):
78
+ """
79
+ Evaluates an expression.
80
+
81
+ :type expr: list[ast.AST] or str
82
+ """
83
+ expr = self.parse(expr)
84
+
85
+ self._preflight()
86
+ try:
87
+ expression = expr[0]
88
+ except IndexError: # if there is no expression, evaluate to None
89
+ return None
90
+ return self._eval(expression)
91
+
92
+ def _eval(self, node):
93
+ """The internal evaluator used on each node in the parsed tree."""
94
+ try:
95
+ handler = self.nodes[type(node)]
96
+ except KeyError:
97
+ raise FeatureNotAvailable(
98
+ "Sorry, {0} is not available in this evaluator".format(type(node).__name__), node, self._expr
99
+ )
100
+
101
+ try:
102
+ return handler(node)
103
+ except _PostponedRaise as pr:
104
+ raise pr.cls(*pr.args, **pr.kwargs, node=node, expr=self._expr)
105
+ except DraconicException:
106
+ raise
107
+ except Exception as e:
108
+ raise AnnotatedException(e, node, self._expr) from e
109
+
110
+ def _preflight(self):
111
+ """Called before starting evaluation."""
112
+ pass
113
+
114
+ @property
115
+ def names(self):
116
+ return self.builtins
117
+
118
+ @names.setter
119
+ def names(self, new_names):
120
+ self.builtins = new_names
121
+
122
+ # ===== nodes =====
123
+ def _eval_expr(self, node):
124
+ return self._eval(node.value)
125
+
126
+ @staticmethod
127
+ def _eval_num(node):
128
+ return node.n
129
+
130
+ def _eval_str(self, node):
131
+ if len(node.s) > self._config.max_const_len:
132
+ raise IterableTooLong(
133
+ f"String literal in statement is too long ({len(node.s)} > {self._config.max_const_len})",
134
+ node,
135
+ self._expr,
136
+ )
137
+ return self._str(node.s)
138
+
139
+ def _eval_constant(self, node):
140
+ if hasattr(node.value, "__len__") and len(node.value) > self._config.max_const_len:
141
+ raise IterableTooLong(
142
+ f"Literal in statement is too long ({len(node.value)} > {self._config.max_const_len})", node, self._expr
143
+ )
144
+ if isinstance(node.value, bytes):
145
+ raise FeatureNotAvailable("Creation of bytes literals is not allowed", node, self._expr)
146
+ return node.value
147
+
148
+ def _eval_unaryop(self, node):
149
+ return self.operators[type(node.op)](self._eval(node.operand))
150
+
151
+ def _eval_binop(self, node):
152
+ return self.operators[type(node.op)](self._eval(node.left), self._eval(node.right))
153
+
154
+ def _eval_boolop(self, node):
155
+ vout = False
156
+ if isinstance(node.op, ast.And):
157
+ for value in node.values:
158
+ vout = self._eval(value)
159
+ if not vout:
160
+ return vout
161
+ elif isinstance(node.op, ast.Or):
162
+ for value in node.values:
163
+ vout = self._eval(value)
164
+ if vout:
165
+ return vout
166
+ return vout
167
+
168
+ def _eval_compare(self, node):
169
+ right = self._eval(node.left)
170
+ to_return = True
171
+ for operation, comp in zip(node.ops, node.comparators):
172
+ if not to_return:
173
+ break
174
+ left = right
175
+ right = self._eval(comp)
176
+ to_return = self.operators[type(operation)](left, right)
177
+ return to_return
178
+
179
+ def _eval_ifexp(self, node):
180
+ return self._eval(node.body) if self._eval(node.test) else self._eval(node.orelse)
181
+
182
+ def _eval_call(self, node):
183
+ func = self._eval(node.func)
184
+ return func(*(self._eval(a) for a in node.args), **dict(self._eval(k) for k in node.keywords))
185
+
186
+ def _eval_keyword(self, node):
187
+ return node.arg, self._eval(node.value)
188
+
189
+ def _eval_name(self, node):
190
+ try:
191
+ return self.names[node.id]
192
+ except KeyError:
193
+ raise NotDefined(f"{node.id} is not defined", node, self._expr)
194
+
195
+ def _eval_subscript(self, node):
196
+ container = self._eval(node.value)
197
+ key = self._eval(node.slice)
198
+ try:
199
+ return container[key]
200
+ except KeyError:
201
+ raise
202
+
203
+ def _eval_attribute(self, node):
204
+ for prefix in self._config.disallow_prefixes:
205
+ if node.attr.startswith(prefix):
206
+ raise FeatureNotAvailable(f"Access to the {node.attr} attribute is not allowed", node, self._expr)
207
+ if node.attr in self._config.disallow_methods:
208
+ raise FeatureNotAvailable(f"Access to the {node.attr} attribute is not allowed", node, self._expr)
209
+ # eval node
210
+ node_evaluated = self._eval(node.value)
211
+
212
+ # Maybe the base object is an actual object, not just a dict
213
+ try:
214
+ return getattr(node_evaluated, node.attr)
215
+ except (AttributeError, TypeError):
216
+ # If it is not present, raise an exception
217
+ raise NotDefined(f"'{type(node_evaluated).__name__}' object has no attribute {node.attr}", node, self._expr)
218
+
219
+ def _eval_index(self, node):
220
+ return self._eval(node.value)
221
+
222
+ def _eval_slice(self, node):
223
+ lower = upper = step = None
224
+ if node.lower is not None:
225
+ lower = self._eval(node.lower)
226
+ if node.upper is not None:
227
+ upper = self._eval(node.upper)
228
+ if node.step is not None:
229
+ step = self._eval(node.step)
230
+ return slice(lower, upper, step)
231
+
232
+ def _eval_joinedstr(self, node):
233
+ length = 0
234
+ evaluated_values = []
235
+ for n in node.values:
236
+ val = str(self._eval(n))
237
+ length += len(val)
238
+ if length > self._config.max_const_len:
239
+ raise IterableTooLong(
240
+ f"f-string in statement is too long ({length} > {self._config.max_const_len})", node, self._expr
241
+ )
242
+ evaluated_values.append(val)
243
+ return "".join(evaluated_values)
244
+
245
+ def _eval_formattedvalue(self, node):
246
+ if node.format_spec:
247
+ format_spec = str(self._eval(node.format_spec))
248
+ check_format_spec(self._config, format_spec)
249
+ return self._str(format(self._eval(node.value), format_spec))
250
+ return self._eval(node.value)
251
+
252
+
253
+ # ===== multiple-line execution, assignment, compound types =====
254
+ class _Break:
255
+ __slots__ = ("node",)
256
+
257
+ def __init__(self, node: ast.Break):
258
+ self.node = node
259
+
260
+
261
+ class _Continue:
262
+ __slots__ = ("node",)
263
+
264
+ def __init__(self, node: ast.Continue):
265
+ self.node = node
266
+
267
+
268
+ class _Return:
269
+ __slots__ = ("value", "node")
270
+
271
+ def __init__(self, retval, node: ast.Return):
272
+ self.value = retval
273
+ self.node = node
274
+
275
+
276
+ class _Callable(abc.ABC):
277
+ """ABC for functions and lambdas"""
278
+
279
+ def __init__(self, interpreter, node, names_at_def, defining_expr):
280
+ self._interpreter = interpreter
281
+ self._node = node
282
+ self._outer_scope_names = names_at_def
283
+ self._defining_expr = defining_expr
284
+
285
+ # faux introspection props since __dunders__ aren't accessible
286
+ @property
287
+ def name(self):
288
+ return self.__name__
289
+
290
+ @cached_property
291
+ def doc(self):
292
+ return ast.get_docstring(self._node)
293
+
294
+ def __call__(self, *args, **kwargs):
295
+ raise NotImplementedError
296
+
297
+
298
+ class _Function(_Callable):
299
+ """A wrapper class around an ast.FunctionDef."""
300
+
301
+ def __init__(self, interpreter, functiondef, names_at_def, defining_expr):
302
+ super().__init__(interpreter, functiondef, names_at_def, defining_expr)
303
+ self.__name__ = self._name = functiondef.name
304
+
305
+ def __repr__(self):
306
+ return f"<Function {self._name}>"
307
+
308
+ def __call__(self, *args, **kwargs):
309
+ try:
310
+ # noinspection PyProtectedMember
311
+ return self._interpreter._exec_function(self, *args, **kwargs)
312
+ except DraconicException as e:
313
+ e.__drac_context__ = self._name
314
+ raise
315
+
316
+
317
+ class _Lambda(_Callable):
318
+ """A wrapper class around an ast.Lambda."""
319
+
320
+ def __init__(self, interpreter, lambdadef, names_at_def, defining_expr):
321
+ super().__init__(interpreter, lambdadef, names_at_def, defining_expr)
322
+ self.__name__ = self._name = "<lambda>"
323
+
324
+ def __repr__(self):
325
+ return f"<Function <lambda>>"
326
+
327
+ @property
328
+ def doc(self):
329
+ return None
330
+
331
+ def __call__(self, *args, **kwargs):
332
+ try:
333
+ # noinspection PyProtectedMember
334
+ return self._interpreter._exec_lambda(self, *args, **kwargs)
335
+ except DraconicException as e:
336
+ e.__drac_context__ = self._name
337
+ raise
338
+
339
+
340
+ class DraconicInterpreter(SimpleInterpreter):
341
+ """The Draconic interpreter. Capable of running Draconic code."""
342
+
343
+ def __init__(self, builtins=None, config=None, initial_names=None):
344
+ super().__init__(builtins, config)
345
+
346
+ if initial_names is None:
347
+ initial_names = {}
348
+
349
+ self.nodes.update(
350
+ {
351
+ # compound types:
352
+ ast.Dict: self._eval_dict,
353
+ ast.Tuple: self._eval_tuple,
354
+ ast.List: self._eval_list,
355
+ ast.Set: self._eval_set,
356
+ # comprehensions:
357
+ ast.ListComp: self._eval_listcomp,
358
+ ast.SetComp: self._eval_setcomp,
359
+ ast.DictComp: self._eval_dictcomp,
360
+ ast.GeneratorExp: self._eval_generatorexp,
361
+ ast.Starred: self._eval_starred, # foo(*iterable), [*iterable], etc.
362
+ # assignments:
363
+ ast.Assign: self._eval_assign,
364
+ ast.AugAssign: self._eval_augassign,
365
+ ast.NamedExpr: self._eval_namedexpr,
366
+ # control:
367
+ ast.Return: self._exec_return,
368
+ ast.If: self._exec_if,
369
+ ast.For: self._exec_for,
370
+ ast.While: self._exec_while,
371
+ ast.Break: lambda node: _Break(node),
372
+ ast.Continue: lambda node: _Continue(node),
373
+ ast.Pass: lambda node: None,
374
+ # functions:
375
+ ast.FunctionDef: self._eval_functiondef,
376
+ ast.Lambda: self._eval_lambda,
377
+ # try/except:
378
+ ast.Try: self._exec_try,
379
+ }
380
+ )
381
+
382
+ self.assign_nodes = {
383
+ ast.Name: self._assign_name,
384
+ ast.Tuple: self._assign_unpack,
385
+ ast.List: self._assign_unpack,
386
+ ast.Subscript: self._assign_subscript,
387
+ # no assigning to attributes
388
+ ast.Starred: self._assign_starred, # a, *b = [x, y, z]
389
+ }
390
+
391
+ self.patma_nodes = {}
392
+
393
+ if PY_310:
394
+ self.nodes.update(
395
+ {
396
+ ast.Match: self._exec_match,
397
+ }
398
+ )
399
+
400
+ self.patma_nodes.update(
401
+ {
402
+ ast.MatchValue: self._patma_match_value,
403
+ ast.MatchSingleton: self._patma_match_singleton,
404
+ ast.MatchSequence: self._patma_match_sequence,
405
+ ast.MatchMapping: self._patma_match_mapping,
406
+ ast.MatchStar: self._patma_match_star,
407
+ # no MatchClass
408
+ ast.MatchAs: self._patma_match_as,
409
+ ast.MatchOr: self._patma_match_or,
410
+ }
411
+ )
412
+
413
+ # compound type helpers
414
+ self._list = self._config.list
415
+ self._set = self._config.set
416
+ self._dict = self._config.dict
417
+
418
+ self._num_stmts = 0
419
+ self._loops = 0
420
+ self._depth = 1
421
+ self._names = initial_names
422
+
423
+ def eval(self, expr: str):
424
+ retval = super().eval(expr)
425
+ if isinstance(retval, _Return):
426
+ return retval.value
427
+ elif isinstance(retval, (_Break, _Continue)):
428
+ raise DraconicSyntaxError.from_node(retval.node, msg="Loop control outside loop", expr=self._expr)
429
+ return retval
430
+
431
+ def execute(self, expr: str):
432
+ """Executes an AST body."""
433
+ expr = self.parse(expr)
434
+
435
+ self._preflight()
436
+ retval = self._exec(expr)
437
+ if isinstance(retval, (_Break, _Continue)):
438
+ raise DraconicSyntaxError.from_node(retval.node, msg="Loop control outside loop", expr=self._expr)
439
+ if isinstance(retval, _Return):
440
+ return retval.value
441
+
442
+ def execute_module(self, expr: str, module_name="<module>"):
443
+ """
444
+ Executes the expression as if it was a module.
445
+ This is similar to *execute* except:
446
+ - it doesn't allow bare returns
447
+ - it doesn't call preflight
448
+ - it saves the previously running expression
449
+ - it sets the exception context
450
+ """
451
+ old_expr = self._expr
452
+ try:
453
+ expr = self.parse(expr)
454
+ retval = self._exec(expr)
455
+ if isinstance(retval, (_Break, _Continue)):
456
+ raise DraconicSyntaxError.from_node(retval.node, msg="Loop control outside loop", expr=self._expr)
457
+ if isinstance(retval, _Return):
458
+ raise DraconicSyntaxError.from_node(retval.node, msg="'return' outside function", expr=self._expr)
459
+ except DraconicException as e:
460
+ e.__drac_context__ = module_name
461
+ raise
462
+ finally:
463
+ self._expr = old_expr
464
+
465
+ def _preflight(self):
466
+ self._num_stmts = 0
467
+ self._loops = 0
468
+ super()._preflight()
469
+
470
+ def _eval(self, node):
471
+ self._num_stmts += 1
472
+ if self._num_stmts > self._config.max_statements:
473
+ raise TooManyStatements("You are trying to execute too many statements.", node, self._expr)
474
+
475
+ val = super()._eval(node)
476
+ # ensure that it's always an instance of our safe compound types being returned
477
+ # note: makes a copy, so the original copy won't be updated
478
+ # we don't use isinstance because we're looking for very specific classes
479
+ if type(val) is str:
480
+ return self._str(val)
481
+ elif type(val) is list:
482
+ return self._list(val)
483
+ elif type(val) is dict:
484
+ return self._dict(val)
485
+ elif type(val) is set:
486
+ return self._set(val)
487
+ return val
488
+
489
+ def _exec(self, body):
490
+ for expression in body:
491
+ retval = self._eval(expression)
492
+ if isinstance(retval, (_Return, _Break, _Continue)):
493
+ return retval
494
+
495
+ @property
496
+ def names(self):
497
+ return {**self.builtins, **self._names}
498
+
499
+ @names.setter
500
+ def names(self, new_names):
501
+ self._names = new_names
502
+
503
+ # ===== compound types =====
504
+ def _eval_dict(self, node):
505
+ return self._dict(self._starred_keyword_unwrap(zip(node.keys, node.values)))
506
+
507
+ def _eval_tuple(self, node):
508
+ return tuple(self._starred_unwrap(node.elts))
509
+
510
+ def _eval_list(self, node):
511
+ return self._list(self._starred_unwrap(node.elts))
512
+
513
+ def _eval_set(self, node):
514
+ return self._set(self._starred_unwrap(node.elts))
515
+
516
+ def _eval_listcomp(self, node):
517
+ return self._list(self._do_comprehension(node))
518
+
519
+ def _eval_setcomp(self, node):
520
+ return self._set(self._do_comprehension(node))
521
+
522
+ def _eval_dictcomp(self, node):
523
+ return self._dict(self._do_comprehension(node, is_dictcomp=True))
524
+
525
+ def _eval_generatorexp(self, node):
526
+ for item in self._do_comprehension(node):
527
+ yield item
528
+
529
+ def _do_comprehension(self, comprehension_node, is_dictcomp=False):
530
+ if is_dictcomp:
531
+
532
+ def do_value(node):
533
+ return self._eval(node.key), self._eval(node.value)
534
+
535
+ else:
536
+
537
+ def do_value(node):
538
+ return self._eval(node.elt)
539
+
540
+ extra_names = {}
541
+ previous_name_evaller = self.nodes[ast.Name]
542
+
543
+ def eval_names_extra(node):
544
+ """
545
+ Here we hide our extra scope for within this comprehension
546
+ """
547
+ if node.id in extra_names:
548
+ return extra_names[node.id]
549
+ return previous_name_evaller(node)
550
+
551
+ self.nodes.update({ast.Name: eval_names_extra})
552
+
553
+ def recurse_targets(target, value):
554
+ """
555
+ Recursively (enter, (into, (nested, name), unpacking)) = \
556
+ and, (assign, (values, to), each
557
+ """
558
+ if isinstance(target, ast.Name):
559
+ extra_names[target.id] = value
560
+ else:
561
+ for t, v in zip(target.elts, value):
562
+ recurse_targets(t, v)
563
+
564
+ def do_generator(gi=0, total_len=0):
565
+ """
566
+ For each generator, set the names used in the final emitted value/the next generator.
567
+ Only the final generator (gi = len(comprehension_node.generator)-1) should emit the final values,
568
+ since only then are all possible necessary values set in extra_names.
569
+ """
570
+ generator_node = comprehension_node.generators[gi]
571
+ for i in self._eval(generator_node.iter):
572
+ self._loops += 1
573
+ if self._loops > self._config.max_loops:
574
+ raise IterableTooLong("Comprehension generates too many elements", comprehension_node, self._expr)
575
+
576
+ # set names
577
+ recurse_targets(generator_node.target, i)
578
+
579
+ if all(self._eval(iff) for iff in generator_node.ifs):
580
+ if len(comprehension_node.generators) > gi + 1:
581
+ # next generator
582
+ yield from do_generator(gi + 1, total_len) # bubble up emitted values
583
+ else:
584
+ # emit values
585
+ value = do_value(comprehension_node)
586
+ total_len += sum(approx_len_of(val) for val in value) if is_dictcomp else approx_len_of(value)
587
+ total_len += 1
588
+ if total_len > self._config.max_const_len:
589
+ raise IterableTooLong("Comprehension generates too much", comprehension_node, self._expr)
590
+ yield value
591
+
592
+ try:
593
+ yield from do_generator()
594
+ finally:
595
+ self.nodes.update({ast.Name: previous_name_evaller})
596
+
597
+ def _eval_starred(self, node):
598
+ raise DraconicSyntaxError.from_node(node, "can't use starred expression here", self._expr)
599
+
600
+ def _starred_unwrap(self, nodes, *, check_len=True):
601
+ total_len = 0
602
+
603
+ for node in nodes:
604
+ if type(node) is ast.Starred:
605
+ evalue = self._eval(node.value)
606
+ try:
607
+ for retval in evalue:
608
+ self._loops += 1
609
+ if self._loops > self._config.max_loops:
610
+ raise IterableTooLong("Unwrapping generates too many elements", node, self._expr)
611
+ if check_len:
612
+ total_len += approx_len_of(retval) + 1
613
+ if total_len > self._config.max_const_len:
614
+ raise IterableTooLong("Unwrapping generates too much", node, self._expr)
615
+ yield retval
616
+ except TypeError:
617
+ raise TypeError(f"Value after * must be iterable, got {type(evalue).__name__}")
618
+ else:
619
+ retval = self._eval(node)
620
+ if check_len:
621
+ total_len += approx_len_of(retval) + 1
622
+ if total_len > self._config.max_const_len:
623
+ raise IterableTooLong("Unwrapping generates too much", node, self._expr)
624
+ yield retval
625
+
626
+ def _starred_keyword_unwrap(self, items, *, check_len=True):
627
+ total_len = 0
628
+
629
+ for key, value in items:
630
+ evalue = self._eval(value)
631
+ if key is None:
632
+ if isinstance(evalue, Mapping):
633
+ for retval in evalue.items():
634
+ self._loops += 1
635
+ if self._loops > self._config.max_loops:
636
+ raise IterableTooLong("Unwrapping generates too many elements", value, self._expr)
637
+ if check_len:
638
+ total_len += sum(approx_len_of(val) for val in retval) + 1
639
+ if total_len > self._config.max_const_len:
640
+ raise IterableTooLong("Unwrapping generates too much", value, self._expr)
641
+ yield retval
642
+ else:
643
+ raise TypeError(f"argument after ** must be a mapping, got {type(value).__name__}")
644
+ else:
645
+ retval = self._eval(key) if isinstance(key, ast.AST) else key, evalue
646
+ if check_len:
647
+ total_len += sum(approx_len_of(val) for val in retval) + 1
648
+ if total_len > self._config.max_const_len:
649
+ raise IterableTooLong("Unwrapping generates too much", value, self._expr)
650
+ yield retval
651
+
652
+ # ===== assignments =====
653
+ def _eval_assign(self, node):
654
+ value = self._eval(node.value)
655
+ for target in node.targets: # a = b = 1
656
+ self._assign(target, value)
657
+
658
+ def _eval_augassign(self, node):
659
+ target = node.target
660
+ # transform a += 1 to a = a + 1, then we can use assign and eval
661
+ new_value = ast.BinOp(left=target, op=node.op, right=node.value)
662
+ ast.copy_location(new_value, target)
663
+ self._assign(target, self._eval_binop(new_value))
664
+
665
+ def _eval_namedexpr(self, node):
666
+ value = self._eval(node.value)
667
+ self._assign(node.target, value)
668
+ return value
669
+
670
+ # ---- primary assign branch ----
671
+ def _assign(self, names, values):
672
+ try:
673
+ handler = self.assign_nodes[type(names)]
674
+ except KeyError:
675
+ raise FeatureNotAvailable(f"Assignment to {type(names).__name__} is not allowed", names, self._expr)
676
+ # noinspection PyArgumentList
677
+ return handler(names, values)
678
+
679
+ def _assign_name(self, name, value):
680
+ if name.id in self.builtins:
681
+ raise DraconicValueError(f"{name.id} is already builtin (no shadow assignments).", name, self._expr)
682
+ self._names[name.id] = value
683
+
684
+ def _assign_subscript(self, name, value):
685
+ container = self._eval(name.value)
686
+ key = self._eval(name.slice)
687
+ container[key] = value # no further evaluation needed, if container is in names it will update
688
+
689
+ def _assign_unpack(self, names, values):
690
+ if not isinstance(names, (ast.Tuple, ast.List)):
691
+ self._assign(names, values)
692
+ else:
693
+ try:
694
+ values = list(iter(values))
695
+ except TypeError:
696
+ raise DraconicValueError(
697
+ f"Cannot unpack non-iterable {type(values).__name__} object", names, self._expr
698
+ )
699
+
700
+ stars = (i for i in names.elts if type(i) is ast.Starred)
701
+ starred = next(stars, None)
702
+
703
+ if starred is None:
704
+ if len(names.elts) > len(values):
705
+ raise DraconicValueError(
706
+ f"not enough values to unpack (expected {len(names.elts)}, got {len(values)})",
707
+ names,
708
+ self._expr,
709
+ )
710
+ elif len(names.elts) < len(values):
711
+ raise DraconicValueError(
712
+ f"too many values to unpack (expected {len(names.elts)}, got {len(values)})",
713
+ names,
714
+ self._expr,
715
+ )
716
+ for t, v in zip(names.elts, values):
717
+ self._assign_unpack(t, v)
718
+ elif (extra := next(stars, None)) is not None:
719
+ raise DraconicSyntaxError.from_node(extra, "multiple starred expressions in assignment", self._expr)
720
+ else:
721
+ if len(values) < (len(names.elts) - 1):
722
+ raise DraconicValueError(
723
+ f"not enough values to unpack (expected at least {len(names.elts) - 1}, got {len(values)})",
724
+ names,
725
+ self._expr,
726
+ )
727
+
728
+ for t, v in zip_star(names.elts, values, star_index=names.elts.index(starred)):
729
+ self._assign_unpack(t, v)
730
+
731
+ def _assign_starred(self, name, value):
732
+ self._assign_name(name.value, value)
733
+
734
+ # ===== execution =====
735
+ def _exec_return(self, node):
736
+ retval = self._eval(node.value) if node.value is not None else None
737
+ return _Return(retval, node)
738
+
739
+ def _exec_if(self, node):
740
+ test = self._eval(node.test)
741
+ if test:
742
+ return self._exec(node.body)
743
+ else:
744
+ return self._exec(node.orelse)
745
+
746
+ def _exec_for(self, node):
747
+ for item in self._eval(node.iter):
748
+ self._loops += 1
749
+ if self._loops > self._config.max_loops:
750
+ raise TooManyStatements("Too many loops (in for block)", node, self._expr)
751
+
752
+ self._assign(node.target, item)
753
+ retval = self._exec(node.body)
754
+ if isinstance(retval, _Return):
755
+ return retval
756
+ elif isinstance(retval, _Break):
757
+ break
758
+ elif isinstance(retval, _Continue):
759
+ continue
760
+ else:
761
+ return self._exec(node.orelse)
762
+
763
+ def _exec_while(self, node):
764
+ while self._eval(node.test):
765
+ self._loops += 1
766
+ if self._loops > self._config.max_loops:
767
+ raise TooManyStatements("Too many loops (in while block)", node, self._expr)
768
+
769
+ retval = self._exec(node.body)
770
+ if isinstance(retval, _Return):
771
+ return retval
772
+ elif isinstance(retval, _Break):
773
+ break
774
+ elif isinstance(retval, _Continue):
775
+ continue
776
+ else:
777
+ return self._exec(node.orelse)
778
+
779
+ # ===== patma =====
780
+ # impl inspired by GVR's impl at https://github.com/gvanrossum/patma/blob/master/patma.py
781
+ # note: we do duplicate binding checks at runtime instead of preflight, which means that
782
+ # some code could run before the duplicate binding is detected, and certain exprs illegal in Python are legal here
783
+ # this is OK for our use case but differs from Python's impl
784
+ def _exec_match(self, node):
785
+ subject = self._eval(node.subject)
786
+ for match_case in node.cases:
787
+ if (bindings := self._patma(match_case.pattern, subject)) is not None:
788
+ self._names.update(bindings) # In python patma, values are bound before the guard executes
789
+ if match_case.guard is not None and not self._eval(match_case.guard):
790
+ continue
791
+ return self._exec(match_case.body)
792
+
793
+ def _patma(self, pattern, subject):
794
+ """
795
+ Execute matching logic for a given match_case and subject.
796
+ If the subject matches the case, return the dict of bindings for this case.
797
+ Otherwise, return None.
798
+ """
799
+ try:
800
+ handler = self.patma_nodes[type(pattern)]
801
+ except KeyError:
802
+ raise FeatureNotAvailable(f"Matching on {type(pattern).__name__} is not allowed", pattern, self._expr)
803
+ self._num_stmts += 1
804
+ return handler(pattern, subject)
805
+
806
+ def _patma_match_value(self, node, subject):
807
+ if subject == self._eval(node.value):
808
+ return {}
809
+ return None
810
+
811
+ @staticmethod
812
+ def _patma_match_singleton(node, subject):
813
+ if subject is node.value:
814
+ return {}
815
+ return None
816
+
817
+ def _patma_match_sequence(self, node, subject):
818
+ if not isinstance(subject, Sequence) or isinstance(subject, (str, bytes)):
819
+ return None
820
+
821
+ match_star_idxs = [idx for idx, pattern in enumerate(node.patterns) if isinstance(pattern, ast.MatchStar)]
822
+ if len(match_star_idxs) > 1:
823
+ # multiple starred names
824
+ raise DraconicValueError(f"multiple starred names in sequence pattern", node, self._expr)
825
+ elif match_star_idxs:
826
+ # one starred name
827
+ if not len(node.patterns) <= len(subject) + 1:
828
+ return None
829
+ pattern_iterator = zip_star(node.patterns, subject, star_index=match_star_idxs[0])
830
+ else:
831
+ # no starred names
832
+ if len(node.patterns) != len(subject):
833
+ return None
834
+ pattern_iterator = zip(node.patterns, subject)
835
+
836
+ # do iteration over patterns and values
837
+ bindings = {}
838
+ bound_names = set()
839
+ for pattern, item in pattern_iterator:
840
+ # recursive check
841
+ # noinspection DuplicatedCode
842
+ match = self._patma(pattern, item)
843
+ if match is None:
844
+ return None
845
+
846
+ # duplicate bindings check
847
+ if bound_names.intersection(match):
848
+ raise DraconicValueError(
849
+ f"multiple assignment to names {sorted(bound_names.intersection(match))} in sequence pattern",
850
+ node,
851
+ self._expr,
852
+ )
853
+ bindings.update(match)
854
+ bound_names.update(match)
855
+
856
+ return bindings
857
+
858
+ def _patma_match_mapping(self, node, subject):
859
+ if not isinstance(subject, Mapping):
860
+ return None
861
+
862
+ bindings = {}
863
+ bound_names = set()
864
+ bound_keys = set()
865
+ for key, pattern in zip(node.keys, node.patterns):
866
+ # recursive check
867
+ key = self._eval(key)
868
+ try:
869
+ value = subject[key]
870
+ except KeyError:
871
+ return None
872
+ # noinspection DuplicatedCode
873
+ match = self._patma(pattern, value)
874
+ if match is None:
875
+ return None
876
+
877
+ # duplicate bindings check
878
+ if bound_names.intersection(match):
879
+ raise DraconicValueError(
880
+ f"multiple assignment to names {sorted(bound_names.intersection(match))} in mapping pattern",
881
+ node,
882
+ self._expr,
883
+ )
884
+ bindings.update(match)
885
+ bound_names.update(match)
886
+ bound_keys.add(key)
887
+
888
+ if node.rest is not None:
889
+ if node.rest in bound_names:
890
+ raise DraconicValueError(
891
+ f"multiple assignment to name {node.rest!r} in mapping pattern", node, self._expr
892
+ )
893
+ bindings[node.rest] = {k: v for k, v in subject.items() if k not in bound_keys}
894
+
895
+ return bindings
896
+
897
+ @staticmethod
898
+ def _patma_match_star(node, subject):
899
+ if node.name is None:
900
+ return {}
901
+ return {node.name: subject}
902
+
903
+ def _patma_match_as(self, node, subject):
904
+ if node.name is None: # this is the wildcard pattern, always matches
905
+ return {}
906
+ if node.pattern is None: # bare name capture pattern, always matches
907
+ return {node.name: subject}
908
+ # otherwise if the inner match matches, we just add an additional binding to it
909
+ inner_match = self._patma(node.pattern, subject)
910
+ if inner_match is None:
911
+ return None
912
+ return {**inner_match, node.name: subject}
913
+
914
+ def _patma_match_or(self, node, subject):
915
+ # since we don't know the subpattern's bindings until it executes, we can't enforce both sides having the
916
+ # same bindings like in Python
917
+ for pattern in node.patterns:
918
+ match = self._patma(pattern, subject)
919
+ if match is not None:
920
+ return match
921
+ return None
922
+
923
+ # ===== functions =====
924
+ # definitions
925
+ def _eval_functiondef(self, node):
926
+ if node.name in self.builtins:
927
+ raise DraconicValueError(f"{node.name} is already builtin (no shadow assignments).", node, self._expr)
928
+ self._names[node.name] = _Function(self, node, self._names, self._expr)
929
+
930
+ def _eval_lambda(self, node):
931
+ return _Lambda(self, node, self._names, self._expr)
932
+
933
+ # executions
934
+ def _eval_call(self, node):
935
+ func = self._eval(node.func)
936
+ args = tuple(self._starred_unwrap(node.args, check_len=False))
937
+ kwargs = dict(self._starred_keyword_unwrap(((k.arg, k.value) for k in node.keywords), check_len=False))
938
+ try:
939
+ return func(*args, **kwargs)
940
+ except DraconicException as e:
941
+ raise NestedException(e.msg, node, self._expr, last_exc=e) from e
942
+
943
+ # noinspection PyProtectedMember
944
+ @contextlib.contextmanager
945
+ def _function_call_context(self, __functiondef, /, *args, **kwargs):
946
+ # check limits
947
+ self._depth += 1
948
+ if self._depth > self._config.max_recursion_depth:
949
+ _raise_in_context(TooMuchRecursion, "Maximum recursion depth exceeded")
950
+ # store current names and expression
951
+ old_names = self._names
952
+ old_expr = self._expr
953
+ # bind closure names and contextual expression
954
+ self._names = __functiondef._outer_scope_names.copy()
955
+ self._expr = __functiondef._defining_expr
956
+
957
+ # yield control to the call
958
+ try:
959
+ self._bind_function_args(__functiondef, *args, **kwargs)
960
+ yield
961
+ finally:
962
+ # restore old names and expr
963
+ self._names = old_names
964
+ self._expr = old_expr
965
+ # reduce recursion depth
966
+ self._depth -= 1
967
+
968
+ # noinspection PyProtectedMember
969
+ def _bind_function_args(self, __functiondef, /, *args, **kwargs):
970
+ # check and bind args
971
+ arguments = __functiondef._node.args
972
+ # check valid pos num
973
+ if len(args) > (numpos := len(arguments.posonlyargs) + len(arguments.args)) and arguments.vararg is None:
974
+ raise TypeError(f"{__functiondef._name}() takes {numpos} positional arguments but {len(args)} were given")
975
+ args_i = 0
976
+ default_i = len(arguments.defaults) - numpos
977
+ # posonly
978
+ for posonly in arguments.posonlyargs:
979
+ if args_i + 1 > len(args):
980
+ if default_i < 0:
981
+ raise TypeError(f"{__functiondef._name}() missing required positional argument: {posonly.arg!r}")
982
+ self._names[posonly.arg] = self._eval(arguments.defaults[default_i])
983
+ else:
984
+ self._names[posonly.arg] = args[args_i]
985
+ args_i += 1
986
+ default_i += 1
987
+ # normal
988
+ for posarg in arguments.args:
989
+ # at least 1
990
+ if args_i + 1 > len(args) and posarg.arg not in kwargs:
991
+ if default_i < 0:
992
+ raise TypeError(f"{__functiondef._name}() missing required positional argument: {posarg.arg!r}")
993
+ self._names[posarg.arg] = self._eval(arguments.defaults[default_i])
994
+ # pos and kw
995
+ elif args_i + 1 <= len(args) and posarg.arg in kwargs:
996
+ raise TypeError(f"{__functiondef._name}() got multiple values for argument {posarg.arg!r}")
997
+ elif posarg.arg in kwargs:
998
+ self._names[posarg.arg] = kwargs.pop(posarg.arg)
999
+ else:
1000
+ # we won't indexerror because if it's not in kwargs and args_i is invalid, the first if catches it
1001
+ self._names[posarg.arg] = args[args_i]
1002
+ args_i += 1
1003
+ default_i += 1
1004
+ # kwargonly
1005
+ for k_i, kwargonly in enumerate(arguments.kwonlyargs):
1006
+ if kwargonly.arg not in kwargs and arguments.kw_defaults[k_i] is None:
1007
+ raise TypeError(f"{__functiondef._name}() missing required keyword argument: {kwargonly.arg!r}")
1008
+ if kwargonly.arg in kwargs:
1009
+ self._names[kwargonly.arg] = kwargs.pop(kwargonly.arg)
1010
+ else:
1011
+ self._names[kwargonly.arg] = self._eval(arguments.kw_defaults[k_i])
1012
+ # *args
1013
+ if arguments.vararg is not None:
1014
+ if approx_len_of(args[args_i:]) > self._config.max_const_len:
1015
+ _raise_in_context(IterableTooLong, f"*{arguments.vararg.arg} would be too large")
1016
+ self._names[arguments.vararg.arg] = tuple(args[args_i:])
1017
+ # **kwargs
1018
+ if arguments.kwarg is not None:
1019
+ if approx_len_of(kwargs) > self._config.max_const_len:
1020
+ _raise_in_context(IterableTooLong, f"**{arguments.kwarg.arg} would be too large")
1021
+ self._names[arguments.kwarg.arg] = kwargs
1022
+ elif kwargs: # and arguments.kwarg is None (implicit)
1023
+ raise TypeError(f"{__functiondef._name}() got unexpected keyword arguments: {tuple(kwargs.keys())}")
1024
+
1025
+ # noinspection PyProtectedMember
1026
+ def _exec_function(self, __functiondef: _Function, /, *args, **kwargs):
1027
+ with self._function_call_context(__functiondef, *args, **kwargs):
1028
+ retval = self._exec(__functiondef._node.body)
1029
+ if isinstance(retval, (_Break, _Continue)):
1030
+ raise DraconicSyntaxError.from_node(retval.node, msg="Loop control outside loop", expr=self._expr)
1031
+ if isinstance(retval, _Return):
1032
+ return retval.value
1033
+
1034
+ # noinspection PyProtectedMember
1035
+ def _exec_lambda(self, __lambdadef: _Lambda, /, *args, **kwargs):
1036
+ with self._function_call_context(__lambdadef, *args, **kwargs):
1037
+ return self._eval(__lambdadef._node.body)
1038
+
1039
+ # ===== try/except =====
1040
+ def _exec_try(self, node: ast.Try):
1041
+ try:
1042
+ retval = self._exec(node.body)
1043
+ if isinstance(retval, (_Return, _Continue, _Break)):
1044
+ return retval
1045
+ except Exception as exc:
1046
+ if isinstance(exc, WrappedException):
1047
+ exc = exc.original
1048
+ # draconic diff: limit errors cannot be caught
1049
+ if isinstance(exc, LimitException):
1050
+ raise
1051
+ # enter into the except handlers
1052
+ for handler in node.handlers:
1053
+ if self._except_handler_matches(handler, exc):
1054
+ retval = self._except_handler(handler)
1055
+ if isinstance(retval, (_Return, _Continue, _Break)):
1056
+ return retval
1057
+ break
1058
+ else:
1059
+ raise
1060
+ else:
1061
+ retval = self._exec(node.orelse)
1062
+ if isinstance(retval, (_Return, _Continue, _Break)):
1063
+ return retval
1064
+ finally:
1065
+ retval = self._exec(node.finalbody)
1066
+ if isinstance(retval, (_Return, _Continue, _Break)):
1067
+ return retval
1068
+
1069
+ def _except_handler_matches(self, node: ast.ExceptHandler, exc: BaseException) -> bool:
1070
+ # draconic diff: exception handlers must be string literals, tuple[str] literals, or bare
1071
+ if node.type is None:
1072
+ return True
1073
+ if isinstance(node.type, ast.Str):
1074
+ return type(exc).__name__ == self._eval_str(node.type)
1075
+ elif isinstance(node.type, ast.Tuple):
1076
+ if not all(isinstance(item, ast.Str) for item in node.type.elts):
1077
+ raise FeatureNotAvailable(
1078
+ "'except' clause expressions must be string literals or tuple of string literals", node, self._expr
1079
+ )
1080
+ return type(exc).__name__ in self._eval_tuple(node.type)
1081
+ else:
1082
+ raise FeatureNotAvailable(
1083
+ "'except' clause expressions must be string literals or tuple of string literals", node, self._expr
1084
+ )
1085
+
1086
+ def _except_handler(self, node: ast.ExceptHandler):
1087
+ # draconic diff: 'as X' clause not allowed
1088
+ if node.name is not None:
1089
+ raise FeatureNotAvailable("'except ... as X' is not available in this interpreter", node, self._expr)
1090
+ # run body
1091
+ return self._exec(node.body)