sonolus.py 0.1.4__py3-none-any.whl → 0.1.6__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 (79) hide show
  1. sonolus/backend/finalize.py +18 -10
  2. sonolus/backend/interpret.py +7 -7
  3. sonolus/backend/ir.py +24 -0
  4. sonolus/backend/optimize/__init__.py +0 -0
  5. sonolus/backend/{allocate.py → optimize/allocate.py} +4 -3
  6. sonolus/backend/{constant_evaluation.py → optimize/constant_evaluation.py} +7 -7
  7. sonolus/backend/{coalesce.py → optimize/copy_coalesce.py} +3 -3
  8. sonolus/backend/optimize/dead_code.py +185 -0
  9. sonolus/backend/{dominance.py → optimize/dominance.py} +2 -17
  10. sonolus/backend/{flow.py → optimize/flow.py} +6 -5
  11. sonolus/backend/{inlining.py → optimize/inlining.py} +4 -17
  12. sonolus/backend/{liveness.py → optimize/liveness.py} +69 -65
  13. sonolus/backend/optimize/optimize.py +44 -0
  14. sonolus/backend/{passes.py → optimize/passes.py} +1 -1
  15. sonolus/backend/optimize/simplify.py +191 -0
  16. sonolus/backend/{ssa.py → optimize/ssa.py} +31 -18
  17. sonolus/backend/place.py +17 -25
  18. sonolus/backend/utils.py +10 -0
  19. sonolus/backend/visitor.py +360 -101
  20. sonolus/build/cli.py +14 -3
  21. sonolus/build/compile.py +8 -8
  22. sonolus/build/engine.py +10 -5
  23. sonolus/build/project.py +30 -1
  24. sonolus/script/archetype.py +429 -138
  25. sonolus/script/array.py +25 -8
  26. sonolus/script/array_like.py +297 -0
  27. sonolus/script/bucket.py +73 -11
  28. sonolus/script/containers.py +234 -51
  29. sonolus/script/debug.py +8 -8
  30. sonolus/script/easing.py +147 -105
  31. sonolus/script/effect.py +60 -0
  32. sonolus/script/engine.py +71 -4
  33. sonolus/script/globals.py +66 -32
  34. sonolus/script/instruction.py +79 -25
  35. sonolus/script/internal/builtin_impls.py +138 -27
  36. sonolus/script/internal/constant.py +139 -0
  37. sonolus/script/internal/context.py +14 -5
  38. sonolus/script/internal/dict_impl.py +65 -0
  39. sonolus/script/internal/generic.py +6 -9
  40. sonolus/script/internal/impl.py +38 -13
  41. sonolus/script/internal/introspection.py +5 -2
  42. sonolus/script/{math.py → internal/math_impls.py} +28 -28
  43. sonolus/script/internal/native.py +3 -3
  44. sonolus/script/internal/random.py +67 -0
  45. sonolus/script/internal/range.py +81 -0
  46. sonolus/script/internal/transient.py +51 -0
  47. sonolus/script/internal/tuple_impl.py +113 -0
  48. sonolus/script/interval.py +234 -16
  49. sonolus/script/iterator.py +120 -167
  50. sonolus/script/level.py +24 -0
  51. sonolus/script/num.py +79 -47
  52. sonolus/script/options.py +78 -12
  53. sonolus/script/particle.py +37 -4
  54. sonolus/script/pointer.py +4 -4
  55. sonolus/script/print.py +22 -1
  56. sonolus/script/project.py +59 -0
  57. sonolus/script/{graphics.py → quad.py} +75 -12
  58. sonolus/script/record.py +44 -13
  59. sonolus/script/runtime.py +50 -1
  60. sonolus/script/sprite.py +198 -115
  61. sonolus/script/text.py +2 -0
  62. sonolus/script/timing.py +72 -0
  63. sonolus/script/transform.py +296 -66
  64. sonolus/script/ui.py +134 -78
  65. sonolus/script/values.py +6 -13
  66. sonolus/script/vec.py +118 -3
  67. {sonolus_py-0.1.4.dist-info → sonolus_py-0.1.6.dist-info}/METADATA +1 -1
  68. sonolus_py-0.1.6.dist-info/RECORD +89 -0
  69. sonolus/backend/dead_code.py +0 -80
  70. sonolus/backend/optimize.py +0 -37
  71. sonolus/backend/simplify.py +0 -47
  72. sonolus/script/comptime.py +0 -160
  73. sonolus/script/random.py +0 -14
  74. sonolus/script/range.py +0 -58
  75. sonolus_py-0.1.4.dist-info/RECORD +0 -84
  76. /sonolus/script/{callbacks.py → internal/callbacks.py} +0 -0
  77. {sonolus_py-0.1.4.dist-info → sonolus_py-0.1.6.dist-info}/WHEEL +0 -0
  78. {sonolus_py-0.1.4.dist-info → sonolus_py-0.1.6.dist-info}/entry_points.txt +0 -0
  79. {sonolus_py-0.1.4.dist-info → sonolus_py-0.1.6.dist-info}/licenses/LICENSE +0 -0
@@ -1,23 +1,25 @@
1
1
  # ruff: noqa: N802
2
2
  import ast
3
+ import builtins
3
4
  import functools
4
5
  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
6
+ from collections.abc import Callable, Sequence
7
+ from types import FunctionType, MethodType, MethodWrapperType
8
+ from typing import Any, Never, Self
9
9
 
10
10
  from sonolus.backend.excepthook import install_excepthook
11
11
  from sonolus.backend.utils import get_function, scan_writes
12
12
  from sonolus.script.debug import assert_true
13
- from sonolus.script.internal.builtin_impls import BUILTIN_IMPLS
13
+ from sonolus.script.internal.builtin_impls import BUILTIN_IMPLS, _bool, _float, _int, _len
14
+ from sonolus.script.internal.constant import ConstantValue
14
15
  from sonolus.script.internal.context import Context, EmptyBinding, Scope, ValueBinding, ctx, set_ctx
15
16
  from sonolus.script.internal.descriptor import SonolusDescriptor
16
17
  from sonolus.script.internal.error import CompilationError
17
- from sonolus.script.internal.impl import try_validate_value, validate_value
18
+ from sonolus.script.internal.impl import validate_value
19
+ from sonolus.script.internal.tuple_impl import TupleImpl
18
20
  from sonolus.script.internal.value import Value
19
21
  from sonolus.script.iterator import SonolusIterator
20
- from sonolus.script.num import Num, is_num
22
+ from sonolus.script.num import Num, _is_num
21
23
 
22
24
  _compiler_internal_ = True
23
25
 
@@ -31,7 +33,7 @@ def compile_and_call[**P, R](fn: Callable[P, R], /, *args: P.args, **kwargs: P.k
31
33
  def generate_fn_impl(fn: Callable):
32
34
  install_excepthook()
33
35
  match fn:
34
- case Value() as value if value._is_py_():
36
+ case ConstantValue() as value if value._is_py_():
35
37
  return generate_fn_impl(value._as_py_())
36
38
  case MethodType() as method:
37
39
  return functools.partial(generate_fn_impl(method.__func__), method.__self__)
@@ -42,20 +44,21 @@ def generate_fn_impl(fn: Callable):
42
44
  case _:
43
45
  if callable(fn) and isinstance(fn, Value):
44
46
  return generate_fn_impl(fn.__call__)
45
- elif fn is type:
46
- return fn
47
47
  elif callable(fn):
48
48
  raise TypeError(f"Unsupported callable {fn!r}")
49
49
  else:
50
- raise TypeError(f"Not callable {fn!r}")
50
+ raise TypeError(f"'{type(fn).__name__}' object is not callable")
51
51
 
52
52
 
53
53
  def eval_fn(fn: Callable, /, *args, **kwargs):
54
54
  source_file, node = get_function(fn)
55
55
  bound_args = inspect.signature(fn).bind(*args, **kwargs)
56
56
  bound_args.apply_defaults()
57
- closurevars = inspect.getclosurevars(fn)
58
- global_vars = {**closurevars.nonlocals, **closurevars.globals, **closurevars.builtins}
57
+ global_vars = {
58
+ **builtins.__dict__,
59
+ **fn.__globals__,
60
+ **inspect.getclosurevars(fn).nonlocals,
61
+ }
59
62
  return Visitor(source_file, bound_args, global_vars).run(node)
60
63
 
61
64
 
@@ -123,16 +126,45 @@ comp_ops = {
123
126
  }
124
127
 
125
128
  rcomp_ops = {
126
- ast.Eq: "__req__",
127
- ast.NotEq: "__rne__",
129
+ ast.Eq: "__eq__",
130
+ ast.NotEq: "__ne__",
128
131
  ast.Lt: "__gt__",
129
132
  ast.LtE: "__ge__",
130
133
  ast.Gt: "__lt__",
131
134
  ast.GtE: "__le__",
132
- ast.In: "__contains__",
135
+ ast.In: "__contains__", # Only supported on the right side
133
136
  ast.NotIn: "__contains__",
134
137
  }
135
138
 
139
+ op_to_symbol = {
140
+ ast.Add: "+",
141
+ ast.Sub: "-",
142
+ ast.Mult: "*",
143
+ ast.Div: "/",
144
+ ast.FloorDiv: "//",
145
+ ast.Mod: "%",
146
+ ast.Pow: "**",
147
+ ast.Eq: "==",
148
+ ast.NotEq: "!=",
149
+ ast.Lt: "<",
150
+ ast.LtE: "<=",
151
+ ast.Gt: ">",
152
+ ast.GtE: ">=",
153
+ ast.And: "and",
154
+ ast.Or: "or",
155
+ ast.BitAnd: "&",
156
+ ast.BitOr: "|",
157
+ ast.BitXor: "^",
158
+ ast.LShift: "<<",
159
+ ast.RShift: ">>",
160
+ ast.USub: "-",
161
+ ast.UAdd: "+",
162
+ ast.Invert: "~",
163
+ ast.Not: "not",
164
+ ast.In: "in",
165
+ ast.NotIn: "not in",
166
+ }
167
+
136
168
 
137
169
  class Visitor(ast.NodeVisitor):
138
170
  source_file: str
@@ -142,22 +174,25 @@ class Visitor(ast.NodeVisitor):
142
174
  return_ctxs: list[Context] # Contexts at return statements, which will branch to the exit
143
175
  loop_head_ctxs: list[Context] # Contexts at loop heads, from outer to inner
144
176
  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]):
177
+ active_ctx: Context | None # The active context for use in nested functions=
178
+ parent: Self | None # The parent visitor for use in nested functions
179
+
180
+ def __init__(
181
+ self,
182
+ source_file: str,
183
+ bound_args: inspect.BoundArguments,
184
+ global_vars: dict[str, Any],
185
+ parent: Self | None = None,
186
+ ):
147
187
  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
188
+ self.globals = global_vars
156
189
  self.bound_args = bound_args
157
190
  self.used_names = {}
158
191
  self.return_ctxs = []
159
192
  self.loop_head_ctxs = []
160
193
  self.break_ctxs = []
194
+ self.active_ctx = None
195
+ self.parent = parent
161
196
 
162
197
  def run(self, node):
163
198
  before_ctx = ctx()
@@ -169,17 +204,49 @@ class Visitor(ast.NodeVisitor):
169
204
  ctx().scope.set_value("$return", validate_value(None))
170
205
  for stmt in body:
171
206
  self.visit(stmt)
207
+ case ast.Lambda(body=body):
208
+ result = self.visit(body)
209
+ ctx().scope.set_value("$return", result)
172
210
  case _:
173
211
  raise NotImplementedError("Unsupported syntax")
174
212
  after_ctx = Context.meet([*self.return_ctxs, ctx()])
213
+ self.active_ctx = after_ctx
175
214
  result_binding = after_ctx.scope.get_binding("$return")
176
215
  if not isinstance(result_binding, ValueBinding):
177
216
  raise ValueError("Function has conflicting return values")
178
217
  set_ctx(after_ctx.branch_with_scope(None, before_ctx.scope.copy()))
179
218
  return result_binding.value
180
219
 
220
+ def visit(self, node):
221
+ """Visit a node."""
222
+ # We want this here so this is filtered out of tracebacks
223
+ method = "visit_" + node.__class__.__name__
224
+ visitor = getattr(self, method, self.generic_visit)
225
+ with self.reporting_errors_at_node(node):
226
+ return visitor(node)
227
+
181
228
  def visit_FunctionDef(self, node):
182
- raise NotImplementedError("Nested functions are not supported")
229
+ name = node.name
230
+ signature = self.arguments_to_signature(node.args)
231
+
232
+ def fn(*args, **kwargs):
233
+ bound = signature.bind(*args, **kwargs)
234
+ bound.apply_defaults()
235
+ return Visitor(
236
+ self.source_file,
237
+ bound,
238
+ self.globals,
239
+ self,
240
+ ).run(node)
241
+
242
+ fn._meta_fn_ = True
243
+ fn.__name__ = name
244
+ fn.__qualname__ = name
245
+
246
+ for decorator in reversed(node.decorator_list):
247
+ fn = self.handle_call(decorator, self.visit(decorator), fn)
248
+
249
+ ctx().scope.set_value(name, validate_value(fn))
183
250
 
184
251
  def visit_AsyncFunctionDef(self, node):
185
252
  raise NotImplementedError("Async functions are not supported")
@@ -194,7 +261,16 @@ class Visitor(ast.NodeVisitor):
194
261
  set_ctx(ctx().into_dead())
195
262
 
196
263
  def visit_Delete(self, node):
197
- raise NotImplementedError("Delete statements are not supported")
264
+ for target in node.targets:
265
+ match target:
266
+ case ast.Name():
267
+ raise NotImplementedError("Deleting variables is not supported")
268
+ case ast.Subscript(value=value, slice=slice):
269
+ self.handle_delitem(target, self.visit(value), self.visit(slice))
270
+ case ast.Attribute():
271
+ raise NotImplementedError("Deleting attributes is not supported")
272
+ case _:
273
+ raise NotImplementedError("Unsupported delete target")
198
274
 
199
275
  def visit_Assign(self, node):
200
276
  value = self.visit(node.value)
@@ -215,27 +291,40 @@ class Visitor(ast.NodeVisitor):
215
291
  if not self.is_not_implemented(result):
216
292
  if result is not lhs_value:
217
293
  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
294
+ self.handle_assign(node.target, result)
220
295
  return
221
296
  if hasattr(lhs_value, regular_fn_name):
222
297
  result = self.handle_call(node, getattr(lhs_value, regular_fn_name), rhs_value)
223
298
  if not self.is_not_implemented(result):
224
299
  self.handle_assign(node.target, result)
225
300
  return
226
- if hasattr(rhs_value, right_fn_name):
301
+ if hasattr(rhs_value, right_fn_name) and type(lhs_value) is not type(rhs_value):
227
302
  result = self.handle_call(node, getattr(rhs_value, right_fn_name), lhs_value)
228
303
  if not self.is_not_implemented(result):
229
304
  self.handle_assign(node.target, result)
230
305
  return
231
- raise NotImplementedError("Unsupported augmented assignment")
306
+ raise TypeError(
307
+ f"unsupported operand type(s) for {op_to_symbol[type(node.op)]}=: "
308
+ f"'{type(lhs_value).__name__}' and '{type(rhs_value).__name__}'"
309
+ )
232
310
 
233
311
  def visit_AnnAssign(self, node):
234
312
  value = self.visit(node.value)
235
313
  self.handle_assign(node.target, value)
236
314
 
237
315
  def visit_For(self, node):
238
- iterator = iter(self.visit(node.iter))
316
+ from sonolus.script.internal.tuple_impl import TupleImpl
317
+
318
+ iterable = self.visit(node.iter)
319
+ if isinstance(iterable, TupleImpl):
320
+ # Unroll the loop
321
+ for value in iterable.value:
322
+ set_ctx(ctx().branch(None))
323
+ self.handle_assign(node.target, validate_value(value))
324
+ for stmt in node.body:
325
+ self.visit(stmt)
326
+ return
327
+ iterator = self.handle_call(node, iterable.__iter__)
239
328
  if not isinstance(iterator, SonolusIterator):
240
329
  raise ValueError("Unsupported iterator")
241
330
  writes = scan_writes(node)
@@ -244,6 +333,11 @@ class Visitor(ast.NodeVisitor):
244
333
  self.break_ctxs.append([])
245
334
  set_ctx(header_ctx)
246
335
  has_next = self.ensure_boolean_num(self.handle_call(node, iterator.has_next))
336
+ if has_next._is_py_() and not has_next._as_py_():
337
+ # The loop will never run, continue after evaluating the condition
338
+ for stmt in node.orelse:
339
+ self.visit(stmt)
340
+ return
247
341
  ctx().test = has_next.ir()
248
342
  body_ctx = ctx().branch(None)
249
343
  else_ctx = ctx().branch(0)
@@ -271,6 +365,11 @@ class Visitor(ast.NodeVisitor):
271
365
  self.break_ctxs.append([])
272
366
  set_ctx(header_ctx)
273
367
  test = self.ensure_boolean_num(self.visit(node.test))
368
+ if test._is_py_() and not test._as_py_():
369
+ # The loop will never run, continue after evaluating the condition
370
+ for stmt in node.orelse:
371
+ self.visit(stmt)
372
+ return
274
373
  ctx().test = test.ir()
275
374
  body_ctx = ctx().branch(None)
276
375
  else_ctx = ctx().branch(0)
@@ -343,24 +442,38 @@ class Visitor(ast.NodeVisitor):
343
442
  self.visit(stmt)
344
443
  end_ctxs.append(ctx())
345
444
  else:
346
- end_ctxs.append(ctx())
445
+ # Merge failing before the guard and failing now at the guard (which we know is guaranteed to fail)
446
+ false_ctx = Context.meet([ctx(), false_ctx])
347
447
  else:
348
448
  ctx().test = guard.ir()
349
- true_ctx = ctx().branch(None)
350
- false_ctx = ctx().branch(0)
351
- set_ctx(true_ctx)
449
+ guard_true_ctx = ctx().branch(None)
450
+ guard_false_ctx = ctx().branch(0)
451
+ set_ctx(guard_true_ctx)
352
452
  for stmt in case.body:
353
453
  self.visit(stmt)
354
454
  end_ctxs.append(ctx())
455
+ false_ctx = Context.meet([false_ctx, guard_false_ctx])
355
456
  set_ctx(false_ctx)
457
+ end_ctxs.append(ctx())
356
458
  if end_ctxs:
357
459
  set_ctx(Context.meet(end_ctxs))
358
460
 
359
461
  def handle_match_pattern(self, subject: Value, pattern: ast.pattern) -> tuple[Context, Context]:
462
+ from sonolus.script.internal.generic import validate_type_spec
463
+ from sonolus.script.internal.tuple_impl import TupleImpl
464
+
465
+ if not ctx().live:
466
+ return ctx().into_dead(), ctx()
467
+
360
468
  match pattern:
361
469
  case ast.MatchValue(value=value):
362
470
  value = self.visit(value)
363
- test = self.ensure_boolean_num(subject == value)
471
+ test = self.ensure_boolean_num(validate_value(subject == value))
472
+ if test._is_py_():
473
+ if test._as_py_():
474
+ return ctx(), ctx().into_dead()
475
+ else:
476
+ return ctx().into_dead(), ctx()
364
477
  ctx_init = ctx()
365
478
  ctx_init.test = test.ir()
366
479
  true_ctx = ctx_init.branch(None)
@@ -369,11 +482,11 @@ class Visitor(ast.NodeVisitor):
369
482
  case ast.MatchSingleton(value=value):
370
483
  match value:
371
484
  case True:
372
- test = self.ensure_boolean_num(subject)
485
+ raise NotImplementedError("Matching against True is not supported, use 1 instead")
373
486
  case False:
374
- test = self.ensure_boolean_num(subject).not_()
487
+ raise NotImplementedError("Matching against False is not supported, use 0 instead")
375
488
  case None:
376
- test = Num._accept_(subject._is_py_() and subject._as_py_() is None)
489
+ test = validate_value(subject._is_py_() and subject._as_py_() is None)
377
490
  case _:
378
491
  raise NotImplementedError("Unsupported match singleton")
379
492
  ctx_init = ctx()
@@ -381,19 +494,39 @@ class Visitor(ast.NodeVisitor):
381
494
  true_ctx = ctx_init.branch(None)
382
495
  false_ctx = ctx_init.branch(0)
383
496
  return true_ctx, false_ctx
384
- case ast.MatchSequence():
385
- raise NotImplementedError("Match sequences are not supported")
497
+ case ast.MatchSequence(patterns=patterns):
498
+ target_len = len(patterns)
499
+ if not (isinstance(subject, Sequence | TupleImpl)):
500
+ return ctx().into_dead(), ctx()
501
+ length_test = self.ensure_boolean_num(validate_value(_len(subject) == target_len))
502
+ ctx_init = ctx()
503
+ if not length_test._is_py_():
504
+ ctx_init.test = length_test.ir()
505
+ true_ctx = ctx_init.branch(None)
506
+ false_ctxs = [ctx_init.branch(0)]
507
+ elif length_test._as_py_():
508
+ true_ctx = ctx_init
509
+ false_ctxs = []
510
+ else:
511
+ return ctx().into_dead(), ctx()
512
+ set_ctx(true_ctx)
513
+ for i, subpattern in enumerate(patterns):
514
+ if not ctx().live:
515
+ break
516
+ value = self.handle_getitem(subpattern, subject, validate_value(i))
517
+ true_ctx, false_ctx = self.handle_match_pattern(value, subpattern)
518
+ false_ctxs.append(false_ctx)
519
+ set_ctx(true_ctx)
520
+ return true_ctx, Context.meet(false_ctxs)
386
521
  case ast.MatchMapping():
387
522
  raise NotImplementedError("Match mappings are not supported")
388
523
  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))
524
+ cls = self.visit(cls)
525
+ if cls._is_py_() and cls._as_py_() in {_int, _float, _bool}:
526
+ raise TypeError("Instance check against int, float, or bool is not supported, use Num instead")
527
+ cls = validate_type_spec(cls)
393
528
  if not isinstance(cls, type):
394
529
  raise TypeError("Class is not a type")
395
- if issubclass(cls, Comptime):
396
- raise TypeError("Comptime is not supported in match patterns")
397
530
  if not isinstance(subject, cls):
398
531
  return ctx().into_dead(), ctx()
399
532
  if patterns:
@@ -431,13 +564,14 @@ class Visitor(ast.NodeVisitor):
431
564
  return ctx(), ctx().into_dead()
432
565
  case ast.MatchOr():
433
566
  true_ctxs = []
434
- false_ctx = ctx()
435
567
  assert pattern.patterns
436
568
  for subpattern in pattern.patterns:
569
+ if not ctx().live:
570
+ break
437
571
  true_ctx, false_ctx = self.handle_match_pattern(subject, subpattern)
438
572
  true_ctxs.append(true_ctx)
439
573
  set_ctx(false_ctx)
440
- return Context.meet(true_ctxs), false_ctx
574
+ return Context.meet(true_ctxs), ctx()
441
575
 
442
576
  def visit_Raise(self, node):
443
577
  raise NotImplementedError("Raise statements are not supported")
@@ -486,7 +620,7 @@ class Visitor(ast.NodeVisitor):
486
620
  case ast.Or():
487
621
  handler = self.handle_or
488
622
  case _:
489
- raise NotImplementedError(f"Unsupported bool operator {node.op}")
623
+ raise NotImplementedError(f"Unsupported bool operator {op_to_symbol[type(node.op)]}")
490
624
 
491
625
  if not node.values:
492
626
  raise ValueError("Bool operator requires at least one operand")
@@ -504,15 +638,23 @@ class Visitor(ast.NodeVisitor):
504
638
  lhs = self.visit(node.left)
505
639
  rhs = self.visit(node.right)
506
640
  op = bin_ops[type(node.op)]
641
+ if lhs._is_py_() and rhs._is_py_():
642
+ lhs_py = lhs._as_py_()
643
+ rhs_py = rhs._as_py_()
644
+ if isinstance(lhs_py, type) and isinstance(rhs_py, type):
645
+ return validate_value(getattr(lhs_py, op)(rhs_py))
507
646
  if hasattr(lhs, op):
508
647
  result = self.handle_call(node, getattr(lhs, op), rhs)
509
648
  if not self.is_not_implemented(result):
510
649
  return result
511
- if hasattr(rhs, rbin_ops[type(node.op)]):
650
+ if hasattr(rhs, rbin_ops[type(node.op)]) and type(lhs) is not type(rhs):
512
651
  result = self.handle_call(node, getattr(rhs, rbin_ops[type(node.op)]), lhs)
513
652
  if not self.is_not_implemented(result):
514
653
  return result
515
- raise NotImplementedError(f"Unsupported operand types for binary operator {node.op}")
654
+ raise TypeError(
655
+ f"unsupported operand type(s) for {op_to_symbol[type(node.op)]}: "
656
+ f"'{type(lhs).__name__}' and '{type(rhs).__name__}'"
657
+ )
516
658
 
517
659
  def visit_UnaryOp(self, node):
518
660
  operand = self.visit(node.operand)
@@ -521,10 +663,25 @@ class Visitor(ast.NodeVisitor):
521
663
  op = unary_ops[type(node.op)]
522
664
  if hasattr(operand, op):
523
665
  return self.handle_call(node, getattr(operand, op))
524
- raise NotImplementedError(f"Unsupported operand type for unary operator {node.op}")
666
+ raise TypeError(f"bad operand type for unary {op_to_symbol[type(node.op)]}: '{type(operand).__name__}'")
525
667
 
526
668
  def visit_Lambda(self, node):
527
- raise NotImplementedError("Lambda functions are not supported")
669
+ signature = self.arguments_to_signature(node.args)
670
+
671
+ def fn(*args, **kwargs):
672
+ bound = signature.bind(*args, **kwargs)
673
+ bound.apply_defaults()
674
+ return Visitor(
675
+ self.source_file,
676
+ bound,
677
+ self.globals,
678
+ self,
679
+ ).run(node)
680
+
681
+ fn._meta_fn_ = True
682
+ fn.__name__ = "<lambda>"
683
+
684
+ return validate_value(fn)
528
685
 
529
686
  def visit_IfExp(self, node):
530
687
  test = self.ensure_boolean_num(self.visit(node.test))
@@ -579,6 +736,9 @@ class Visitor(ast.NodeVisitor):
579
736
  def visit_YieldFrom(self, node):
580
737
  raise NotImplementedError("Yield from expressions are not supported")
581
738
 
739
+ def _has_real_method(self, obj: Value, method_name: str) -> bool:
740
+ return hasattr(obj, method_name) and not isinstance(getattr(obj, method_name), MethodWrapperType)
741
+
582
742
  def visit_Compare(self, node):
583
743
  result_name = self.new_name("compare")
584
744
  ctx().scope.set_value(result_name, Num._accept_(0))
@@ -595,22 +755,37 @@ class Visitor(ast.NodeVisitor):
595
755
  result = Num._accept_(l_val._is_py_() and l_val._as_py_() is None)
596
756
  else:
597
757
  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)]):
758
+ elif type(op) in comp_ops and self._has_real_method(l_val, comp_ops[type(op)]):
599
759
  result = self.handle_call(node, getattr(l_val, comp_ops[type(op)]), r_val)
600
760
  if (
601
761
  (result is None or self.is_not_implemented(result))
602
762
  and type(op) in rcomp_ops
603
- and hasattr(r_val, rcomp_ops[type(op)])
763
+ and self._has_real_method(r_val, rcomp_ops[type(op)])
604
764
  ):
605
765
  result = self.handle_call(node, getattr(r_val, rcomp_ops[type(op)]), l_val)
606
766
  if result is None or self.is_not_implemented(result):
607
- raise NotImplementedError(f"Unsupported comparison operator {op}")
767
+ if type(op) is ast.Eq:
768
+ result = Num._accept_(l_val is r_val)
769
+ elif type(op) is ast.NotEq:
770
+ result = Num._accept_(l_val is not r_val)
771
+ else:
772
+ raise TypeError(
773
+ f"'{op_to_symbol[type(op)]}' not supported between instances of '{type(l_val).__name__}' and "
774
+ f"'{type(r_val).__name__}'"
775
+ )
608
776
  result = self.ensure_boolean_num(result)
609
777
  if inverted:
610
778
  result = result.not_()
611
779
  curr_ctx = ctx()
612
780
  if i == len(node.ops) - 1:
613
781
  curr_ctx.scope.set_value(result_name, result)
782
+ elif result._is_py_():
783
+ if result._as_py_():
784
+ l_val = r_val
785
+ else:
786
+ false_ctxs.append(curr_ctx)
787
+ set_ctx(curr_ctx.into_dead())
788
+ break
614
789
  else:
615
790
  curr_ctx.test = result.ir()
616
791
  true_ctx = curr_ctx.branch(None)
@@ -623,9 +798,9 @@ class Visitor(ast.NodeVisitor):
623
798
  return ctx().scope.get_value(result_name)
624
799
 
625
800
  def visit_Call(self, node):
801
+ from sonolus.script.internal.dict_impl import DictImpl
802
+
626
803
  fn = self.visit(node.func)
627
- if fn is Num:
628
- raise ValueError("Calling int/bool/float is not supported")
629
804
  args = []
630
805
  kwargs = {}
631
806
  for arg in node.args:
@@ -638,8 +813,10 @@ class Visitor(ast.NodeVisitor):
638
813
  kwargs[keyword.arg] = self.visit(keyword.value)
639
814
  else:
640
815
  value = self.visit(keyword.value)
641
- if value._is_py_() and isinstance(value._as_py_(), Mapping):
642
- kwargs.update(value._as_py_())
816
+ if isinstance(value, DictImpl):
817
+ if not all(isinstance(k, str) for k in value.value):
818
+ raise ValueError("Keyword arguments must be strings")
819
+ kwargs.update(value.value)
643
820
  else:
644
821
  raise ValueError("Starred keyword arguments (**kwargs) must be dictionaries")
645
822
  return self.handle_call(node, fn, *args, **kwargs)
@@ -665,11 +842,18 @@ class Visitor(ast.NodeVisitor):
665
842
  raise NotImplementedError("Starred expressions are not supported")
666
843
 
667
844
  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)
845
+ self.active_ctx = ctx()
846
+ v = self
847
+ while v:
848
+ if not isinstance(v.active_ctx.scope.get_binding(node.id), EmptyBinding):
849
+ return v.active_ctx.scope.get_value(node.id)
850
+ v = v.parent
851
+ if node.id in self.globals:
852
+ value = self.globals[node.id]
853
+ if value is ctx:
854
+ raise ValueError("Unexpected use of ctx in non meta-function")
855
+ return validate_value(BUILTIN_IMPLS.get(id(value), value))
856
+ raise NameError(f"Name {node.id} is not defined")
673
857
 
674
858
  def visit_List(self, node):
675
859
  raise NotImplementedError("List literals are not supported")
@@ -712,8 +896,12 @@ class Visitor(ast.NodeVisitor):
712
896
  ctx_init = ctx()
713
897
  l_val = self.ensure_boolean_num(l_val)
714
898
 
715
- if l_val._is_py_() and not l_val._as_py_():
716
- return l_val
899
+ if l_val._is_py_():
900
+ if l_val._as_py_():
901
+ # The rhs is definitely evaluated, so we can return it directly
902
+ return self.ensure_boolean_num(self.visit(r_expr))
903
+ else:
904
+ return l_val
717
905
 
718
906
  ctx_init.test = l_val.ir()
719
907
  res_name = self.new_name("and")
@@ -736,8 +924,12 @@ class Visitor(ast.NodeVisitor):
736
924
  ctx_init = ctx()
737
925
  l_val = self.ensure_boolean_num(l_val)
738
926
 
739
- if l_val._is_py_() and l_val._as_py_():
740
- return l_val
927
+ if l_val._is_py_():
928
+ if l_val._as_py_():
929
+ return l_val
930
+ else:
931
+ # The rhs is definitely evaluated, so we can return it directly
932
+ return self.ensure_boolean_num(self.visit(r_expr))
741
933
 
742
934
  ctx_init.test = l_val.ir()
743
935
  res_name = self.new_name("or")
@@ -764,7 +956,8 @@ class Visitor(ast.NodeVisitor):
764
956
 
765
957
  def handle_getattr(self, node: ast.stmt | ast.expr, target: Value, key: str) -> Value:
766
958
  with self.reporting_errors_at_node(node):
767
- if target._is_py_():
959
+ if isinstance(target, ConstantValue):
960
+ # Unwrap so we can access fields
768
961
  target = target._as_py_()
769
962
  descriptor = type(target).__dict__.get(key)
770
963
  match descriptor:
@@ -796,6 +989,7 @@ class Visitor(ast.NodeVisitor):
796
989
  self, node: ast.stmt | ast.expr, fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs
797
990
  ) -> R:
798
991
  """Handles a call to the given callable."""
992
+ self.active_ctx = ctx()
799
993
  if (
800
994
  isinstance(fn, Value)
801
995
  and fn._is_py_()
@@ -808,13 +1002,10 @@ class Visitor(ast.NodeVisitor):
808
1002
 
809
1003
  def handle_getitem(self, node: ast.stmt | ast.expr, target: Value, key: Value) -> Value:
810
1004
  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__}")
1005
+ if target._is_py_() and isinstance(target._as_py_(), type):
1006
+ if not key._is_py_():
1007
+ raise ValueError("Type parameters must be compile-time constants")
1008
+ return validate_value(target._as_py_()[key._as_py_()])
818
1009
  else:
819
1010
  if isinstance(target, Value) and hasattr(target, "__getitem__"):
820
1011
  return self.handle_call(node, target.__getitem__, key)
@@ -822,21 +1013,19 @@ class Visitor(ast.NodeVisitor):
822
1013
 
823
1014
  def handle_setitem(self, node: ast.stmt | ast.expr, target: Value, key: Value, value: Value):
824
1015
  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__}")
1016
+ if isinstance(target, Value) and hasattr(target, "__setitem__"):
1017
+ return self.handle_call(node, target.__setitem__, key, value)
1018
+ raise TypeError(f"Cannot set items on {type(target).__name__}")
1019
+
1020
+ def handle_delitem(self, node: ast.stmt | ast.expr, target: Value, key: Value):
1021
+ with self.reporting_errors_at_node(node):
1022
+ if isinstance(target, Value) and hasattr(target, "__delitem__"):
1023
+ return self.handle_call(node, target.__delitem__, key)
1024
+ raise TypeError(f"Cannot delete items on {type(target).__name__}")
836
1025
 
837
1026
  def handle_starred(self, value: Value) -> tuple[Value, ...]:
838
- if value._is_py_() and isinstance(value._as_py_(), tuple):
839
- return value._as_py_()
1027
+ if isinstance(value, TupleImpl):
1028
+ return value.value
840
1029
  raise ValueError("Unsupported starred expression")
841
1030
 
842
1031
  def is_not_implemented(self, value):
@@ -845,10 +1034,66 @@ class Visitor(ast.NodeVisitor):
845
1034
 
846
1035
  def ensure_boolean_num(self, value) -> Num:
847
1036
  # This just checks the type for now, although we could support custom __bool__ implementations in the future
848
- if not is_num(value):
1037
+ if not _is_num(value):
849
1038
  raise TypeError(f"Invalid type where a bool (Num) was expected: {type(value).__name__}")
850
1039
  return value
851
1040
 
1041
+ def arguments_to_signature(self, arguments: ast.arguments) -> inspect.Signature:
1042
+ parameters: list[inspect.Parameter] = []
1043
+ pos_only_count = len(arguments.posonlyargs)
1044
+ for i, arg in enumerate(arguments.posonlyargs):
1045
+ default_idx = i - pos_only_count + len(arguments.defaults)
1046
+ default = self.visit(arguments.defaults[default_idx]) if default_idx >= 0 else None
1047
+ param = inspect.Parameter(
1048
+ name=arg.arg,
1049
+ kind=inspect.Parameter.POSITIONAL_ONLY,
1050
+ default=default if default_idx >= 0 else inspect.Parameter.empty,
1051
+ annotation=inspect.Parameter.empty,
1052
+ )
1053
+ parameters.append(param)
1054
+
1055
+ pos_kw_count = len(arguments.args)
1056
+ for i, arg in enumerate(arguments.args):
1057
+ default_idx = i - pos_kw_count + len(arguments.defaults)
1058
+ default = self.visit(arguments.defaults[default_idx]) if default_idx >= 0 else None
1059
+ param = inspect.Parameter(
1060
+ name=arg.arg,
1061
+ kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
1062
+ default=default if default_idx >= 0 else inspect.Parameter.empty,
1063
+ annotation=inspect.Parameter.empty,
1064
+ )
1065
+ parameters.append(param)
1066
+
1067
+ if arguments.vararg:
1068
+ param = inspect.Parameter(
1069
+ name=arguments.vararg.arg,
1070
+ kind=inspect.Parameter.VAR_POSITIONAL,
1071
+ default=inspect.Parameter.empty,
1072
+ annotation=inspect.Parameter.empty,
1073
+ )
1074
+ parameters.append(param)
1075
+
1076
+ for i, arg in enumerate(arguments.kwonlyargs):
1077
+ default = self.visit(arguments.kw_defaults[i]) if arguments.kw_defaults[i] is not None else None
1078
+ param = inspect.Parameter(
1079
+ name=arg.arg,
1080
+ kind=inspect.Parameter.KEYWORD_ONLY,
1081
+ default=default if default is not None else inspect.Parameter.empty,
1082
+ annotation=inspect.Parameter.empty,
1083
+ )
1084
+ parameters.append(param)
1085
+
1086
+ if arguments.kwarg:
1087
+ param = inspect.Parameter(
1088
+ name=arguments.kwarg.arg,
1089
+ kind=inspect.Parameter.VAR_KEYWORD,
1090
+ default=inspect.Parameter.empty,
1091
+ annotation=inspect.Parameter.empty,
1092
+ )
1093
+ parameters.append(param)
1094
+
1095
+ return inspect.Signature(parameters)
1096
+
852
1097
  def raise_exception_at_node(self, node: ast.stmt | ast.expr, cause: Exception) -> Never:
853
1098
  """Throws a compilation error at the given node."""
854
1099
 
@@ -878,15 +1123,29 @@ class Visitor(ast.NodeVisitor):
878
1123
  {"fn": fn, "args": args, "kwargs": kwargs, "_filter_traceback_": True},
879
1124
  )
880
1125
 
881
- @contextmanager
882
1126
  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)
1127
+ return ReportingErrorsAtNode(self, node)
889
1128
 
890
1129
  def new_name(self, name: str):
891
1130
  self.used_names[name] = self.used_names.get(name, 0) + 1
892
1131
  return f"${name}_{self.used_names[name]}"
1132
+
1133
+
1134
+ # Not using @contextmanager so it doesn't end up in tracebacks
1135
+ class ReportingErrorsAtNode:
1136
+ def __init__(self, compiler, node: ast.stmt | ast.expr):
1137
+ self.compiler = compiler
1138
+ self.node = node
1139
+
1140
+ def __enter__(self):
1141
+ return self
1142
+
1143
+ def __exit__(self, exc_type, exc_value, traceback):
1144
+ if exc_type is None:
1145
+ return
1146
+
1147
+ if issubclass(exc_type, CompilationError):
1148
+ raise exc_value from exc_value.__cause__
1149
+
1150
+ if exc_value is not None:
1151
+ self.compiler.raise_exception_at_node(self.node, exc_value)