sonolus.py 0.1.3__py3-none-any.whl → 0.1.4__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.

Potentially problematic release.


This version of sonolus.py might be problematic. Click here for more details.

Files changed (68) hide show
  1. sonolus/backend/allocate.py +125 -51
  2. sonolus/backend/blocks.py +756 -756
  3. sonolus/backend/coalesce.py +85 -0
  4. sonolus/backend/constant_evaluation.py +374 -0
  5. sonolus/backend/dead_code.py +80 -0
  6. sonolus/backend/dominance.py +111 -0
  7. sonolus/backend/excepthook.py +37 -37
  8. sonolus/backend/finalize.py +69 -69
  9. sonolus/backend/flow.py +121 -92
  10. sonolus/backend/inlining.py +150 -0
  11. sonolus/backend/ir.py +5 -3
  12. sonolus/backend/liveness.py +173 -0
  13. sonolus/backend/mode.py +24 -24
  14. sonolus/backend/node.py +40 -40
  15. sonolus/backend/ops.py +197 -197
  16. sonolus/backend/optimize.py +37 -9
  17. sonolus/backend/passes.py +52 -6
  18. sonolus/backend/simplify.py +47 -30
  19. sonolus/backend/ssa.py +187 -0
  20. sonolus/backend/utils.py +48 -48
  21. sonolus/backend/visitor.py +892 -882
  22. sonolus/build/cli.py +7 -1
  23. sonolus/build/compile.py +88 -90
  24. sonolus/build/level.py +24 -23
  25. sonolus/build/node.py +43 -43
  26. sonolus/script/archetype.py +23 -6
  27. sonolus/script/array.py +2 -2
  28. sonolus/script/bucket.py +191 -191
  29. sonolus/script/callbacks.py +127 -127
  30. sonolus/script/comptime.py +1 -1
  31. sonolus/script/containers.py +23 -0
  32. sonolus/script/debug.py +19 -3
  33. sonolus/script/easing.py +323 -0
  34. sonolus/script/effect.py +131 -131
  35. sonolus/script/globals.py +269 -269
  36. sonolus/script/graphics.py +200 -150
  37. sonolus/script/instruction.py +151 -151
  38. sonolus/script/internal/__init__.py +5 -5
  39. sonolus/script/internal/builtin_impls.py +144 -144
  40. sonolus/script/internal/context.py +12 -4
  41. sonolus/script/internal/descriptor.py +17 -17
  42. sonolus/script/internal/introspection.py +14 -14
  43. sonolus/script/internal/native.py +40 -38
  44. sonolus/script/internal/value.py +3 -3
  45. sonolus/script/interval.py +120 -112
  46. sonolus/script/iterator.py +214 -214
  47. sonolus/script/math.py +30 -1
  48. sonolus/script/num.py +1 -1
  49. sonolus/script/options.py +191 -191
  50. sonolus/script/particle.py +157 -157
  51. sonolus/script/pointer.py +30 -30
  52. sonolus/script/print.py +81 -81
  53. sonolus/script/random.py +14 -0
  54. sonolus/script/range.py +58 -58
  55. sonolus/script/record.py +3 -3
  56. sonolus/script/runtime.py +2 -0
  57. sonolus/script/sprite.py +333 -333
  58. sonolus/script/text.py +407 -407
  59. sonolus/script/timing.py +42 -42
  60. sonolus/script/transform.py +77 -23
  61. sonolus/script/ui.py +160 -160
  62. sonolus/script/vec.py +81 -78
  63. {sonolus_py-0.1.3.dist-info → sonolus_py-0.1.4.dist-info}/METADATA +1 -1
  64. sonolus_py-0.1.4.dist-info/RECORD +84 -0
  65. {sonolus_py-0.1.3.dist-info → sonolus_py-0.1.4.dist-info}/WHEEL +1 -1
  66. {sonolus_py-0.1.3.dist-info → sonolus_py-0.1.4.dist-info}/licenses/LICENSE +21 -21
  67. sonolus_py-0.1.3.dist-info/RECORD +0 -75
  68. {sonolus_py-0.1.3.dist-info → sonolus_py-0.1.4.dist-info}/entry_points.txt +0 -0
@@ -1,882 +1,892 @@
1
- # ruff: noqa: N802
2
- import ast
3
- import functools
4
- import inspect
5
- from collections.abc import Callable, Mapping
6
- from contextlib import contextmanager
7
- from types import FunctionType, MethodType
8
- from typing import Any, Never
9
-
10
- from sonolus.backend.excepthook import install_excepthook
11
- from sonolus.backend.utils import get_function, scan_writes
12
- from sonolus.script.debug import assert_true
13
- from sonolus.script.internal.builtin_impls import BUILTIN_IMPLS
14
- from sonolus.script.internal.context import Context, EmptyBinding, Scope, ValueBinding, ctx, set_ctx
15
- from sonolus.script.internal.descriptor import SonolusDescriptor
16
- from sonolus.script.internal.error import CompilationError
17
- from sonolus.script.internal.impl import try_validate_value, validate_value
18
- from sonolus.script.internal.value import Value
19
- from sonolus.script.iterator import SonolusIterator
20
- from sonolus.script.num import Num, is_num
21
-
22
- _compiler_internal_ = True
23
-
24
-
25
- def compile_and_call[**P, R](fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs) -> R:
26
- if not ctx():
27
- return fn(*args, **kwargs)
28
- return validate_value(generate_fn_impl(fn)(*args, **kwargs))
29
-
30
-
31
- def generate_fn_impl(fn: Callable):
32
- install_excepthook()
33
- match fn:
34
- case Value() as value if value._is_py_():
35
- return generate_fn_impl(value._as_py_())
36
- case MethodType() as method:
37
- return functools.partial(generate_fn_impl(method.__func__), method.__self__)
38
- case FunctionType() as function:
39
- if getattr(function, "_meta_fn_", False):
40
- return function
41
- return functools.partial(eval_fn, function)
42
- case _:
43
- if callable(fn) and isinstance(fn, Value):
44
- return generate_fn_impl(fn.__call__)
45
- elif fn is type:
46
- return fn
47
- elif callable(fn):
48
- raise TypeError(f"Unsupported callable {fn!r}")
49
- else:
50
- raise TypeError(f"Not callable {fn!r}")
51
-
52
-
53
- def eval_fn(fn: Callable, /, *args, **kwargs):
54
- source_file, node = get_function(fn)
55
- bound_args = inspect.signature(fn).bind(*args, **kwargs)
56
- bound_args.apply_defaults()
57
- closurevars = inspect.getclosurevars(fn)
58
- global_vars = {**closurevars.nonlocals, **closurevars.globals, **closurevars.builtins}
59
- return Visitor(source_file, bound_args, global_vars).run(node)
60
-
61
-
62
- unary_ops = {
63
- ast.Invert: "__invert__",
64
- ast.UAdd: "__pos__",
65
- ast.USub: "__neg__",
66
- }
67
-
68
- bin_ops = {
69
- ast.Add: "__add__",
70
- ast.Sub: "__sub__",
71
- ast.Mult: "__mul__",
72
- ast.Div: "__truediv__",
73
- ast.FloorDiv: "__floordiv__",
74
- ast.Mod: "__mod__",
75
- ast.Pow: "__pow__",
76
- ast.LShift: "__lshift__",
77
- ast.RShift: "__rshift__",
78
- ast.BitOr: "__or__",
79
- ast.BitAnd: "__and__",
80
- ast.BitXor: "__xor__",
81
- ast.MatMult: "__matmul__",
82
- }
83
-
84
- rbin_ops = {
85
- ast.Add: "__radd__",
86
- ast.Sub: "__rsub__",
87
- ast.Mult: "__rmul__",
88
- ast.Div: "__rtruediv__",
89
- ast.FloorDiv: "__rfloordiv__",
90
- ast.Mod: "__rmod__",
91
- ast.Pow: "__rpow__",
92
- ast.LShift: "__rlshift__",
93
- ast.RShift: "__rrshift__",
94
- ast.BitOr: "__ror__",
95
- ast.BitAnd: "__rand__",
96
- ast.BitXor: "__rxor__",
97
- ast.MatMult: "__rmatmul__",
98
- }
99
-
100
- inplace_ops = {
101
- ast.Add: "__iadd__",
102
- ast.Sub: "__isub__",
103
- ast.Mult: "__imul__",
104
- ast.Div: "__itruediv__",
105
- ast.FloorDiv: "__ifloordiv__",
106
- ast.Mod: "__imod__",
107
- ast.Pow: "__ipow__",
108
- ast.LShift: "__ilshift__",
109
- ast.RShift: "__irshift__",
110
- ast.BitOr: "__ior__",
111
- ast.BitXor: "__ixor__",
112
- ast.BitAnd: "__iand__",
113
- ast.MatMult: "__imatmul__",
114
- }
115
-
116
- comp_ops = {
117
- ast.Eq: "__eq__",
118
- ast.NotEq: "__ne__",
119
- ast.Lt: "__lt__",
120
- ast.LtE: "__le__",
121
- ast.Gt: "__gt__",
122
- ast.GtE: "__ge__",
123
- }
124
-
125
- rcomp_ops = {
126
- ast.Eq: "__req__",
127
- ast.NotEq: "__rne__",
128
- ast.Lt: "__gt__",
129
- ast.LtE: "__ge__",
130
- ast.Gt: "__lt__",
131
- ast.GtE: "__le__",
132
- ast.In: "__contains__",
133
- ast.NotIn: "__contains__",
134
- }
135
-
136
-
137
- class Visitor(ast.NodeVisitor):
138
- source_file: str
139
- globals: dict[str, Any]
140
- bound_args: inspect.BoundArguments
141
- used_names: dict[str, int]
142
- return_ctxs: list[Context] # Contexts at return statements, which will branch to the exit
143
- loop_head_ctxs: list[Context] # Contexts at loop heads, from outer to inner
144
- break_ctxs: list[list[Context]] # Contexts at break statements, from outer to inner
145
-
146
- def __init__(self, source_file: str, bound_args: inspect.BoundArguments, global_vars: dict[str, Any]):
147
- self.source_file = source_file
148
- self.globals = {}
149
- for k, v in global_vars.items():
150
- # Unfortunately, inspect.closurevars also includes attributes
151
- if v is ctx:
152
- raise ValueError("Unexpected use of ctx in non-meta function")
153
- value = try_validate_value(BUILTIN_IMPLS.get(id(v), v))
154
- if value is not None:
155
- self.globals[k] = value
156
- self.bound_args = bound_args
157
- self.used_names = {}
158
- self.return_ctxs = []
159
- self.loop_head_ctxs = []
160
- self.break_ctxs = []
161
-
162
- def run(self, node):
163
- before_ctx = ctx()
164
- set_ctx(before_ctx.branch_with_scope(None, Scope()))
165
- for name, value in self.bound_args.arguments.items():
166
- ctx().scope.set_value(name, validate_value(value))
167
- match node:
168
- case ast.FunctionDef(body=body):
169
- ctx().scope.set_value("$return", validate_value(None))
170
- for stmt in body:
171
- self.visit(stmt)
172
- case _:
173
- raise NotImplementedError("Unsupported syntax")
174
- after_ctx = Context.meet([*self.return_ctxs, ctx()])
175
- result_binding = after_ctx.scope.get_binding("$return")
176
- if not isinstance(result_binding, ValueBinding):
177
- raise ValueError("Function has conflicting return values")
178
- set_ctx(after_ctx.branch_with_scope(None, before_ctx.scope.copy()))
179
- return result_binding.value
180
-
181
- def visit_FunctionDef(self, node):
182
- raise NotImplementedError("Nested functions are not supported")
183
-
184
- def visit_AsyncFunctionDef(self, node):
185
- raise NotImplementedError("Async functions are not supported")
186
-
187
- def visit_ClassDef(self, node):
188
- raise NotImplementedError("Classes within functions are not supported")
189
-
190
- def visit_Return(self, node):
191
- value = self.visit(node.value) if node.value else validate_value(None)
192
- ctx().scope.set_value("$return", value)
193
- self.return_ctxs.append(ctx())
194
- set_ctx(ctx().into_dead())
195
-
196
- def visit_Delete(self, node):
197
- raise NotImplementedError("Delete statements are not supported")
198
-
199
- def visit_Assign(self, node):
200
- value = self.visit(node.value)
201
- for target in node.targets:
202
- self.handle_assign(target, value)
203
-
204
- def visit_TypeAlias(self, node):
205
- raise NotImplementedError("Type aliases are not supported")
206
-
207
- def visit_AugAssign(self, node):
208
- lhs_value = self.visit(node.target)
209
- rhs_value = self.visit(node.value)
210
- inplace_fn_name = inplace_ops[type(node.op)]
211
- regular_fn_name = bin_ops[type(node.op)]
212
- right_fn_name = rbin_ops[type(node.op)]
213
- if hasattr(lhs_value, inplace_fn_name):
214
- result = self.handle_call(node, getattr(lhs_value, inplace_fn_name), rhs_value)
215
- if not self.is_not_implemented(result):
216
- if result is not lhs_value:
217
- raise ValueError("Inplace operation must return the same object")
218
- # Skip the actual assignment because the inplace operation already did the job, as an optimization
219
- # There could be side effects of assignment, but that's atypical
220
- return
221
- if hasattr(lhs_value, regular_fn_name):
222
- result = self.handle_call(node, getattr(lhs_value, regular_fn_name), rhs_value)
223
- if not self.is_not_implemented(result):
224
- self.handle_assign(node.target, result)
225
- return
226
- if hasattr(rhs_value, right_fn_name):
227
- result = self.handle_call(node, getattr(rhs_value, right_fn_name), lhs_value)
228
- if not self.is_not_implemented(result):
229
- self.handle_assign(node.target, result)
230
- return
231
- raise NotImplementedError("Unsupported augmented assignment")
232
-
233
- def visit_AnnAssign(self, node):
234
- value = self.visit(node.value)
235
- self.handle_assign(node.target, value)
236
-
237
- def visit_For(self, node):
238
- iterator = iter(self.visit(node.iter))
239
- if not isinstance(iterator, SonolusIterator):
240
- raise ValueError("Unsupported iterator")
241
- writes = scan_writes(node)
242
- header_ctx = ctx().prepare_loop_header(writes)
243
- self.loop_head_ctxs.append(header_ctx)
244
- self.break_ctxs.append([])
245
- set_ctx(header_ctx)
246
- has_next = self.ensure_boolean_num(self.handle_call(node, iterator.has_next))
247
- ctx().test = has_next.ir()
248
- body_ctx = ctx().branch(None)
249
- else_ctx = ctx().branch(0)
250
-
251
- set_ctx(body_ctx)
252
- self.handle_assign(node.target, self.handle_call(node, iterator.next))
253
- for stmt in node.body:
254
- self.visit(stmt)
255
- ctx().branch_to_loop_header(header_ctx)
256
-
257
- set_ctx(else_ctx)
258
- for stmt in node.orelse:
259
- self.visit(stmt)
260
- else_end_ctx = ctx()
261
-
262
- break_ctxs = self.break_ctxs.pop()
263
- after_ctx = Context.meet([else_end_ctx, *break_ctxs])
264
- set_ctx(after_ctx)
265
-
266
- def visit_While(self, node):
267
- writes = scan_writes(node)
268
- header_ctx = ctx().prepare_loop_header(writes)
269
- self.loop_head_ctxs.append(header_ctx)
270
- self.break_ctxs.append([])
271
- set_ctx(header_ctx)
272
- test = self.ensure_boolean_num(self.visit(node.test))
273
- ctx().test = test.ir()
274
- body_ctx = ctx().branch(None)
275
- else_ctx = ctx().branch(0)
276
-
277
- set_ctx(body_ctx)
278
- for stmt in node.body:
279
- self.visit(stmt)
280
- ctx().branch_to_loop_header(header_ctx)
281
-
282
- set_ctx(else_ctx)
283
- for stmt in node.orelse:
284
- self.visit(stmt)
285
- else_end_ctx = ctx()
286
-
287
- break_ctxs = self.break_ctxs.pop()
288
- after_ctx = Context.meet([else_end_ctx, *break_ctxs])
289
- set_ctx(after_ctx)
290
-
291
- def visit_If(self, node):
292
- test = self.ensure_boolean_num(self.visit(node.test))
293
-
294
- if test._is_py_():
295
- if test._as_py_():
296
- for stmt in node.body:
297
- self.visit(stmt)
298
- else:
299
- for stmt in node.orelse:
300
- self.visit(stmt)
301
- return
302
-
303
- ctx_init = ctx()
304
- ctx_init.test = test.ir()
305
- true_ctx = ctx_init.branch(None)
306
- false_ctx = ctx_init.branch(0)
307
-
308
- set_ctx(true_ctx)
309
- for stmt in node.body:
310
- self.visit(stmt)
311
- true_end_ctx = ctx()
312
-
313
- set_ctx(false_ctx)
314
- for stmt in node.orelse:
315
- self.visit(stmt)
316
- false_end_ctx = ctx()
317
-
318
- set_ctx(Context.meet([true_end_ctx, false_end_ctx]))
319
-
320
- def visit_With(self, node):
321
- raise NotImplementedError("With statements are not supported")
322
-
323
- def visit_AsyncWith(self, node):
324
- raise NotImplementedError("Async with statements are not supported")
325
-
326
- def visit_Match(self, node):
327
- subject = self.visit(node.subject)
328
- end_ctxs = []
329
- for case in node.cases:
330
- if not ctx().live:
331
- break
332
- true_ctx, false_ctx = self.handle_match_pattern(subject, case.pattern)
333
- if not true_ctx.live:
334
- set_ctx(false_ctx)
335
- continue
336
- set_ctx(true_ctx)
337
- guard = self.ensure_boolean_num(self.visit(case.guard)) if case.guard else validate_value(True)
338
- if guard._is_py_():
339
- if guard._as_py_():
340
- for stmt in case.body:
341
- self.visit(stmt)
342
- end_ctxs.append(ctx())
343
- else:
344
- end_ctxs.append(ctx())
345
- else:
346
- ctx().test = guard.ir()
347
- true_ctx = ctx().branch(None)
348
- false_ctx = ctx().branch(0)
349
- set_ctx(true_ctx)
350
- for stmt in case.body:
351
- self.visit(stmt)
352
- end_ctxs.append(ctx())
353
- set_ctx(false_ctx)
354
- if end_ctxs:
355
- set_ctx(Context.meet(end_ctxs))
356
-
357
- def handle_match_pattern(self, subject: Value, pattern: ast.pattern) -> tuple[Context, Context]:
358
- match pattern:
359
- case ast.MatchValue(value=value):
360
- value = self.visit(value)
361
- test = self.ensure_boolean_num(subject == value)
362
- ctx_init = ctx()
363
- ctx_init.test = test.ir()
364
- true_ctx = ctx_init.branch(None)
365
- false_ctx = ctx_init.branch(0)
366
- return true_ctx, false_ctx
367
- case ast.MatchSingleton(value=value):
368
- match value:
369
- case True:
370
- test = self.ensure_boolean_num(subject)
371
- case False:
372
- test = self.ensure_boolean_num(subject).not_()
373
- case None:
374
- test = Num._accept_(subject._is_py_() and subject._as_py_() is None)
375
- case _:
376
- raise NotImplementedError("Unsupported match singleton")
377
- ctx_init = ctx()
378
- ctx_init.test = test.ir()
379
- true_ctx = ctx_init.branch(None)
380
- false_ctx = ctx_init.branch(0)
381
- return true_ctx, false_ctx
382
- case ast.MatchSequence():
383
- raise NotImplementedError("Match sequences are not supported")
384
- case ast.MatchMapping():
385
- raise NotImplementedError("Match mappings are not supported")
386
- case ast.MatchClass(cls=cls, patterns=patterns, kwd_attrs=kwd_attrs, kwd_patterns=kwd_patterns):
387
- from sonolus.script.comptime import Comptime
388
- from sonolus.script.internal.generic import validate_type_spec
389
-
390
- cls = validate_type_spec(self.visit(cls))
391
- if not isinstance(cls, type):
392
- raise TypeError("Class is not a type")
393
- if issubclass(cls, Comptime):
394
- raise TypeError("Comptime is not supported in match patterns")
395
- if not isinstance(subject, cls):
396
- return ctx().into_dead(), ctx()
397
- if patterns:
398
- if not hasattr(cls, "__match_args__"):
399
- raise TypeError("Class does not support match patterns")
400
- if len(cls.__match_args__) < len(patterns):
401
- raise ValueError("Too many match patterns")
402
- # kwd_attrs can't be mixed with patterns on the syntax level,
403
- # so we can just set it like this since it's empty
404
- kwd_attrs = cls.__match_args__[: len(patterns)]
405
- kwd_patterns = patterns
406
- if kwd_attrs:
407
- true_ctx = ctx()
408
- false_ctxs = []
409
- for attr, subpattern in zip(kwd_attrs, kwd_patterns, strict=False):
410
- if not hasattr(subject, attr):
411
- raise AttributeError(f"Object has no attribute {attr}")
412
- value = self.handle_getattr(subpattern, subject, attr)
413
- true_ctx, false_ctx = self.handle_match_pattern(value, subpattern)
414
- false_ctxs.append(false_ctx)
415
- set_ctx(true_ctx)
416
- return true_ctx, Context.meet(false_ctxs)
417
- return ctx(), ctx().into_dead()
418
- case ast.MatchStar():
419
- raise NotImplementedError("Match stars are not supported")
420
- case ast.MatchAs(pattern=pattern, name=name):
421
- if pattern:
422
- true_ctx, false_ctx = self.handle_match_pattern(subject, pattern)
423
- if name:
424
- true_ctx.scope.set_value(name, subject)
425
- return true_ctx, false_ctx
426
- else:
427
- if name:
428
- ctx().scope.set_value(name, subject)
429
- return ctx(), ctx().into_dead()
430
- case ast.MatchOr():
431
- true_ctxs = []
432
- false_ctx = ctx()
433
- assert pattern.patterns
434
- for subpattern in pattern.patterns:
435
- true_ctx, false_ctx = self.handle_match_pattern(subject, subpattern)
436
- true_ctxs.append(true_ctx)
437
- set_ctx(false_ctx)
438
- return Context.meet(true_ctxs), false_ctx
439
-
440
- def visit_Raise(self, node):
441
- raise NotImplementedError("Raise statements are not supported")
442
-
443
- def visit_Try(self, node):
444
- raise NotImplementedError("Try statements are not supported")
445
-
446
- def visit_TryStar(self, node):
447
- raise NotImplementedError("Try* statements are not supported")
448
-
449
- def visit_Assert(self, node):
450
- self.handle_call(
451
- node, assert_true, self.visit(node.test), self.visit(node.msg) if node.msg else validate_value(None)
452
- )
453
-
454
- def visit_Import(self, node):
455
- raise NotImplementedError("Import statements are not supported")
456
-
457
- def visit_ImportFrom(self, node):
458
- raise NotImplementedError("Import statements are not supported")
459
-
460
- def visit_Global(self, node):
461
- raise NotImplementedError("Global statements are not supported")
462
-
463
- def visit_Nonlocal(self, node):
464
- raise NotImplementedError("Nonlocal statements are not supported")
465
-
466
- def visit_Expr(self, node):
467
- return self.visit(node.value)
468
-
469
- def visit_Pass(self, node):
470
- pass
471
-
472
- def visit_Break(self, node):
473
- self.break_ctxs[-1].append(ctx())
474
- set_ctx(ctx().into_dead())
475
-
476
- def visit_Continue(self, node):
477
- ctx().branch_to_loop_header(self.loop_head_ctxs[-1])
478
- set_ctx(ctx().into_dead())
479
-
480
- def visit_BoolOp(self, node) -> Value:
481
- match node.op:
482
- case ast.And():
483
- handler = self.handle_and
484
- case ast.Or():
485
- handler = self.handle_or
486
- case _:
487
- raise NotImplementedError(f"Unsupported bool operator {node.op}")
488
-
489
- if not node.values:
490
- raise ValueError("Bool operator requires at least one operand")
491
- if len(node.values) == 1:
492
- return self.visit(node.values[0])
493
- initial, *rest = node.values
494
- return handler(self.visit(initial), ast.copy_location(ast.BoolOp(op=node.op, values=rest), node))
495
-
496
- def visit_NamedExpr(self, node):
497
- value = self.visit(node.value)
498
- self.handle_assign(node.target, value)
499
- return value
500
-
501
- def visit_BinOp(self, node):
502
- lhs = self.visit(node.left)
503
- rhs = self.visit(node.right)
504
- op = bin_ops[type(node.op)]
505
- if hasattr(lhs, op):
506
- result = self.handle_call(node, getattr(lhs, op), rhs)
507
- if not self.is_not_implemented(result):
508
- return result
509
- if hasattr(rhs, rbin_ops[type(node.op)]):
510
- result = self.handle_call(node, getattr(rhs, rbin_ops[type(node.op)]), lhs)
511
- if not self.is_not_implemented(result):
512
- return result
513
- raise NotImplementedError(f"Unsupported operand types for binary operator {node.op}")
514
-
515
- def visit_UnaryOp(self, node):
516
- operand = self.visit(node.operand)
517
- if isinstance(node.op, ast.Not):
518
- return self.ensure_boolean_num(operand).not_()
519
- op = unary_ops[type(node.op)]
520
- if hasattr(operand, op):
521
- return self.handle_call(node, getattr(operand, op))
522
- raise NotImplementedError(f"Unsupported operand type for unary operator {node.op}")
523
-
524
- def visit_Lambda(self, node):
525
- raise NotImplementedError("Lambda functions are not supported")
526
-
527
- def visit_IfExp(self, node):
528
- test = self.ensure_boolean_num(self.visit(node.test))
529
-
530
- if test._is_py_():
531
- if test._as_py_():
532
- return self.visit(node.body)
533
- else:
534
- return self.visit(node.orelse)
535
-
536
- res_name = self.new_name("ifexp")
537
- ctx_init = ctx()
538
- ctx_init.test = test.ir()
539
-
540
- set_ctx(ctx_init.branch(None))
541
- true_value = self.visit(node.body)
542
- ctx().scope.set_value(res_name, true_value)
543
- ctx_true = ctx()
544
-
545
- set_ctx(ctx_init.branch(0))
546
- false_value = self.visit(node.orelse)
547
- ctx().scope.set_value(res_name, false_value)
548
- ctx_false = ctx()
549
-
550
- set_ctx(Context.meet([ctx_true, ctx_false]))
551
- return ctx().scope.get_value(res_name)
552
-
553
- def visit_Dict(self, node):
554
- return validate_value({self.visit(k): self.visit(v) for k, v in zip(node.keys, node.values, strict=True)})
555
-
556
- def visit_Set(self, node):
557
- raise NotImplementedError("Set literals are not supported")
558
-
559
- def visit_ListComp(self, node):
560
- raise NotImplementedError("List comprehensions are not supported")
561
-
562
- def visit_SetComp(self, node):
563
- raise NotImplementedError("Set comprehensions are not supported")
564
-
565
- def visit_DictComp(self, node):
566
- raise NotImplementedError("Dict comprehensions are not supported")
567
-
568
- def visit_GeneratorExp(self, node):
569
- raise NotImplementedError("Generator expressions are not supported")
570
-
571
- def visit_Await(self, node):
572
- raise NotImplementedError("Await expressions are not supported")
573
-
574
- def visit_Yield(self, node):
575
- raise NotImplementedError("Yield expressions are not supported")
576
-
577
- def visit_YieldFrom(self, node):
578
- raise NotImplementedError("Yield from expressions are not supported")
579
-
580
- def visit_Compare(self, node):
581
- result_name = self.new_name("compare")
582
- ctx().scope.set_value(result_name, Num._accept_(0))
583
- l_val = self.visit(node.left)
584
- false_ctxs = []
585
- for i, (op, rhs) in enumerate(zip(node.ops, node.comparators, strict=True)):
586
- r_val = self.visit(rhs)
587
- inverted = isinstance(op, ast.NotIn)
588
- result = None
589
- if isinstance(op, ast.Is | ast.IsNot):
590
- if not (r_val._is_py_() and r_val._as_py_() is None):
591
- raise TypeError("The right operand of 'is' must be None")
592
- if isinstance(op, ast.Is):
593
- result = Num._accept_(l_val._is_py_() and l_val._as_py_() is None)
594
- else:
595
- result = Num._accept_(not (l_val._is_py_() and l_val._as_py_() is None))
596
- elif type(op) in comp_ops and hasattr(l_val, comp_ops[type(op)]):
597
- result = self.handle_call(node, getattr(l_val, comp_ops[type(op)]), r_val)
598
- if (
599
- (result is None or self.is_not_implemented(result))
600
- and type(op) in rcomp_ops
601
- and hasattr(r_val, rcomp_ops[type(op)])
602
- ):
603
- result = self.handle_call(node, getattr(r_val, rcomp_ops[type(op)]), l_val)
604
- if result is None or self.is_not_implemented(result):
605
- raise NotImplementedError(f"Unsupported comparison operator {op}")
606
- result = self.ensure_boolean_num(result)
607
- if inverted:
608
- result = result.not_()
609
- curr_ctx = ctx()
610
- if i == len(node.ops) - 1:
611
- curr_ctx.scope.set_value(result_name, result)
612
- else:
613
- curr_ctx.test = result.ir()
614
- true_ctx = curr_ctx.branch(None)
615
- false_ctx = curr_ctx.branch(0)
616
- false_ctxs.append(false_ctx)
617
- set_ctx(true_ctx)
618
- l_val = r_val
619
- last_ctx = ctx() # This is the result of the last comparison returning true
620
- set_ctx(Context.meet([last_ctx, *false_ctxs]))
621
- return ctx().scope.get_value(result_name)
622
-
623
- def visit_Call(self, node):
624
- fn = self.visit(node.func)
625
- if fn is Num:
626
- raise ValueError("Calling int/bool/float is not supported")
627
- args = []
628
- kwargs = {}
629
- for arg in node.args:
630
- if isinstance(arg, ast.Starred):
631
- args.extend(self.handle_starred(self.visit(arg.value)))
632
- else:
633
- args.append(self.visit(arg))
634
- for keyword in node.keywords:
635
- if keyword.arg:
636
- kwargs[keyword.arg] = self.visit(keyword.value)
637
- else:
638
- value = self.visit(keyword.value)
639
- if value._is_py_() and isinstance(value._as_py_(), Mapping):
640
- kwargs.update(value._as_py_())
641
- else:
642
- raise ValueError("Starred keyword arguments (**kwargs) must be dictionaries")
643
- return self.handle_call(node, fn, *args, **kwargs)
644
-
645
- def visit_FormattedValue(self, node):
646
- raise NotImplementedError("F-strings are not supported")
647
-
648
- def visit_JoinedStr(self, node):
649
- raise NotImplementedError("F-strings are not supported")
650
-
651
- def visit_Constant(self, node):
652
- return validate_value(node.value)
653
-
654
- def visit_Attribute(self, node):
655
- return self.handle_getattr(node, self.visit(node.value), node.attr)
656
-
657
- def visit_Subscript(self, node):
658
- value = self.visit(node.value)
659
- slice_value = self.visit(node.slice)
660
- return self.handle_getitem(node, value, slice_value)
661
-
662
- def visit_Starred(self, node):
663
- raise NotImplementedError("Starred expressions are not supported")
664
-
665
- def visit_Name(self, node):
666
- if isinstance(ctx().scope.get_binding(node.id), EmptyBinding) and node.id in self.globals:
667
- # globals can have false positives due to limitations of inspect.closurevars
668
- # so we need to check that it's not defined as a local variable
669
- return self.globals[node.id]
670
- return ctx().scope.get_value(node.id)
671
-
672
- def visit_List(self, node):
673
- raise NotImplementedError("List literals are not supported")
674
-
675
- def visit_Tuple(self, node):
676
- values = []
677
- for elt in node.elts:
678
- if isinstance(elt, ast.Starred):
679
- values.extend(self.handle_starred(self.visit(elt.value)))
680
- else:
681
- values.append(self.visit(elt))
682
- return validate_value(tuple(values))
683
-
684
- def visit_Slice(self, node):
685
- raise NotImplementedError("Slices are not supported")
686
-
687
- def handle_assign(self, target: ast.stmt | ast.expr, value: Value):
688
- match target:
689
- case ast.Name(id=name):
690
- ctx().scope.set_value(name, value)
691
- case ast.Attribute(value=attr_value, attr=attr):
692
- attr_value = self.visit(attr_value)
693
- self.handle_setattr(target, attr_value, attr, value)
694
- case ast.Subscript(value=sub_value, slice=slice_expr):
695
- sub_value = self.visit(sub_value)
696
- slice_value = self.visit(slice_expr)
697
- self.handle_setitem(target, sub_value, slice_value, value)
698
- case ast.Tuple(elts=elts) | ast.List(elts=elts):
699
- values = self.handle_starred(value)
700
- if len(elts) != len(values):
701
- raise ValueError("Unpacking assignment requires the same number of elements")
702
- for elt, v in zip(elts, values, strict=False):
703
- self.handle_assign(elt, validate_value(v))
704
- case ast.Starred():
705
- raise NotImplementedError("Starred assignment is not supported")
706
- case _:
707
- raise NotImplementedError("Unsupported assignment target")
708
-
709
- def handle_and(self, l_val: Value, r_expr: ast.expr) -> Value:
710
- ctx_init = ctx()
711
- l_val = self.ensure_boolean_num(l_val)
712
- ctx_init.test = l_val.ir()
713
- res_name = self.new_name("and")
714
-
715
- set_ctx(ctx_init.branch(None))
716
- r_val = self.ensure_boolean_num(self.visit(r_expr))
717
- ctx().scope.set_value(res_name, r_val)
718
- ctx_true = ctx()
719
-
720
- set_ctx(ctx_init.branch(0))
721
- ctx().scope.set_value(res_name, Num._accept_(0))
722
- ctx_false = ctx()
723
-
724
- set_ctx(Context.meet([ctx_true, ctx_false]))
725
- if l_val._is_py_() and r_val._is_py_():
726
- return Num._accept_(l_val._as_py_() and r_val._as_py_())
727
- return ctx().scope.get_value(res_name)
728
-
729
- def handle_or(self, l_val: Value, r_expr: ast.expr) -> Value:
730
- ctx_init = ctx()
731
- l_val = self.ensure_boolean_num(l_val)
732
- ctx_init.test = l_val.ir()
733
- res_name = self.new_name("or")
734
-
735
- set_ctx(ctx_init.branch(None))
736
- ctx().scope.set_value(res_name, l_val)
737
- ctx_true = ctx()
738
-
739
- set_ctx(ctx_init.branch(0))
740
- r_val = self.ensure_boolean_num(self.visit(r_expr))
741
- ctx().scope.set_value(res_name, r_val)
742
- ctx_false = ctx()
743
-
744
- set_ctx(Context.meet([ctx_true, ctx_false]))
745
- if l_val._is_py_() and r_val._is_py_():
746
- return Num._accept_(l_val._as_py_() or r_val._as_py_())
747
- return ctx().scope.get_value(res_name)
748
-
749
- def generic_visit(self, node):
750
- if isinstance(node, ast.stmt | ast.expr):
751
- with self.reporting_errors_at_node(node):
752
- raise NotImplementedError(f"Unsupported syntax: {type(node).__name__}")
753
- raise NotImplementedError(f"Unsupported syntax: {type(node).__name__}")
754
-
755
- def handle_getattr(self, node: ast.stmt | ast.expr, target: Value, key: str) -> Value:
756
- with self.reporting_errors_at_node(node):
757
- if target._is_py_():
758
- target = target._as_py_()
759
- descriptor = type(target).__dict__.get(key)
760
- match descriptor:
761
- case property(fget=getter):
762
- return self.handle_call(node, getter, target)
763
- case SonolusDescriptor() | FunctionType() | classmethod() | staticmethod() | None:
764
- return validate_value(getattr(target, key))
765
- case non_descriptor if not hasattr(non_descriptor, "__get__"):
766
- return validate_value(getattr(target, key))
767
- case _:
768
- raise TypeError(f"Unsupported field or descriptor {key}")
769
-
770
- def handle_setattr(self, node: ast.stmt | ast.expr, target: Value, key: str, value: Value):
771
- with self.reporting_errors_at_node(node):
772
- if target._is_py_():
773
- target = target._as_py_()
774
- descriptor = getattr(type(target), key, None)
775
- match descriptor:
776
- case property(fset=setter):
777
- if setter is None:
778
- raise AttributeError(f"Cannot set attribute {key} because property has no setter")
779
- self.handle_call(node, setter, target, value)
780
- case SonolusDescriptor():
781
- setattr(target, key, value)
782
- case _:
783
- raise TypeError(f"Unsupported field or descriptor {key}")
784
-
785
- def handle_call[**P, R](
786
- self, node: ast.stmt | ast.expr, fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs
787
- ) -> R:
788
- """Handles a call to the given callable."""
789
- if (
790
- isinstance(fn, Value)
791
- and fn._is_py_()
792
- and isinstance(fn._as_py_(), type)
793
- and issubclass(fn._as_py_(), Value)
794
- ):
795
- return validate_value(self.execute_at_node(node, fn._as_py_(), *args, **kwargs))
796
- else:
797
- return self.execute_at_node(node, lambda: validate_value(compile_and_call(fn, *args, **kwargs)))
798
-
799
- def handle_getitem(self, node: ast.stmt | ast.expr, target: Value, key: Value) -> Value:
800
- with self.reporting_errors_at_node(node):
801
- if target._is_py_():
802
- target = target._as_py_()
803
- if key._is_py_():
804
- return validate_value(target[key._as_py_()])
805
- if isinstance(target, Value) and hasattr(target, "__getitem__"):
806
- return self.handle_call(node, target.__getitem__, key)
807
- raise TypeError(f"Cannot get items on {type(target).__name__}")
808
- else:
809
- if isinstance(target, Value) and hasattr(target, "__getitem__"):
810
- return self.handle_call(node, target.__getitem__, key)
811
- raise TypeError(f"Cannot get items on {type(target).__name__}")
812
-
813
- def handle_setitem(self, node: ast.stmt | ast.expr, target: Value, key: Value, value: Value):
814
- with self.reporting_errors_at_node(node):
815
- if target._is_py_():
816
- target = target._as_py_()
817
- if key._is_py_():
818
- target[key._as_py_()] = value._as_py_()
819
- if isinstance(target, Value) and hasattr(target, "__setitem__"):
820
- return self.handle_call(node, target.__setitem__, key, value)
821
- raise TypeError(f"Cannot set items on {type(target).__name__}")
822
- else:
823
- if isinstance(target, Value) and hasattr(target, "__setitem__"):
824
- return self.handle_call(node, target.__setitem__, key, value)
825
- raise TypeError(f"Cannot set items on {type(target).__name__}")
826
-
827
- def handle_starred(self, value: Value) -> tuple[Value, ...]:
828
- if value._is_py_() and isinstance(value._as_py_(), tuple):
829
- return value._as_py_()
830
- raise ValueError("Unsupported starred expression")
831
-
832
- def is_not_implemented(self, value):
833
- value = validate_value(value)
834
- return value._is_py_() and value._as_py_() is NotImplemented
835
-
836
- def ensure_boolean_num(self, value) -> Num:
837
- # This just checks the type for now, although we could support custom __bool__ implementations in the future
838
- if not is_num(value):
839
- raise TypeError(f"Invalid type where a bool (Num) was expected: {type(value).__name__}")
840
- return value
841
-
842
- def raise_exception_at_node(self, node: ast.stmt | ast.expr, cause: Exception) -> Never:
843
- """Throws a compilation error at the given node."""
844
-
845
- def thrower() -> Never:
846
- raise CompilationError(str(cause)) from cause
847
-
848
- self.execute_at_node(node, thrower)
849
-
850
- def execute_at_node[**P, R](
851
- self, node: ast.stmt | ast.expr, fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs
852
- ) -> R:
853
- """Executes the given function at the given node for a better traceback."""
854
- expr = ast.Expression(
855
- body=ast.Call(
856
- func=ast.Name(id="fn", ctx=ast.Load()),
857
- args=[ast.Starred(value=ast.Name(id="args", ctx=ast.Load()), ctx=ast.Load())],
858
- keywords=[ast.keyword(value=ast.Name(id="kwargs", ctx=ast.Load()), arg=None)],
859
- lineno=node.lineno,
860
- col_offset=node.col_offset,
861
- end_lineno=node.end_lineno,
862
- end_col_offset=node.end_col_offset,
863
- ),
864
- )
865
- expr = ast.fix_missing_locations(expr)
866
- return eval(
867
- compile(expr, filename=self.source_file, mode="eval"),
868
- {"fn": fn, "args": args, "kwargs": kwargs, "_filter_traceback_": True},
869
- )
870
-
871
- @contextmanager
872
- def reporting_errors_at_node(self, node: ast.stmt | ast.expr):
873
- try:
874
- yield
875
- except CompilationError as e:
876
- raise e from None
877
- except Exception as e:
878
- self.raise_exception_at_node(node, e)
879
-
880
- def new_name(self, name: str):
881
- self.used_names[name] = self.used_names.get(name, 0) + 1
882
- return f"${name}_{self.used_names[name]}"
1
+ # ruff: noqa: N802
2
+ import ast
3
+ import functools
4
+ import inspect
5
+ from collections.abc import Callable, Mapping
6
+ from contextlib import contextmanager
7
+ from types import FunctionType, MethodType
8
+ from typing import Any, Never
9
+
10
+ from sonolus.backend.excepthook import install_excepthook
11
+ from sonolus.backend.utils import get_function, scan_writes
12
+ from sonolus.script.debug import assert_true
13
+ from sonolus.script.internal.builtin_impls import BUILTIN_IMPLS
14
+ from sonolus.script.internal.context import Context, EmptyBinding, Scope, ValueBinding, ctx, set_ctx
15
+ from sonolus.script.internal.descriptor import SonolusDescriptor
16
+ from sonolus.script.internal.error import CompilationError
17
+ from sonolus.script.internal.impl import try_validate_value, validate_value
18
+ from sonolus.script.internal.value import Value
19
+ from sonolus.script.iterator import SonolusIterator
20
+ from sonolus.script.num import Num, is_num
21
+
22
+ _compiler_internal_ = True
23
+
24
+
25
+ def compile_and_call[**P, R](fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs) -> R:
26
+ if not ctx():
27
+ return fn(*args, **kwargs)
28
+ return validate_value(generate_fn_impl(fn)(*args, **kwargs))
29
+
30
+
31
+ def generate_fn_impl(fn: Callable):
32
+ install_excepthook()
33
+ match fn:
34
+ case Value() as value if value._is_py_():
35
+ return generate_fn_impl(value._as_py_())
36
+ case MethodType() as method:
37
+ return functools.partial(generate_fn_impl(method.__func__), method.__self__)
38
+ case FunctionType() as function:
39
+ if getattr(function, "_meta_fn_", False):
40
+ return function
41
+ return functools.partial(eval_fn, function)
42
+ case _:
43
+ if callable(fn) and isinstance(fn, Value):
44
+ return generate_fn_impl(fn.__call__)
45
+ elif fn is type:
46
+ return fn
47
+ elif callable(fn):
48
+ raise TypeError(f"Unsupported callable {fn!r}")
49
+ else:
50
+ raise TypeError(f"Not callable {fn!r}")
51
+
52
+
53
+ def eval_fn(fn: Callable, /, *args, **kwargs):
54
+ source_file, node = get_function(fn)
55
+ bound_args = inspect.signature(fn).bind(*args, **kwargs)
56
+ bound_args.apply_defaults()
57
+ closurevars = inspect.getclosurevars(fn)
58
+ global_vars = {**closurevars.nonlocals, **closurevars.globals, **closurevars.builtins}
59
+ return Visitor(source_file, bound_args, global_vars).run(node)
60
+
61
+
62
+ unary_ops = {
63
+ ast.Invert: "__invert__",
64
+ ast.UAdd: "__pos__",
65
+ ast.USub: "__neg__",
66
+ }
67
+
68
+ bin_ops = {
69
+ ast.Add: "__add__",
70
+ ast.Sub: "__sub__",
71
+ ast.Mult: "__mul__",
72
+ ast.Div: "__truediv__",
73
+ ast.FloorDiv: "__floordiv__",
74
+ ast.Mod: "__mod__",
75
+ ast.Pow: "__pow__",
76
+ ast.LShift: "__lshift__",
77
+ ast.RShift: "__rshift__",
78
+ ast.BitOr: "__or__",
79
+ ast.BitAnd: "__and__",
80
+ ast.BitXor: "__xor__",
81
+ ast.MatMult: "__matmul__",
82
+ }
83
+
84
+ rbin_ops = {
85
+ ast.Add: "__radd__",
86
+ ast.Sub: "__rsub__",
87
+ ast.Mult: "__rmul__",
88
+ ast.Div: "__rtruediv__",
89
+ ast.FloorDiv: "__rfloordiv__",
90
+ ast.Mod: "__rmod__",
91
+ ast.Pow: "__rpow__",
92
+ ast.LShift: "__rlshift__",
93
+ ast.RShift: "__rrshift__",
94
+ ast.BitOr: "__ror__",
95
+ ast.BitAnd: "__rand__",
96
+ ast.BitXor: "__rxor__",
97
+ ast.MatMult: "__rmatmul__",
98
+ }
99
+
100
+ inplace_ops = {
101
+ ast.Add: "__iadd__",
102
+ ast.Sub: "__isub__",
103
+ ast.Mult: "__imul__",
104
+ ast.Div: "__itruediv__",
105
+ ast.FloorDiv: "__ifloordiv__",
106
+ ast.Mod: "__imod__",
107
+ ast.Pow: "__ipow__",
108
+ ast.LShift: "__ilshift__",
109
+ ast.RShift: "__irshift__",
110
+ ast.BitOr: "__ior__",
111
+ ast.BitXor: "__ixor__",
112
+ ast.BitAnd: "__iand__",
113
+ ast.MatMult: "__imatmul__",
114
+ }
115
+
116
+ comp_ops = {
117
+ ast.Eq: "__eq__",
118
+ ast.NotEq: "__ne__",
119
+ ast.Lt: "__lt__",
120
+ ast.LtE: "__le__",
121
+ ast.Gt: "__gt__",
122
+ ast.GtE: "__ge__",
123
+ }
124
+
125
+ rcomp_ops = {
126
+ ast.Eq: "__req__",
127
+ ast.NotEq: "__rne__",
128
+ ast.Lt: "__gt__",
129
+ ast.LtE: "__ge__",
130
+ ast.Gt: "__lt__",
131
+ ast.GtE: "__le__",
132
+ ast.In: "__contains__",
133
+ ast.NotIn: "__contains__",
134
+ }
135
+
136
+
137
+ class Visitor(ast.NodeVisitor):
138
+ source_file: str
139
+ globals: dict[str, Any]
140
+ bound_args: inspect.BoundArguments
141
+ used_names: dict[str, int]
142
+ return_ctxs: list[Context] # Contexts at return statements, which will branch to the exit
143
+ loop_head_ctxs: list[Context] # Contexts at loop heads, from outer to inner
144
+ break_ctxs: list[list[Context]] # Contexts at break statements, from outer to inner
145
+
146
+ def __init__(self, source_file: str, bound_args: inspect.BoundArguments, global_vars: dict[str, Any]):
147
+ self.source_file = source_file
148
+ self.globals = {}
149
+ for k, v in global_vars.items():
150
+ # Unfortunately, inspect.closurevars also includes attributes
151
+ if v is ctx:
152
+ raise ValueError("Unexpected use of ctx in non-meta function")
153
+ value = try_validate_value(BUILTIN_IMPLS.get(id(v), v))
154
+ if value is not None:
155
+ self.globals[k] = value
156
+ self.bound_args = bound_args
157
+ self.used_names = {}
158
+ self.return_ctxs = []
159
+ self.loop_head_ctxs = []
160
+ self.break_ctxs = []
161
+
162
+ def run(self, node):
163
+ before_ctx = ctx()
164
+ set_ctx(before_ctx.branch_with_scope(None, Scope()))
165
+ for name, value in self.bound_args.arguments.items():
166
+ ctx().scope.set_value(name, validate_value(value))
167
+ match node:
168
+ case ast.FunctionDef(body=body):
169
+ ctx().scope.set_value("$return", validate_value(None))
170
+ for stmt in body:
171
+ self.visit(stmt)
172
+ case _:
173
+ raise NotImplementedError("Unsupported syntax")
174
+ after_ctx = Context.meet([*self.return_ctxs, ctx()])
175
+ result_binding = after_ctx.scope.get_binding("$return")
176
+ if not isinstance(result_binding, ValueBinding):
177
+ raise ValueError("Function has conflicting return values")
178
+ set_ctx(after_ctx.branch_with_scope(None, before_ctx.scope.copy()))
179
+ return result_binding.value
180
+
181
+ def visit_FunctionDef(self, node):
182
+ raise NotImplementedError("Nested functions are not supported")
183
+
184
+ def visit_AsyncFunctionDef(self, node):
185
+ raise NotImplementedError("Async functions are not supported")
186
+
187
+ def visit_ClassDef(self, node):
188
+ raise NotImplementedError("Classes within functions are not supported")
189
+
190
+ def visit_Return(self, node):
191
+ value = self.visit(node.value) if node.value else validate_value(None)
192
+ ctx().scope.set_value("$return", value)
193
+ self.return_ctxs.append(ctx())
194
+ set_ctx(ctx().into_dead())
195
+
196
+ def visit_Delete(self, node):
197
+ raise NotImplementedError("Delete statements are not supported")
198
+
199
+ def visit_Assign(self, node):
200
+ value = self.visit(node.value)
201
+ for target in node.targets:
202
+ self.handle_assign(target, value)
203
+
204
+ def visit_TypeAlias(self, node):
205
+ raise NotImplementedError("Type aliases are not supported")
206
+
207
+ def visit_AugAssign(self, node):
208
+ lhs_value = self.visit(node.target)
209
+ rhs_value = self.visit(node.value)
210
+ inplace_fn_name = inplace_ops[type(node.op)]
211
+ regular_fn_name = bin_ops[type(node.op)]
212
+ right_fn_name = rbin_ops[type(node.op)]
213
+ if hasattr(lhs_value, inplace_fn_name):
214
+ result = self.handle_call(node, getattr(lhs_value, inplace_fn_name), rhs_value)
215
+ if not self.is_not_implemented(result):
216
+ if result is not lhs_value:
217
+ raise ValueError("Inplace operation must return the same object")
218
+ # Skip the actual assignment because the inplace operation already did the job, as an optimization
219
+ # There could be side effects of assignment, but that's atypical
220
+ return
221
+ if hasattr(lhs_value, regular_fn_name):
222
+ result = self.handle_call(node, getattr(lhs_value, regular_fn_name), rhs_value)
223
+ if not self.is_not_implemented(result):
224
+ self.handle_assign(node.target, result)
225
+ return
226
+ if hasattr(rhs_value, right_fn_name):
227
+ result = self.handle_call(node, getattr(rhs_value, right_fn_name), lhs_value)
228
+ if not self.is_not_implemented(result):
229
+ self.handle_assign(node.target, result)
230
+ return
231
+ raise NotImplementedError("Unsupported augmented assignment")
232
+
233
+ def visit_AnnAssign(self, node):
234
+ value = self.visit(node.value)
235
+ self.handle_assign(node.target, value)
236
+
237
+ def visit_For(self, node):
238
+ iterator = iter(self.visit(node.iter))
239
+ if not isinstance(iterator, SonolusIterator):
240
+ raise ValueError("Unsupported iterator")
241
+ writes = scan_writes(node)
242
+ header_ctx = ctx().prepare_loop_header(writes)
243
+ self.loop_head_ctxs.append(header_ctx)
244
+ self.break_ctxs.append([])
245
+ set_ctx(header_ctx)
246
+ has_next = self.ensure_boolean_num(self.handle_call(node, iterator.has_next))
247
+ ctx().test = has_next.ir()
248
+ body_ctx = ctx().branch(None)
249
+ else_ctx = ctx().branch(0)
250
+
251
+ set_ctx(body_ctx)
252
+ self.handle_assign(node.target, self.handle_call(node, iterator.next))
253
+ for stmt in node.body:
254
+ self.visit(stmt)
255
+ ctx().branch_to_loop_header(header_ctx)
256
+
257
+ set_ctx(else_ctx)
258
+ for stmt in node.orelse:
259
+ self.visit(stmt)
260
+ else_end_ctx = ctx()
261
+
262
+ self.loop_head_ctxs.pop()
263
+ break_ctxs = self.break_ctxs.pop()
264
+ after_ctx = Context.meet([else_end_ctx, *break_ctxs])
265
+ set_ctx(after_ctx)
266
+
267
+ def visit_While(self, node):
268
+ writes = scan_writes(node)
269
+ header_ctx = ctx().prepare_loop_header(writes)
270
+ self.loop_head_ctxs.append(header_ctx)
271
+ self.break_ctxs.append([])
272
+ set_ctx(header_ctx)
273
+ test = self.ensure_boolean_num(self.visit(node.test))
274
+ ctx().test = test.ir()
275
+ body_ctx = ctx().branch(None)
276
+ else_ctx = ctx().branch(0)
277
+
278
+ set_ctx(body_ctx)
279
+ for stmt in node.body:
280
+ self.visit(stmt)
281
+ ctx().branch_to_loop_header(header_ctx)
282
+
283
+ set_ctx(else_ctx)
284
+ for stmt in node.orelse:
285
+ self.visit(stmt)
286
+ else_end_ctx = ctx()
287
+
288
+ self.loop_head_ctxs.pop()
289
+ break_ctxs = self.break_ctxs.pop()
290
+ after_ctx = Context.meet([else_end_ctx, *break_ctxs])
291
+ set_ctx(after_ctx)
292
+
293
+ def visit_If(self, node):
294
+ test = self.ensure_boolean_num(self.visit(node.test))
295
+
296
+ if test._is_py_():
297
+ if test._as_py_():
298
+ for stmt in node.body:
299
+ self.visit(stmt)
300
+ else:
301
+ for stmt in node.orelse:
302
+ self.visit(stmt)
303
+ return
304
+
305
+ ctx_init = ctx()
306
+ ctx_init.test = test.ir()
307
+ true_ctx = ctx_init.branch(None)
308
+ false_ctx = ctx_init.branch(0)
309
+
310
+ set_ctx(true_ctx)
311
+ for stmt in node.body:
312
+ self.visit(stmt)
313
+ true_end_ctx = ctx()
314
+
315
+ set_ctx(false_ctx)
316
+ for stmt in node.orelse:
317
+ self.visit(stmt)
318
+ false_end_ctx = ctx()
319
+
320
+ set_ctx(Context.meet([true_end_ctx, false_end_ctx]))
321
+
322
+ def visit_With(self, node):
323
+ raise NotImplementedError("With statements are not supported")
324
+
325
+ def visit_AsyncWith(self, node):
326
+ raise NotImplementedError("Async with statements are not supported")
327
+
328
+ def visit_Match(self, node):
329
+ subject = self.visit(node.subject)
330
+ end_ctxs = []
331
+ for case in node.cases:
332
+ if not ctx().live:
333
+ break
334
+ true_ctx, false_ctx = self.handle_match_pattern(subject, case.pattern)
335
+ if not true_ctx.live:
336
+ set_ctx(false_ctx)
337
+ continue
338
+ set_ctx(true_ctx)
339
+ guard = self.ensure_boolean_num(self.visit(case.guard)) if case.guard else validate_value(True)
340
+ if guard._is_py_():
341
+ if guard._as_py_():
342
+ for stmt in case.body:
343
+ self.visit(stmt)
344
+ end_ctxs.append(ctx())
345
+ else:
346
+ end_ctxs.append(ctx())
347
+ else:
348
+ ctx().test = guard.ir()
349
+ true_ctx = ctx().branch(None)
350
+ false_ctx = ctx().branch(0)
351
+ set_ctx(true_ctx)
352
+ for stmt in case.body:
353
+ self.visit(stmt)
354
+ end_ctxs.append(ctx())
355
+ set_ctx(false_ctx)
356
+ if end_ctxs:
357
+ set_ctx(Context.meet(end_ctxs))
358
+
359
+ def handle_match_pattern(self, subject: Value, pattern: ast.pattern) -> tuple[Context, Context]:
360
+ match pattern:
361
+ case ast.MatchValue(value=value):
362
+ value = self.visit(value)
363
+ test = self.ensure_boolean_num(subject == value)
364
+ ctx_init = ctx()
365
+ ctx_init.test = test.ir()
366
+ true_ctx = ctx_init.branch(None)
367
+ false_ctx = ctx_init.branch(0)
368
+ return true_ctx, false_ctx
369
+ case ast.MatchSingleton(value=value):
370
+ match value:
371
+ case True:
372
+ test = self.ensure_boolean_num(subject)
373
+ case False:
374
+ test = self.ensure_boolean_num(subject).not_()
375
+ case None:
376
+ test = Num._accept_(subject._is_py_() and subject._as_py_() is None)
377
+ case _:
378
+ raise NotImplementedError("Unsupported match singleton")
379
+ ctx_init = ctx()
380
+ ctx_init.test = test.ir()
381
+ true_ctx = ctx_init.branch(None)
382
+ false_ctx = ctx_init.branch(0)
383
+ return true_ctx, false_ctx
384
+ case ast.MatchSequence():
385
+ raise NotImplementedError("Match sequences are not supported")
386
+ case ast.MatchMapping():
387
+ raise NotImplementedError("Match mappings are not supported")
388
+ case ast.MatchClass(cls=cls, patterns=patterns, kwd_attrs=kwd_attrs, kwd_patterns=kwd_patterns):
389
+ from sonolus.script.comptime import Comptime
390
+ from sonolus.script.internal.generic import validate_type_spec
391
+
392
+ cls = validate_type_spec(self.visit(cls))
393
+ if not isinstance(cls, type):
394
+ raise TypeError("Class is not a type")
395
+ if issubclass(cls, Comptime):
396
+ raise TypeError("Comptime is not supported in match patterns")
397
+ if not isinstance(subject, cls):
398
+ return ctx().into_dead(), ctx()
399
+ if patterns:
400
+ if not hasattr(cls, "__match_args__"):
401
+ raise TypeError("Class does not support match patterns")
402
+ if len(cls.__match_args__) < len(patterns):
403
+ raise ValueError("Too many match patterns")
404
+ # kwd_attrs can't be mixed with patterns on the syntax level,
405
+ # so we can just set it like this since it's empty
406
+ kwd_attrs = cls.__match_args__[: len(patterns)]
407
+ kwd_patterns = patterns
408
+ if kwd_attrs:
409
+ true_ctx = ctx()
410
+ false_ctxs = []
411
+ for attr, subpattern in zip(kwd_attrs, kwd_patterns, strict=False):
412
+ if not hasattr(subject, attr):
413
+ raise AttributeError(f"Object has no attribute {attr}")
414
+ value = self.handle_getattr(subpattern, subject, attr)
415
+ true_ctx, false_ctx = self.handle_match_pattern(value, subpattern)
416
+ false_ctxs.append(false_ctx)
417
+ set_ctx(true_ctx)
418
+ return true_ctx, Context.meet(false_ctxs)
419
+ return ctx(), ctx().into_dead()
420
+ case ast.MatchStar():
421
+ raise NotImplementedError("Match stars are not supported")
422
+ case ast.MatchAs(pattern=pattern, name=name):
423
+ if pattern:
424
+ true_ctx, false_ctx = self.handle_match_pattern(subject, pattern)
425
+ if name:
426
+ true_ctx.scope.set_value(name, subject)
427
+ return true_ctx, false_ctx
428
+ else:
429
+ if name:
430
+ ctx().scope.set_value(name, subject)
431
+ return ctx(), ctx().into_dead()
432
+ case ast.MatchOr():
433
+ true_ctxs = []
434
+ false_ctx = ctx()
435
+ assert pattern.patterns
436
+ for subpattern in pattern.patterns:
437
+ true_ctx, false_ctx = self.handle_match_pattern(subject, subpattern)
438
+ true_ctxs.append(true_ctx)
439
+ set_ctx(false_ctx)
440
+ return Context.meet(true_ctxs), false_ctx
441
+
442
+ def visit_Raise(self, node):
443
+ raise NotImplementedError("Raise statements are not supported")
444
+
445
+ def visit_Try(self, node):
446
+ raise NotImplementedError("Try statements are not supported")
447
+
448
+ def visit_TryStar(self, node):
449
+ raise NotImplementedError("Try* statements are not supported")
450
+
451
+ def visit_Assert(self, node):
452
+ self.handle_call(
453
+ node, assert_true, self.visit(node.test), self.visit(node.msg) if node.msg else validate_value(None)
454
+ )
455
+
456
+ def visit_Import(self, node):
457
+ raise NotImplementedError("Import statements are not supported")
458
+
459
+ def visit_ImportFrom(self, node):
460
+ raise NotImplementedError("Import statements are not supported")
461
+
462
+ def visit_Global(self, node):
463
+ raise NotImplementedError("Global statements are not supported")
464
+
465
+ def visit_Nonlocal(self, node):
466
+ raise NotImplementedError("Nonlocal statements are not supported")
467
+
468
+ def visit_Expr(self, node):
469
+ return self.visit(node.value)
470
+
471
+ def visit_Pass(self, node):
472
+ pass
473
+
474
+ def visit_Break(self, node):
475
+ self.break_ctxs[-1].append(ctx())
476
+ set_ctx(ctx().into_dead())
477
+
478
+ def visit_Continue(self, node):
479
+ ctx().branch_to_loop_header(self.loop_head_ctxs[-1])
480
+ set_ctx(ctx().into_dead())
481
+
482
+ def visit_BoolOp(self, node) -> Value:
483
+ match node.op:
484
+ case ast.And():
485
+ handler = self.handle_and
486
+ case ast.Or():
487
+ handler = self.handle_or
488
+ case _:
489
+ raise NotImplementedError(f"Unsupported bool operator {node.op}")
490
+
491
+ if not node.values:
492
+ raise ValueError("Bool operator requires at least one operand")
493
+ if len(node.values) == 1:
494
+ return self.visit(node.values[0])
495
+ initial, *rest = node.values
496
+ return handler(self.visit(initial), ast.copy_location(ast.BoolOp(op=node.op, values=rest), node))
497
+
498
+ def visit_NamedExpr(self, node):
499
+ value = self.visit(node.value)
500
+ self.handle_assign(node.target, value)
501
+ return value
502
+
503
+ def visit_BinOp(self, node):
504
+ lhs = self.visit(node.left)
505
+ rhs = self.visit(node.right)
506
+ op = bin_ops[type(node.op)]
507
+ if hasattr(lhs, op):
508
+ result = self.handle_call(node, getattr(lhs, op), rhs)
509
+ if not self.is_not_implemented(result):
510
+ return result
511
+ if hasattr(rhs, rbin_ops[type(node.op)]):
512
+ result = self.handle_call(node, getattr(rhs, rbin_ops[type(node.op)]), lhs)
513
+ if not self.is_not_implemented(result):
514
+ return result
515
+ raise NotImplementedError(f"Unsupported operand types for binary operator {node.op}")
516
+
517
+ def visit_UnaryOp(self, node):
518
+ operand = self.visit(node.operand)
519
+ if isinstance(node.op, ast.Not):
520
+ return self.ensure_boolean_num(operand).not_()
521
+ op = unary_ops[type(node.op)]
522
+ if hasattr(operand, op):
523
+ return self.handle_call(node, getattr(operand, op))
524
+ raise NotImplementedError(f"Unsupported operand type for unary operator {node.op}")
525
+
526
+ def visit_Lambda(self, node):
527
+ raise NotImplementedError("Lambda functions are not supported")
528
+
529
+ def visit_IfExp(self, node):
530
+ test = self.ensure_boolean_num(self.visit(node.test))
531
+
532
+ if test._is_py_():
533
+ if test._as_py_():
534
+ return self.visit(node.body)
535
+ else:
536
+ return self.visit(node.orelse)
537
+
538
+ res_name = self.new_name("ifexp")
539
+ ctx_init = ctx()
540
+ ctx_init.test = test.ir()
541
+
542
+ set_ctx(ctx_init.branch(None))
543
+ true_value = self.visit(node.body)
544
+ ctx().scope.set_value(res_name, true_value)
545
+ ctx_true = ctx()
546
+
547
+ set_ctx(ctx_init.branch(0))
548
+ false_value = self.visit(node.orelse)
549
+ ctx().scope.set_value(res_name, false_value)
550
+ ctx_false = ctx()
551
+
552
+ set_ctx(Context.meet([ctx_true, ctx_false]))
553
+ return ctx().scope.get_value(res_name)
554
+
555
+ def visit_Dict(self, node):
556
+ return validate_value({self.visit(k): self.visit(v) for k, v in zip(node.keys, node.values, strict=True)})
557
+
558
+ def visit_Set(self, node):
559
+ raise NotImplementedError("Set literals are not supported")
560
+
561
+ def visit_ListComp(self, node):
562
+ raise NotImplementedError("List comprehensions are not supported")
563
+
564
+ def visit_SetComp(self, node):
565
+ raise NotImplementedError("Set comprehensions are not supported")
566
+
567
+ def visit_DictComp(self, node):
568
+ raise NotImplementedError("Dict comprehensions are not supported")
569
+
570
+ def visit_GeneratorExp(self, node):
571
+ raise NotImplementedError("Generator expressions are not supported")
572
+
573
+ def visit_Await(self, node):
574
+ raise NotImplementedError("Await expressions are not supported")
575
+
576
+ def visit_Yield(self, node):
577
+ raise NotImplementedError("Yield expressions are not supported")
578
+
579
+ def visit_YieldFrom(self, node):
580
+ raise NotImplementedError("Yield from expressions are not supported")
581
+
582
+ def visit_Compare(self, node):
583
+ result_name = self.new_name("compare")
584
+ ctx().scope.set_value(result_name, Num._accept_(0))
585
+ l_val = self.visit(node.left)
586
+ false_ctxs = []
587
+ for i, (op, rhs) in enumerate(zip(node.ops, node.comparators, strict=True)):
588
+ r_val = self.visit(rhs)
589
+ inverted = isinstance(op, ast.NotIn)
590
+ result = None
591
+ if isinstance(op, ast.Is | ast.IsNot):
592
+ if not (r_val._is_py_() and r_val._as_py_() is None):
593
+ raise TypeError("The right operand of 'is' must be None")
594
+ if isinstance(op, ast.Is):
595
+ result = Num._accept_(l_val._is_py_() and l_val._as_py_() is None)
596
+ else:
597
+ result = Num._accept_(not (l_val._is_py_() and l_val._as_py_() is None))
598
+ elif type(op) in comp_ops and hasattr(l_val, comp_ops[type(op)]):
599
+ result = self.handle_call(node, getattr(l_val, comp_ops[type(op)]), r_val)
600
+ if (
601
+ (result is None or self.is_not_implemented(result))
602
+ and type(op) in rcomp_ops
603
+ and hasattr(r_val, rcomp_ops[type(op)])
604
+ ):
605
+ result = self.handle_call(node, getattr(r_val, rcomp_ops[type(op)]), l_val)
606
+ if result is None or self.is_not_implemented(result):
607
+ raise NotImplementedError(f"Unsupported comparison operator {op}")
608
+ result = self.ensure_boolean_num(result)
609
+ if inverted:
610
+ result = result.not_()
611
+ curr_ctx = ctx()
612
+ if i == len(node.ops) - 1:
613
+ curr_ctx.scope.set_value(result_name, result)
614
+ else:
615
+ curr_ctx.test = result.ir()
616
+ true_ctx = curr_ctx.branch(None)
617
+ false_ctx = curr_ctx.branch(0)
618
+ false_ctxs.append(false_ctx)
619
+ set_ctx(true_ctx)
620
+ l_val = r_val
621
+ last_ctx = ctx() # This is the result of the last comparison returning true
622
+ set_ctx(Context.meet([last_ctx, *false_ctxs]))
623
+ return ctx().scope.get_value(result_name)
624
+
625
+ def visit_Call(self, node):
626
+ fn = self.visit(node.func)
627
+ if fn is Num:
628
+ raise ValueError("Calling int/bool/float is not supported")
629
+ args = []
630
+ kwargs = {}
631
+ for arg in node.args:
632
+ if isinstance(arg, ast.Starred):
633
+ args.extend(self.handle_starred(self.visit(arg.value)))
634
+ else:
635
+ args.append(self.visit(arg))
636
+ for keyword in node.keywords:
637
+ if keyword.arg:
638
+ kwargs[keyword.arg] = self.visit(keyword.value)
639
+ else:
640
+ value = self.visit(keyword.value)
641
+ if value._is_py_() and isinstance(value._as_py_(), Mapping):
642
+ kwargs.update(value._as_py_())
643
+ else:
644
+ raise ValueError("Starred keyword arguments (**kwargs) must be dictionaries")
645
+ return self.handle_call(node, fn, *args, **kwargs)
646
+
647
+ def visit_FormattedValue(self, node):
648
+ raise NotImplementedError("F-strings are not supported")
649
+
650
+ def visit_JoinedStr(self, node):
651
+ raise NotImplementedError("F-strings are not supported")
652
+
653
+ def visit_Constant(self, node):
654
+ return validate_value(node.value)
655
+
656
+ def visit_Attribute(self, node):
657
+ return self.handle_getattr(node, self.visit(node.value), node.attr)
658
+
659
+ def visit_Subscript(self, node):
660
+ value = self.visit(node.value)
661
+ slice_value = self.visit(node.slice)
662
+ return self.handle_getitem(node, value, slice_value)
663
+
664
+ def visit_Starred(self, node):
665
+ raise NotImplementedError("Starred expressions are not supported")
666
+
667
+ def visit_Name(self, node):
668
+ if isinstance(ctx().scope.get_binding(node.id), EmptyBinding) and node.id in self.globals:
669
+ # globals can have false positives due to limitations of inspect.closurevars
670
+ # so we need to check that it's not defined as a local variable
671
+ return self.globals[node.id]
672
+ return ctx().scope.get_value(node.id)
673
+
674
+ def visit_List(self, node):
675
+ raise NotImplementedError("List literals are not supported")
676
+
677
+ def visit_Tuple(self, node):
678
+ values = []
679
+ for elt in node.elts:
680
+ if isinstance(elt, ast.Starred):
681
+ values.extend(self.handle_starred(self.visit(elt.value)))
682
+ else:
683
+ values.append(self.visit(elt))
684
+ return validate_value(tuple(values))
685
+
686
+ def visit_Slice(self, node):
687
+ raise NotImplementedError("Slices are not supported")
688
+
689
+ def handle_assign(self, target: ast.stmt | ast.expr, value: Value):
690
+ match target:
691
+ case ast.Name(id=name):
692
+ ctx().scope.set_value(name, value)
693
+ case ast.Attribute(value=attr_value, attr=attr):
694
+ attr_value = self.visit(attr_value)
695
+ self.handle_setattr(target, attr_value, attr, value)
696
+ case ast.Subscript(value=sub_value, slice=slice_expr):
697
+ sub_value = self.visit(sub_value)
698
+ slice_value = self.visit(slice_expr)
699
+ self.handle_setitem(target, sub_value, slice_value, value)
700
+ case ast.Tuple(elts=elts) | ast.List(elts=elts):
701
+ values = self.handle_starred(value)
702
+ if len(elts) != len(values):
703
+ raise ValueError("Unpacking assignment requires the same number of elements")
704
+ for elt, v in zip(elts, values, strict=False):
705
+ self.handle_assign(elt, validate_value(v))
706
+ case ast.Starred():
707
+ raise NotImplementedError("Starred assignment is not supported")
708
+ case _:
709
+ raise NotImplementedError("Unsupported assignment target")
710
+
711
+ def handle_and(self, l_val: Value, r_expr: ast.expr) -> Value:
712
+ ctx_init = ctx()
713
+ l_val = self.ensure_boolean_num(l_val)
714
+
715
+ if l_val._is_py_() and not l_val._as_py_():
716
+ return l_val
717
+
718
+ ctx_init.test = l_val.ir()
719
+ res_name = self.new_name("and")
720
+
721
+ set_ctx(ctx_init.branch(None))
722
+ r_val = self.ensure_boolean_num(self.visit(r_expr))
723
+ ctx().scope.set_value(res_name, r_val)
724
+ ctx_true = ctx()
725
+
726
+ set_ctx(ctx_init.branch(0))
727
+ ctx().scope.set_value(res_name, Num._accept_(0))
728
+ ctx_false = ctx()
729
+
730
+ set_ctx(Context.meet([ctx_true, ctx_false]))
731
+ if l_val._is_py_() and r_val._is_py_():
732
+ return Num._accept_(l_val._as_py_() and r_val._as_py_())
733
+ return ctx().scope.get_value(res_name)
734
+
735
+ def handle_or(self, l_val: Value, r_expr: ast.expr) -> Value:
736
+ ctx_init = ctx()
737
+ l_val = self.ensure_boolean_num(l_val)
738
+
739
+ if l_val._is_py_() and l_val._as_py_():
740
+ return l_val
741
+
742
+ ctx_init.test = l_val.ir()
743
+ res_name = self.new_name("or")
744
+
745
+ set_ctx(ctx_init.branch(None))
746
+ ctx().scope.set_value(res_name, l_val)
747
+ ctx_true = ctx()
748
+
749
+ set_ctx(ctx_init.branch(0))
750
+ r_val = self.ensure_boolean_num(self.visit(r_expr))
751
+ ctx().scope.set_value(res_name, r_val)
752
+ ctx_false = ctx()
753
+
754
+ set_ctx(Context.meet([ctx_true, ctx_false]))
755
+ if l_val._is_py_() and r_val._is_py_():
756
+ return Num._accept_(l_val._as_py_() or r_val._as_py_())
757
+ return ctx().scope.get_value(res_name)
758
+
759
+ def generic_visit(self, node):
760
+ if isinstance(node, ast.stmt | ast.expr):
761
+ with self.reporting_errors_at_node(node):
762
+ raise NotImplementedError(f"Unsupported syntax: {type(node).__name__}")
763
+ raise NotImplementedError(f"Unsupported syntax: {type(node).__name__}")
764
+
765
+ def handle_getattr(self, node: ast.stmt | ast.expr, target: Value, key: str) -> Value:
766
+ with self.reporting_errors_at_node(node):
767
+ if target._is_py_():
768
+ target = target._as_py_()
769
+ descriptor = type(target).__dict__.get(key)
770
+ match descriptor:
771
+ case property(fget=getter):
772
+ return self.handle_call(node, getter, target)
773
+ case SonolusDescriptor() | FunctionType() | classmethod() | staticmethod() | None:
774
+ return validate_value(getattr(target, key))
775
+ case non_descriptor if not hasattr(non_descriptor, "__get__"):
776
+ return validate_value(getattr(target, key))
777
+ case _:
778
+ raise TypeError(f"Unsupported field or descriptor {key}")
779
+
780
+ def handle_setattr(self, node: ast.stmt | ast.expr, target: Value, key: str, value: Value):
781
+ with self.reporting_errors_at_node(node):
782
+ if target._is_py_():
783
+ target = target._as_py_()
784
+ descriptor = getattr(type(target), key, None)
785
+ match descriptor:
786
+ case property(fset=setter):
787
+ if setter is None:
788
+ raise AttributeError(f"Cannot set attribute {key} because property has no setter")
789
+ self.handle_call(node, setter, target, value)
790
+ case SonolusDescriptor():
791
+ setattr(target, key, value)
792
+ case _:
793
+ raise TypeError(f"Unsupported field or descriptor {key}")
794
+
795
+ def handle_call[**P, R](
796
+ self, node: ast.stmt | ast.expr, fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs
797
+ ) -> R:
798
+ """Handles a call to the given callable."""
799
+ if (
800
+ isinstance(fn, Value)
801
+ and fn._is_py_()
802
+ and isinstance(fn._as_py_(), type)
803
+ and issubclass(fn._as_py_(), Value)
804
+ ):
805
+ return validate_value(self.execute_at_node(node, fn._as_py_(), *args, **kwargs))
806
+ else:
807
+ return self.execute_at_node(node, lambda: validate_value(compile_and_call(fn, *args, **kwargs)))
808
+
809
+ def handle_getitem(self, node: ast.stmt | ast.expr, target: Value, key: Value) -> Value:
810
+ with self.reporting_errors_at_node(node):
811
+ if target._is_py_():
812
+ target = target._as_py_()
813
+ if key._is_py_():
814
+ return validate_value(target[key._as_py_()])
815
+ if isinstance(target, Value) and hasattr(target, "__getitem__"):
816
+ return self.handle_call(node, target.__getitem__, key)
817
+ raise TypeError(f"Cannot get items on {type(target).__name__}")
818
+ else:
819
+ if isinstance(target, Value) and hasattr(target, "__getitem__"):
820
+ return self.handle_call(node, target.__getitem__, key)
821
+ raise TypeError(f"Cannot get items on {type(target).__name__}")
822
+
823
+ def handle_setitem(self, node: ast.stmt | ast.expr, target: Value, key: Value, value: Value):
824
+ with self.reporting_errors_at_node(node):
825
+ if target._is_py_():
826
+ target = target._as_py_()
827
+ if key._is_py_():
828
+ target[key._as_py_()] = value._as_py_()
829
+ if isinstance(target, Value) and hasattr(target, "__setitem__"):
830
+ return self.handle_call(node, target.__setitem__, key, value)
831
+ raise TypeError(f"Cannot set items on {type(target).__name__}")
832
+ else:
833
+ if isinstance(target, Value) and hasattr(target, "__setitem__"):
834
+ return self.handle_call(node, target.__setitem__, key, value)
835
+ raise TypeError(f"Cannot set items on {type(target).__name__}")
836
+
837
+ def handle_starred(self, value: Value) -> tuple[Value, ...]:
838
+ if value._is_py_() and isinstance(value._as_py_(), tuple):
839
+ return value._as_py_()
840
+ raise ValueError("Unsupported starred expression")
841
+
842
+ def is_not_implemented(self, value):
843
+ value = validate_value(value)
844
+ return value._is_py_() and value._as_py_() is NotImplemented
845
+
846
+ def ensure_boolean_num(self, value) -> Num:
847
+ # This just checks the type for now, although we could support custom __bool__ implementations in the future
848
+ if not is_num(value):
849
+ raise TypeError(f"Invalid type where a bool (Num) was expected: {type(value).__name__}")
850
+ return value
851
+
852
+ def raise_exception_at_node(self, node: ast.stmt | ast.expr, cause: Exception) -> Never:
853
+ """Throws a compilation error at the given node."""
854
+
855
+ def thrower() -> Never:
856
+ raise CompilationError(str(cause)) from cause
857
+
858
+ self.execute_at_node(node, thrower)
859
+
860
+ def execute_at_node[**P, R](
861
+ self, node: ast.stmt | ast.expr, fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs
862
+ ) -> R:
863
+ """Executes the given function at the given node for a better traceback."""
864
+ expr = ast.Expression(
865
+ body=ast.Call(
866
+ func=ast.Name(id="fn", ctx=ast.Load()),
867
+ args=[ast.Starred(value=ast.Name(id="args", ctx=ast.Load()), ctx=ast.Load())],
868
+ keywords=[ast.keyword(value=ast.Name(id="kwargs", ctx=ast.Load()), arg=None)],
869
+ lineno=node.lineno,
870
+ col_offset=node.col_offset,
871
+ end_lineno=node.end_lineno,
872
+ end_col_offset=node.end_col_offset,
873
+ ),
874
+ )
875
+ expr = ast.fix_missing_locations(expr)
876
+ return eval(
877
+ compile(expr, filename=self.source_file, mode="eval"),
878
+ {"fn": fn, "args": args, "kwargs": kwargs, "_filter_traceback_": True},
879
+ )
880
+
881
+ @contextmanager
882
+ def reporting_errors_at_node(self, node: ast.stmt | ast.expr):
883
+ try:
884
+ yield
885
+ except CompilationError as e:
886
+ raise e from None
887
+ except Exception as e:
888
+ self.raise_exception_at_node(node, e)
889
+
890
+ def new_name(self, name: str):
891
+ self.used_names[name] = self.used_names.get(name, 0) + 1
892
+ return f"${name}_{self.used_names[name]}"