sonolus.py 0.3.4__py3-none-any.whl → 0.4.0__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 (63) hide show
  1. sonolus/backend/excepthook.py +30 -0
  2. sonolus/backend/finalize.py +15 -1
  3. sonolus/backend/ops.py +4 -0
  4. sonolus/backend/optimize/allocate.py +5 -5
  5. sonolus/backend/optimize/constant_evaluation.py +124 -19
  6. sonolus/backend/optimize/copy_coalesce.py +15 -12
  7. sonolus/backend/optimize/dead_code.py +7 -6
  8. sonolus/backend/optimize/dominance.py +2 -2
  9. sonolus/backend/optimize/flow.py +54 -8
  10. sonolus/backend/optimize/inlining.py +137 -30
  11. sonolus/backend/optimize/liveness.py +2 -2
  12. sonolus/backend/optimize/optimize.py +15 -1
  13. sonolus/backend/optimize/passes.py +11 -3
  14. sonolus/backend/optimize/simplify.py +137 -8
  15. sonolus/backend/optimize/ssa.py +47 -13
  16. sonolus/backend/place.py +5 -4
  17. sonolus/backend/utils.py +24 -0
  18. sonolus/backend/visitor.py +260 -17
  19. sonolus/build/cli.py +47 -19
  20. sonolus/build/compile.py +12 -5
  21. sonolus/build/engine.py +70 -1
  22. sonolus/build/level.py +3 -3
  23. sonolus/build/project.py +2 -2
  24. sonolus/script/archetype.py +12 -9
  25. sonolus/script/array.py +23 -18
  26. sonolus/script/array_like.py +26 -29
  27. sonolus/script/bucket.py +1 -1
  28. sonolus/script/containers.py +22 -26
  29. sonolus/script/debug.py +20 -43
  30. sonolus/script/effect.py +1 -1
  31. sonolus/script/globals.py +3 -3
  32. sonolus/script/instruction.py +2 -2
  33. sonolus/script/internal/builtin_impls.py +155 -28
  34. sonolus/script/internal/constant.py +13 -3
  35. sonolus/script/internal/context.py +46 -15
  36. sonolus/script/internal/impl.py +9 -3
  37. sonolus/script/internal/introspection.py +8 -1
  38. sonolus/script/internal/native.py +2 -2
  39. sonolus/script/internal/range.py +8 -11
  40. sonolus/script/internal/simulation_context.py +1 -1
  41. sonolus/script/internal/transient.py +2 -2
  42. sonolus/script/internal/value.py +41 -3
  43. sonolus/script/interval.py +13 -13
  44. sonolus/script/iterator.py +38 -107
  45. sonolus/script/maybe.py +139 -0
  46. sonolus/script/num.py +17 -2
  47. sonolus/script/options.py +1 -1
  48. sonolus/script/particle.py +1 -1
  49. sonolus/script/project.py +24 -5
  50. sonolus/script/quad.py +15 -15
  51. sonolus/script/record.py +16 -12
  52. sonolus/script/runtime.py +22 -18
  53. sonolus/script/sprite.py +1 -1
  54. sonolus/script/stream.py +66 -82
  55. sonolus/script/transform.py +35 -34
  56. sonolus/script/values.py +10 -10
  57. sonolus/script/vec.py +21 -18
  58. {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.0.dist-info}/METADATA +1 -1
  59. sonolus_py-0.4.0.dist-info/RECORD +93 -0
  60. sonolus_py-0.3.4.dist-info/RECORD +0 -92
  61. {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.0.dist-info}/WHEEL +0 -0
  62. {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.0.dist-info}/entry_points.txt +0 -0
  63. {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,12 +1,12 @@
1
- from collections.abc import Iterable
2
- from typing import overload
3
-
1
+ from sonolus.backend.ops import Op
4
2
  from sonolus.script.array import Array
5
3
  from sonolus.script.array_like import ArrayLike
4
+ from sonolus.script.debug import error
6
5
  from sonolus.script.internal.context import ctx
7
6
  from sonolus.script.internal.dict_impl import DictImpl
8
7
  from sonolus.script.internal.impl import meta_fn, validate_value
9
8
  from sonolus.script.internal.math_impls import MATH_BUILTIN_IMPLS, _trunc
9
+ from sonolus.script.internal.native import native_function
10
10
  from sonolus.script.internal.random import RANDOM_BUILTIN_IMPLS
11
11
  from sonolus.script.internal.range import Range
12
12
  from sonolus.script.internal.tuple_impl import TupleImpl
@@ -21,6 +21,8 @@ from sonolus.script.iterator import (
21
21
  )
22
22
  from sonolus.script.num import Num, _is_num
23
23
 
24
+ _empty = object()
25
+
24
26
 
25
27
  @meta_fn
26
28
  def _isinstance(value, type_):
@@ -44,7 +46,7 @@ def _len(value):
44
46
  value = validate_value(value)
45
47
  if not hasattr(value, "__len__"):
46
48
  raise TypeError(f"object of type '{type(value).__name__}' has no len()")
47
- return compile_and_call(value.__len__)
49
+ return compile_and_call(value.__len__) # type: ignore
48
50
 
49
51
 
50
52
  @meta_fn
@@ -59,7 +61,7 @@ def _enumerate(iterable, start=0):
59
61
  elif isinstance(iterable, ArrayLike):
60
62
  return compile_and_call(iterable._enumerate_, start)
61
63
  else:
62
- iterator = compile_and_call(iterable.__iter__)
64
+ iterator = compile_and_call(iterable.__iter__) # type: ignore
63
65
  if not isinstance(iterator, SonolusIterator):
64
66
  raise TypeError("Only subclasses of SonolusIterator are supported as iterators")
65
67
  return _Enumerator(0, start, iterator)
@@ -104,25 +106,20 @@ def _abs(value):
104
106
  value = validate_value(value)
105
107
  if not hasattr(value, "__abs__"):
106
108
  raise TypeError(f"bad operand type for abs(): '{type(value).__name__}'")
107
- return compile_and_call(value.__abs__)
109
+ return compile_and_call(value.__abs__) # type: ignore
108
110
 
109
111
 
110
112
  def _identity(value):
111
113
  return value
112
114
 
113
115
 
114
- @overload
115
- def _max[T](iterable: Iterable[T], *, key: callable = ...) -> T: ...
116
-
117
-
118
- @overload
119
- def _max[T](a: T, b: T, *args: T, key: callable = ...) -> T: ...
120
-
121
-
122
116
  @meta_fn
123
- def _max(*args, key: callable = _identity):
117
+ def _max(*args, default=_empty, key=None):
124
118
  from sonolus.backend.visitor import compile_and_call
125
119
 
120
+ if key is None:
121
+ key = _identity
122
+
126
123
  args = tuple(validate_value(arg) for arg in args)
127
124
  if len(args) == 0:
128
125
  raise ValueError("Expected at least one argument to max")
@@ -131,40 +128,91 @@ def _max(*args, key: callable = _identity):
131
128
  if isinstance(iterable, ArrayLike):
132
129
  return compile_and_call(iterable._max_, key=key)
133
130
  elif isinstance(iterable, TupleImpl) and all(_is_num(v) for v in iterable.value):
131
+ if len(iterable.value) == 0:
132
+ if default is not _empty:
133
+ return default
134
+ raise ValueError("max() arg is an empty sequence")
134
135
  return compile_and_call(Array(*iterable.value)._max_, key=key)
136
+ elif isinstance(iterable, SonolusIterator):
137
+ if not (default is _empty or Num._accepts_(default)):
138
+ raise TypeError("default argument must be a number")
139
+ return compile_and_call(
140
+ _max_num_iterator,
141
+ iterable,
142
+ Num._accept_(default) if default is not _empty else None,
143
+ key=key if key is not _identity else None,
144
+ )
135
145
  else:
136
146
  raise TypeError(f"Unsupported type: {type(iterable)} for max")
137
147
  else:
148
+ if default is not _empty:
149
+ raise TypeError("default argument is not supported for max with multiple arguments")
138
150
  if not all(_is_num(arg) for arg in args):
139
151
  raise TypeError("Arguments to max must be numbers")
140
152
  if ctx():
141
- result = compile_and_call(_max2, args[0], args[1], key=key)
153
+ result = _max2(args[0], args[1], key=key)
142
154
  for arg in args[2:]:
143
- result = compile_and_call(_max2, result, arg, key=key)
155
+ result = _max2(result, arg, key=key)
144
156
  return result
145
157
  else:
146
158
  return max(arg._as_py_() for arg in args)
147
159
 
148
160
 
149
161
  def _max2(a, b, key=_identity):
150
- if key(a) > key(b):
162
+ from sonolus.backend.visitor import compile_and_call
163
+
164
+ a = validate_value(a)
165
+ b = validate_value(b)
166
+ if _is_num(a) and _is_num(b) and key == _identity:
167
+ return compile_and_call(_max2_num, a, b)
168
+ return compile_and_call(_max2_generic, a, b, key=key)
169
+
170
+
171
+ @native_function(Op.Max)
172
+ def _max2_num(a, b):
173
+ if a > b:
151
174
  return a
152
175
  else:
153
176
  return b
154
177
 
155
178
 
156
- @overload
157
- def _min[T](iterable: Iterable[T], *, key: callable = ...) -> T: ...
179
+ def _max2_generic(a, b, key=_identity):
180
+ if key(a) > key(b):
181
+ return a
182
+ else:
183
+ return b
158
184
 
159
185
 
160
- @overload
161
- def _min[T](a: T, b: T, *args: T, key: callable = ...) -> T: ...
186
+ def _max_num_iterator(iterable, default, key):
187
+ iterator = iterable.__iter__() # noqa: PLC2801
188
+ initial = iterator.next()
189
+ if initial.is_nothing:
190
+ assert default is not None
191
+ return default
192
+ if key is not None:
193
+ result = initial.get_unsafe()
194
+ best_key = key(result)
195
+ for value in iterator:
196
+ new_key = key(value)
197
+ if new_key > best_key:
198
+ result = value
199
+ best_key = new_key
200
+ return result
201
+ else:
202
+ result = initial.get_unsafe()
203
+ for value in iterator:
204
+ if value > result: # noqa: PLR1730
205
+ result = value
206
+ return result
162
207
 
163
208
 
164
209
  @meta_fn
165
- def _min(*args, key: callable = _identity):
210
+ def _min(*args, default=_empty, key=None):
166
211
  from sonolus.backend.visitor import compile_and_call
167
212
 
213
+ if key is None:
214
+ key = _identity
215
+
168
216
  args = tuple(validate_value(arg) for arg in args)
169
217
  if len(args) == 0:
170
218
  raise ValueError("Expected at least one argument to min")
@@ -173,34 +221,92 @@ def _min(*args, key: callable = _identity):
173
221
  if isinstance(iterable, ArrayLike):
174
222
  return compile_and_call(iterable._min_, key=key)
175
223
  elif isinstance(iterable, TupleImpl) and all(_is_num(v) for v in iterable.value):
224
+ if len(iterable.value) == 0:
225
+ if default is not _empty:
226
+ return default
227
+ raise ValueError("min() arg is an empty sequence")
176
228
  return compile_and_call(Array(*iterable.value)._min_, key=key)
229
+ elif isinstance(iterable, SonolusIterator):
230
+ if not (default is _empty or Num._accepts_(default)):
231
+ raise TypeError("default argument must be a number")
232
+ return compile_and_call(
233
+ _min_num_iterator,
234
+ iterable,
235
+ Num._accept_(default) if default is not _empty else None,
236
+ key=key if key is not _identity else None,
237
+ )
177
238
  else:
178
239
  raise TypeError(f"Unsupported type: {type(iterable)} for min")
179
240
  else:
241
+ if default is not _empty:
242
+ raise TypeError("default argument is not supported for min with multiple arguments")
180
243
  if not all(_is_num(arg) for arg in args):
181
244
  raise TypeError("Arguments to min must be numbers")
182
245
  if ctx():
183
- result = compile_and_call(_min2, args[0], args[1], key=key)
246
+ result = _min2(args[0], args[1], key=key)
184
247
  for arg in args[2:]:
185
- result = compile_and_call(_min2, result, arg, key=key)
248
+ result = _min2(result, arg, key=key)
186
249
  return result
187
250
  else:
188
251
  return min(arg._as_py_() for arg in args)
189
252
 
190
253
 
191
254
  def _min2(a, b, key=_identity):
255
+ from sonolus.backend.visitor import compile_and_call
256
+
257
+ a = validate_value(a)
258
+ b = validate_value(b)
259
+ if _is_num(a) and _is_num(b) and key == _identity:
260
+ return compile_and_call(_min2_num, a, b)
261
+ return compile_and_call(_min2_generic, a, b, key=key)
262
+
263
+
264
+ @native_function(Op.Min)
265
+ def _min2_num(a, b):
266
+ if a < b:
267
+ return a
268
+ else:
269
+ return b
270
+
271
+
272
+ def _min2_generic(a, b, key=_identity):
192
273
  if key(a) < key(b):
193
274
  return a
194
275
  else:
195
276
  return b
196
277
 
197
278
 
279
+ def _min_num_iterator(iterable, default, key):
280
+ iterator = iterable.__iter__() # noqa: PLC2801
281
+ initial = iterator.next()
282
+ if initial.is_nothing:
283
+ assert default is not None
284
+ return default
285
+ if key is not None:
286
+ result = initial.get_unsafe()
287
+ best_key = key(result)
288
+ for value in iterator:
289
+ new_key = key(value)
290
+ if new_key < best_key:
291
+ result = value
292
+ best_key = new_key
293
+ return result
294
+ else:
295
+ result = initial.get_unsafe()
296
+ for value in iterator:
297
+ if value < result: # noqa: PLR1730
298
+ result = value
299
+ return result
300
+
301
+
198
302
  @meta_fn
199
303
  def _callable(value):
200
304
  return callable(value)
201
305
 
202
306
 
203
307
  def _map(fn, iterable, *iterables):
308
+ if len(iterables) == 0:
309
+ return _MappingIterator(fn, iterable.__iter__()) # noqa: PLC2801
204
310
  return _MappingIterator(lambda args: fn(*args), zip(iterable, *iterables)) # noqa: B905
205
311
 
206
312
 
@@ -234,9 +340,9 @@ def _bool(value=False):
234
340
  return False
235
341
 
236
342
 
237
- _int._type_mapping_ = Num
238
- _float._type_mapping_ = Num
239
- _bool._type_mapping_ = Num
343
+ _int._type_mapping_ = Num # type: ignore
344
+ _float._type_mapping_ = Num # type: ignore
345
+ _bool._type_mapping_ = Num # type: ignore
240
346
 
241
347
 
242
348
  def _any(iterable):
@@ -253,12 +359,31 @@ def _all(iterable):
253
359
  return True
254
360
 
255
361
 
362
+ def _sum(iterable, /, start=0):
363
+ for value in iterable:
364
+ start += value
365
+ return start
366
+
367
+
368
+ def _next(iterator):
369
+ assert isinstance(iterator, SonolusIterator)
370
+ value = iterator.next()
371
+ if value.is_some:
372
+ return value.get_unsafe()
373
+ error("Iterator has been exhausted")
374
+
375
+
376
+ def _iter(iterable):
377
+ return iterable.__iter__() # type: ignore # noqa: PLC2801
378
+
379
+
256
380
  # classmethod, property, staticmethod are supported as decorators, but not within functions
257
381
 
258
382
  BUILTIN_IMPLS = {
259
383
  id(abs): _abs,
260
384
  id(all): _all,
261
385
  id(any): _any,
386
+ id(sum): _sum,
262
387
  id(bool): _bool,
263
388
  id(callable): _callable,
264
389
  id(enumerate): _enumerate,
@@ -266,10 +391,12 @@ BUILTIN_IMPLS = {
266
391
  id(float): _float,
267
392
  id(int): _int,
268
393
  id(isinstance): _isinstance,
394
+ id(iter): _iter,
269
395
  id(len): _len,
270
396
  id(map): _map,
271
397
  id(max): _max,
272
398
  id(min): _min,
399
+ id(next): _next,
273
400
  id(range): Range,
274
401
  id(reversed): _reversed,
275
402
  id(zip): _zip,
@@ -48,8 +48,8 @@ class ConstantValue(Value):
48
48
  class Parameterized(cls):
49
49
  _value = (parameter,)
50
50
 
51
- Parameterized.__name__ = f"{parameter}"
52
- Parameterized.__qualname__ = f"{parameter}"
51
+ Parameterized.__name__ = f"Const[{object.__repr__(parameter)}]" # noqa: PLC2801
52
+ Parameterized.__qualname__ = Parameterized.__name__
53
53
  Parameterized.__module__ = cls.__module__
54
54
  Parameterized.instance = object.__new__(Parameterized)
55
55
  return Parameterized
@@ -113,7 +113,7 @@ class ConstantValue(Value):
113
113
  if value is not self:
114
114
  raise ValueError(f"{type(self).__name__} is immutable")
115
115
 
116
- def _copy_from_(self, value: Self):
116
+ def _copy_from_(self, value: Any):
117
117
  if value is not self:
118
118
  raise ValueError(f"{type(self).__name__} is immutable")
119
119
 
@@ -143,3 +143,13 @@ class ConstantValue(Value):
143
143
 
144
144
  class BasicConstantValue(ConstantValue):
145
145
  """For constants without any special behavior."""
146
+
147
+
148
+ class TypingSpecialFormConstant(ConstantValue):
149
+ """For constants that are typing special forms that have a [] operator."""
150
+
151
+ @meta_fn
152
+ def __getitem__(self, item: Any) -> Self:
153
+ if not item._is_py_():
154
+ raise TypeError(f"Invalid value for type parameter: {item}")
155
+ return self.value()[item._as_py_()]
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Iterable
3
4
  from contextlib import contextmanager
4
5
  from contextvars import ContextVar
5
6
  from dataclasses import dataclass
@@ -7,7 +8,7 @@ from threading import Lock
7
8
  from typing import Any, Self
8
9
 
9
10
  from sonolus.backend.blocks import BlockData, PlayBlock
10
- from sonolus.backend.ir import IRConst, IRStmt
11
+ from sonolus.backend.ir import IRConst, IRExpr, IRStmt
11
12
  from sonolus.backend.mode import Mode
12
13
  from sonolus.backend.optimize.flow import BasicBlock, FlowEdge
13
14
  from sonolus.backend.place import Block, BlockPlace, TempBlock
@@ -16,7 +17,7 @@ from sonolus.script.internal.value import Value
16
17
 
17
18
  _compiler_internal_ = True
18
19
 
19
- context_var = ContextVar("context_var", default=None)
20
+ context_var: ContextVar[Context | None] = ContextVar("context_var", default=None) # type: ignore
20
21
 
21
22
 
22
23
  @dataclass(frozen=True)
@@ -70,7 +71,7 @@ class Context:
70
71
  global_state: GlobalContextState
71
72
  callback_state: CallbackContextState
72
73
  statements: list[IRStmt]
73
- test: IRStmt
74
+ test: IRExpr
74
75
  outgoing: dict[float | None, Context]
75
76
  scope: Scope
76
77
  loop_variables: dict[str, Value]
@@ -115,7 +116,7 @@ class Context:
115
116
  return
116
117
  if not self.callback:
117
118
  return
118
- if isinstance(place.block, BlockData) and self.callback not in self.blocks(place.block).readable:
119
+ if isinstance(place.block, BlockData) and not self.is_readable(place):
119
120
  raise RuntimeError(f"Block {place.block} is not readable in {self.callback}")
120
121
 
121
122
  def check_writable(self, place: BlockPlace):
@@ -123,9 +124,19 @@ class Context:
123
124
  return
124
125
  if not self.callback:
125
126
  return
126
- if isinstance(place.block, BlockData) and self.callback not in self.blocks(place.block).writable:
127
+ if isinstance(place.block, BlockData) and not self.is_writable(place):
127
128
  raise RuntimeError(f"Block {place.block} is not writable in {self.callback}")
128
129
 
130
+ def is_readable(self, place: BlockPlace) -> bool:
131
+ if debug_config().unchecked_reads:
132
+ return True
133
+ return self.callback and self.callback in self.blocks(place.block).readable
134
+
135
+ def is_writable(self, place: BlockPlace) -> bool:
136
+ if debug_config().unchecked_writes:
137
+ return True
138
+ return self.callback and self.callback in self.blocks(place.block).writable
139
+
129
140
  def add_statement(self, statement: IRStmt):
130
141
  if not self.live:
131
142
  return
@@ -169,20 +180,26 @@ class Context:
169
180
  self.outgoing[condition] = result
170
181
  return result
171
182
 
183
+ def new_disconnected(self):
184
+ return self.copy_with_scope(self.scope.copy())
185
+
186
+ def new_empty_disconnected(self):
187
+ return self.copy_with_scope(Scope())
188
+
172
189
  def into_dead(self):
173
190
  """Create a new context for code that is unreachable, like after a return statement."""
174
191
  result = self.copy_with_scope(self.scope.copy())
175
192
  result.live = False
176
193
  return result
177
194
 
178
- def prepare_loop_header(self, to_merge: set[str]) -> Self:
195
+ def prepare_loop_header(self, to_merge: set[str]) -> Context:
179
196
  # to_merge is the set of bindings set anywhere in the loop
180
197
  # we need to invalidate them in the header if they're reference types
181
198
  # or merge them if they're value types
182
199
  # structure is self -> intermediate -> header -> body (continue -> header) | exit
183
200
  assert len(self.outgoing) == 0
184
201
  header = self.branch(None)
185
- for name in to_merge:
202
+ for name in sorted(to_merge):
186
203
  binding = self.scope.get_binding(name)
187
204
  if not isinstance(binding, ValueBinding):
188
205
  continue
@@ -205,12 +222,15 @@ class Context:
205
222
  assert len(self.outgoing) == 0
206
223
  self.outgoing[None] = header
207
224
  values = {}
208
- # First do a pass through and copy every value
225
+ # First do a pass through and get every value
209
226
  for name, target_value in header.loop_variables.items():
210
227
  with using_ctx(self):
211
228
  if type(target_value)._is_value_type_():
212
229
  value = self.scope.get_value(name)
213
- values[name] = value._get_() # _get_() will make a copy on value types
230
+ # We make this call to _get_readonly_() to ensure that we're reading the value at this
231
+ # point in time specifically, since _get_readonly_ will make a copy if the value is
232
+ # e.g. a Num backed by a TempBlock which could be mutated.
233
+ values[name] = value._get_readonly_()
214
234
  # Then actually set them
215
235
  for name, target_value in header.loop_variables.items():
216
236
  with using_ctx(self):
@@ -236,7 +256,7 @@ class Context:
236
256
  with self.global_state.lock:
237
257
  block = value.blocks.get(self.global_state.mode)
238
258
  if block is None:
239
- raise RuntimeError(f"Global {value.name} is not available in '{self.global_state.mode.name}' mode")
259
+ raise RuntimeError(f"Global {value} is not available in '{self.global_state.mode.name}' mode")
240
260
  if value not in self.global_state.environment_mappings:
241
261
  if value.offset is None:
242
262
  offset = self.global_state.environment_offsets.get(block, 0)
@@ -268,7 +288,7 @@ class Context:
268
288
  return self.global_state.archetypes[type_]
269
289
 
270
290
 
271
- def ctx() -> Context | None:
291
+ def ctx() -> Context | Any: # Using Any to silence type checker warnings if it's None
272
292
  return context_var.get()
273
293
 
274
294
 
@@ -368,7 +388,7 @@ class Scope:
368
388
  def set_binding(self, name: str, binding: Binding):
369
389
  self.bindings[name] = binding
370
390
 
371
- def get_value(self, name: str) -> Value:
391
+ def get_value(self, name: str) -> Value | Any:
372
392
  binding = self.get_binding(name)
373
393
  match binding:
374
394
  case ValueBinding(value):
@@ -398,7 +418,7 @@ class Scope:
398
418
  return
399
419
  assert all(len(inc.outgoing) == 0 for inc in incoming)
400
420
  sources = [context.scope for context in incoming]
401
- keys = {key for source in sources for key in source.bindings}
421
+ keys = unique(key for source in sources for key in source.bindings)
402
422
  for key in keys:
403
423
  bindings = [source.get_binding(key) for source in sources]
404
424
  if not all(isinstance(binding, ValueBinding) for binding in bindings):
@@ -413,8 +433,9 @@ class Scope:
413
433
  target.scope.set_binding(key, ConflictBinding())
414
434
  continue
415
435
  common_type: type[Value] = types.pop()
416
- if common_type._is_value_type_():
417
- target_value = common_type._from_place_(target.alloc(size=common_type._size_()))
436
+ with using_ctx(target):
437
+ target_value = common_type._get_merge_target_(values)
438
+ if target_value is not NotImplemented:
418
439
  for inc in incoming:
419
440
  with using_ctx(inc):
420
441
  target_value._set_(inc.scope.get_value(key))
@@ -447,3 +468,13 @@ def context_to_cfg(context: Context) -> BasicBlock:
447
468
  blocks[current].outgoing.add(edge)
448
469
  blocks[target].incoming.add(edge)
449
470
  return blocks[context]
471
+
472
+
473
+ def unique[T](iterable: Iterable[T]) -> list[T]:
474
+ result = []
475
+ seen = set()
476
+ for item in iterable:
477
+ if item not in seen:
478
+ seen.add(item)
479
+ result.append(item)
480
+ return result
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from collections.abc import Callable
4
4
  from enum import Enum
5
5
  from types import EllipsisType, FunctionType, MethodType, ModuleType, NoneType, NotImplementedType, UnionType
6
- from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeVar, get_origin, overload
6
+ from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeVar, Union, get_origin, overload
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from sonolus.script.internal.value import Value
@@ -33,7 +33,7 @@ def meta_fn(fn=None):
33
33
  return decorator(fn)
34
34
 
35
35
 
36
- def validate_value(value: Any) -> Value:
36
+ def validate_value[T](value: T) -> Value | T:
37
37
  result = try_validate_value(value)
38
38
  if result is None:
39
39
  raise TypeError(f"Unsupported value: {value!r}")
@@ -42,7 +42,7 @@ def validate_value(value: Any) -> Value:
42
42
 
43
43
  def try_validate_value(value: Any) -> Value | None:
44
44
  from sonolus.script.globals import _GlobalPlaceholder
45
- from sonolus.script.internal.constant import BasicConstantValue
45
+ from sonolus.script.internal.constant import BasicConstantValue, TypingSpecialFormConstant
46
46
  from sonolus.script.internal.dict_impl import DictImpl
47
47
  from sonolus.script.internal.generic import PartialGeneric
48
48
  from sonolus.script.internal.tuple_impl import TupleImpl
@@ -85,6 +85,12 @@ def try_validate_value(value: Any) -> Value | None:
85
85
  | EllipsisType()
86
86
  ):
87
87
  return BasicConstantValue.of(value)
88
+ case special_form if value in {
89
+ Literal,
90
+ Annotated,
91
+ Union,
92
+ }:
93
+ return TypingSpecialFormConstant.of(special_form)
88
94
  case other_type if get_origin(value) in {Literal, Annotated, UnionType, tuple}:
89
95
  return BasicConstantValue.of(other_type)
90
96
  case _GlobalPlaceholder():
@@ -4,7 +4,14 @@ from typing import Annotated
4
4
  _missing = object()
5
5
 
6
6
 
7
- def get_field_specifiers(cls, *, skip: set[str] = frozenset(), globals=None, locals=None, eval_str=True): # noqa: A002
7
+ def get_field_specifiers(
8
+ cls,
9
+ *,
10
+ skip: frozenset[str] | set[str] = frozenset(),
11
+ globals=None, # noqa: A002
12
+ locals=None, # noqa: A002
13
+ eval_str=True,
14
+ ):
8
15
  """Like inspect.get_annotations, but also turns class attributes into Annotated."""
9
16
  results = inspect.get_annotations(cls, globals=globals, locals=locals, eval_str=eval_str)
10
17
  for key, value in results.items():
@@ -33,8 +33,8 @@ def native_function[**P, R](op: Op) -> Callable[[Callable[P, R]], Callable[P, R]
33
33
  bound_args = signature.bind(*args)
34
34
  bound_args.apply_defaults()
35
35
  return native_call(op, *(Num._accept_(arg) for arg in bound_args.args))
36
- return fn(*args)
36
+ return fn(*args) # type: ignore
37
37
 
38
38
  return wrapper
39
39
 
40
- return decorator
40
+ return decorator # type: ignore
@@ -2,6 +2,7 @@ from sonolus.script.array_like import ArrayLike, get_positive_index
2
2
  from sonolus.script.internal.context import ctx
3
3
  from sonolus.script.internal.impl import meta_fn, validate_value
4
4
  from sonolus.script.iterator import SonolusIterator
5
+ from sonolus.script.maybe import Maybe, Nothing, Some
5
6
  from sonolus.script.num import Num
6
7
  from sonolus.script.record import Record
7
8
 
@@ -70,17 +71,13 @@ class RangeIterator(Record, SonolusIterator):
70
71
  stop: int
71
72
  step: int
72
73
 
73
- def has_next(self) -> bool:
74
- if self.step > 0:
75
- return self.value < self.stop
76
- else:
77
- return self.value > self.stop
78
-
79
- def get(self) -> int:
80
- return self.value
81
-
82
- def advance(self):
83
- self.value += self.step
74
+ def next(self) -> Maybe[int]:
75
+ has_next = self.value < self.stop if self.step > 0 else self.value > self.stop
76
+ if has_next:
77
+ current = self.value
78
+ self.value += self.step
79
+ return Some(current)
80
+ return Nothing
84
81
 
85
82
 
86
83
  @meta_fn
@@ -126,6 +126,6 @@ class SimulationContext:
126
126
  SimulationContext._active_context = None
127
127
 
128
128
 
129
- def sim_ctx() -> SimulationContext | None:
129
+ def sim_ctx() -> SimulationContext | Any:
130
130
  """Get the current simulation context, or None if not active."""
131
131
  return SimulationContext._active_context
@@ -36,11 +36,11 @@ class TransientValue(Value):
36
36
  def _get_(self) -> Self:
37
37
  return self
38
38
 
39
- def _set_(self, value: Self) -> None:
39
+ def _set_(self, value: Any) -> None:
40
40
  if value is not self:
41
41
  raise TypeError(f"{type(self).__name__} is immutable")
42
42
 
43
- def _copy_from_(self, value: Self):
43
+ def _copy_from_(self, value: Any):
44
44
  raise TypeError(f"{type(self).__name__} is immutable")
45
45
 
46
46
  def _copy_(self) -> Self: