sonolus.py 0.3.1__py3-none-any.whl → 0.3.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (36) hide show
  1. sonolus/backend/finalize.py +16 -4
  2. sonolus/backend/node.py +13 -5
  3. sonolus/backend/optimize/allocate.py +41 -4
  4. sonolus/backend/optimize/flow.py +24 -7
  5. sonolus/backend/optimize/optimize.py +2 -9
  6. sonolus/backend/utils.py +6 -1
  7. sonolus/backend/visitor.py +72 -23
  8. sonolus/build/cli.py +6 -1
  9. sonolus/build/engine.py +1 -1
  10. sonolus/script/archetype.py +52 -24
  11. sonolus/script/array.py +20 -8
  12. sonolus/script/array_like.py +30 -3
  13. sonolus/script/containers.py +27 -7
  14. sonolus/script/debug.py +66 -8
  15. sonolus/script/globals.py +17 -0
  16. sonolus/script/internal/builtin_impls.py +12 -8
  17. sonolus/script/internal/context.py +55 -1
  18. sonolus/script/internal/range.py +25 -2
  19. sonolus/script/internal/simulation_context.py +131 -0
  20. sonolus/script/internal/tuple_impl.py +18 -11
  21. sonolus/script/interval.py +60 -2
  22. sonolus/script/iterator.py +3 -2
  23. sonolus/script/num.py +11 -2
  24. sonolus/script/options.py +24 -1
  25. sonolus/script/quad.py +41 -3
  26. sonolus/script/record.py +24 -3
  27. sonolus/script/runtime.py +411 -0
  28. sonolus/script/stream.py +133 -16
  29. sonolus/script/transform.py +291 -2
  30. sonolus/script/values.py +9 -3
  31. sonolus/script/vec.py +14 -2
  32. {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/METADATA +1 -1
  33. {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/RECORD +36 -35
  34. {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/WHEEL +0 -0
  35. {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/entry_points.txt +0 -0
  36. {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/licenses/LICENSE +0 -0
@@ -185,14 +185,14 @@ class _ArchetypeField(SonolusDescriptor):
185
185
  def imported(*, name: str | None = None) -> Any:
186
186
  """Declare a field as imported.
187
187
 
188
- Imported fields may be loaded from the level data.
188
+ Imported fields may be loaded from the level.
189
189
 
190
190
  In watch mode, data may also be loaded from a corresponding exported field in play mode.
191
191
 
192
192
  Imported fields may only be updated in the `preprocess` callback, and are read-only in other callbacks.
193
193
 
194
194
  Usage:
195
- ```
195
+ ```python
196
196
  class MyArchetype(PlayArchetype):
197
197
  field: int = imported()
198
198
  field_with_explicit_name: int = imported(name="field_name")
@@ -207,10 +207,10 @@ def entity_data() -> Any:
207
207
  Entity data is accessible from other entities, but may only be updated in the `preprocess` callback
208
208
  and is read-only in other callbacks.
209
209
 
210
- It functions like `imported`, except that it is not loaded from the level data.
210
+ It functions like `imported` and shares the same underlying storage, except that it is not loaded from a level.
211
211
 
212
212
  Usage:
213
- ```
213
+ ```python
214
214
  class MyArchetype(PlayArchetype):
215
215
  field: int = entity_data()
216
216
  ```
@@ -226,7 +226,7 @@ def exported(*, name: str | None = None) -> Any:
226
226
  Exported fields are write-only.
227
227
 
228
228
  Usage:
229
- ```
229
+ ```python
230
230
  class MyArchetype(PlayArchetype):
231
231
  field: int = exported()
232
232
  field_with_explicit_name: int = exported(name="#FIELD")
@@ -238,12 +238,13 @@ def exported(*, name: str | None = None) -> Any:
238
238
  def entity_memory() -> Any:
239
239
  """Declare a field as entity memory.
240
240
 
241
- Entity memory is private to the entity and is not accessible from other entities.
241
+ Entity memory is private to the entity and is not accessible from other entities. It may be read or updated in any
242
+ callback associated with the entity.
242
243
 
243
244
  Entity memory fields may also be set when an entity is spawned using the `spawn()` method.
244
245
 
245
246
  Usage:
246
- ```
247
+ ```python
247
248
  class MyArchetype(PlayArchetype):
248
249
  field: int = entity_memory()
249
250
 
@@ -257,10 +258,11 @@ def shared_memory() -> Any:
257
258
 
258
259
  Shared memory is accessible from other entities.
259
260
 
260
- Shared memory may only be updated by sequential callbacks such as `preprocess`, `update_sequential`, and `touch`.
261
+ Shared memory may be read in any callback, but may only be updated by sequential callbacks
262
+ (`preprocess`, `update_sequential`, and `touch`).
261
263
 
262
264
  Usage:
263
- ```
265
+ ```python
264
266
  class MyArchetype(PlayArchetype):
265
267
  field: int = shared_memory()
266
268
  ```
@@ -280,7 +282,7 @@ class StandardImport:
280
282
  """Standard import annotations for Archetype fields.
281
283
 
282
284
  Usage:
283
- ```
285
+ ```python
284
286
  class MyArchetype(WatchArchetype):
285
287
  judgment: StandardImport.JUDGMENT
286
288
  ```
@@ -313,7 +315,7 @@ def callback[T: Callable](*, order: int = 0) -> Callable[[T], T]:
313
315
  Callbacks are execute from lowest to highest order. By default, callbacks have an order of 0.
314
316
 
315
317
  Usage:
316
- ```
318
+ ```python
317
319
  class MyArchetype(PlayArchetype):
318
320
  @callback(order=1)
319
321
  def update_sequential(self):
@@ -360,6 +362,8 @@ class ArchetypeSchema(TypedDict):
360
362
  class _BaseArchetype:
361
363
  _is_comptime_value_ = True
362
364
 
365
+ _removable_prefix: ClassVar[str] = ""
366
+
363
367
  _supported_callbacks_: ClassVar[dict[str, CallbackInfo]]
364
368
  _default_callbacks_: ClassVar[set[Callable]]
365
369
 
@@ -440,7 +444,7 @@ class _BaseArchetype:
440
444
  """Spawn an entity of this archetype, injecting the given values into entity memory.
441
445
 
442
446
  Usage:
443
- ```
447
+ ```python
444
448
  class MyArchetype(PlayArchetype):
445
449
  field: int = entity_memory()
446
450
 
@@ -492,7 +496,7 @@ class _BaseArchetype:
492
496
  cls._default_callbacks_ = {getattr(cls, cb_info.py_name) for cb_info in cls._supported_callbacks_.values()}
493
497
  return
494
498
  if cls.name is None or cls.name in {getattr(mro_entry, "name", None) for mro_entry in cls.mro()[1:]}:
495
- cls.name = cls.__name__
499
+ cls.name = cls.__name__.removeprefix(cls._removable_prefix)
496
500
  cls._callbacks_ = []
497
501
  for name in cls._supported_callbacks_:
498
502
  cb = getattr(cls, name)
@@ -545,11 +549,19 @@ class _BaseArchetype:
545
549
  metadata = _annotation_defaults.get(metadata, metadata)
546
550
  if isinstance(metadata, _ArchetypeFieldInfo):
547
551
  if field_info is not None:
548
- raise TypeError(
549
- f"Unexpected multiple field annotations for '{name}', "
550
- f"expected exactly one of imported, exported, entity_memory, or shared_memory"
551
- )
552
- field_info = metadata
552
+ if field_info.storage == metadata.storage and field_info.name is None:
553
+ field_info = metadata
554
+ elif field_info.storage == metadata.storage and (
555
+ metadata.name is None or field_info.name == metadata.name
556
+ ):
557
+ pass
558
+ else:
559
+ raise TypeError(
560
+ f"Unexpected multiple field annotations for '{name}', "
561
+ f"expected exactly one of imported, exported, entity_memory, or shared_memory"
562
+ )
563
+ else:
564
+ field_info = metadata
553
565
  if field_info is None:
554
566
  raise TypeError(
555
567
  f"Missing field annotation for '{name}', "
@@ -642,7 +654,7 @@ class PlayArchetype(_BaseArchetype):
642
654
  """Base class for play mode archetypes.
643
655
 
644
656
  Usage:
645
- ```
657
+ ```python
646
658
  class MyArchetype(PlayArchetype):
647
659
  # Set to True if the entity is a note and contributes to combo and score
648
660
  # Default is False
@@ -659,6 +671,8 @@ class PlayArchetype(_BaseArchetype):
659
671
  ```
660
672
  """
661
673
 
674
+ _removable_prefix: ClassVar[str] = "Play"
675
+
662
676
  _supported_callbacks_ = PLAY_CALLBACKS
663
677
 
664
678
  is_scored: ClassVar[bool] = False
@@ -808,7 +822,7 @@ class WatchArchetype(_BaseArchetype):
808
822
  """Base class for watch mode archetypes.
809
823
 
810
824
  Usage:
811
- ```
825
+ ```python
812
826
  class MyArchetype(WatchArchetype):
813
827
  imported_field: int = imported()
814
828
  entity_memory_field: int = entity_memory()
@@ -820,6 +834,8 @@ class WatchArchetype(_BaseArchetype):
820
834
  ```
821
835
  """
822
836
 
837
+ _removable_prefix: ClassVar[str] = "Watch"
838
+
823
839
  _supported_callbacks_ = WATCH_ARCHETYPE_CALLBACKS
824
840
 
825
841
  def preprocess(self):
@@ -872,7 +888,7 @@ class WatchArchetype(_BaseArchetype):
872
888
  case _ArchetypeSelfData():
873
889
  return _deref(ctx().blocks.EntityInfo, 0, WatchEntityInfo)
874
890
  case _ArchetypeReferenceData(index=index):
875
- return _deref(ctx().blocks.EntityInfoArray, index * WatchEntityInfo._size_(), PlayEntityInfo)
891
+ return _deref(ctx().blocks.EntityInfoArray, index * WatchEntityInfo._size_(), WatchEntityInfo)
876
892
  case _:
877
893
  raise RuntimeError("Info is only accessible from the entity itself")
878
894
 
@@ -923,7 +939,7 @@ class PreviewArchetype(_BaseArchetype):
923
939
  """Base class for preview mode archetypes.
924
940
 
925
941
  Usage:
926
- ```
942
+ ```python
927
943
  class MyArchetype(PreviewArchetype):
928
944
  imported_field: int = imported()
929
945
  entity_memory_field: int = entity_memory()
@@ -935,6 +951,8 @@ class PreviewArchetype(_BaseArchetype):
935
951
  ```
936
952
  """
937
953
 
954
+ _removable_prefix: ClassVar[str] = "Preview"
955
+
938
956
  _supported_callbacks_ = PREVIEW_CALLBACKS
939
957
 
940
958
  def preprocess(self):
@@ -1078,7 +1096,7 @@ class EntityRef[A: _BaseArchetype](Record):
1078
1096
  May be used with `Any` to reference an unknown archetype.
1079
1097
 
1080
1098
  Usage:
1081
- ```
1099
+ ```python
1082
1100
  class MyArchetype(PlayArchetype):
1083
1101
  ref_1: EntityRef[OtherArchetype] = imported()
1084
1102
  ref_2: EntityRef[Any] = imported()
@@ -1096,10 +1114,20 @@ class EntityRef[A: _BaseArchetype](Record):
1096
1114
  """Return a new reference with the given archetype type."""
1097
1115
  return EntityRef[archetype](index=self.index)
1098
1116
 
1117
+ @meta_fn
1099
1118
  def get(self) -> A:
1100
1119
  """Get the entity."""
1120
+ if ref := getattr(self, "_ref_", None):
1121
+ return ref
1101
1122
  return self.archetype().at(self.index)
1102
1123
 
1124
+ @meta_fn
1125
+ def get_as(self, archetype: type[_BaseArchetype]) -> _BaseArchetype:
1126
+ """Get the entity as the given archetype type."""
1127
+ if getattr(archetype, "_ref_", None):
1128
+ raise TypeError("Using get_as in level data is not supported.")
1129
+ return self.with_archetype(archetype).get()
1130
+
1103
1131
  def archetype_matches(self) -> bool:
1104
1132
  """Check if entity at the index is precisely of the archetype."""
1105
1133
  return self.index >= 0 and self.archetype().is_at(self.index)
@@ -1107,7 +1135,7 @@ class EntityRef[A: _BaseArchetype](Record):
1107
1135
  def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
1108
1136
  ref = getattr(self, "_ref_", None)
1109
1137
  if ref is None:
1110
- return [self.index]
1138
+ return Num._accept_(self.index)._to_list_()
1111
1139
  else:
1112
1140
  if ref not in level_refs:
1113
1141
  raise KeyError("Reference to entity not in level data")
sonolus/script/array.py CHANGED
@@ -1,4 +1,3 @@
1
- # ruff: noqa: A005
2
1
  from __future__ import annotations
3
2
 
4
3
  from collections.abc import Iterable
@@ -6,7 +5,7 @@ from typing import Any, Self, final
6
5
 
7
6
  from sonolus.backend.ir import IRConst, IRSet
8
7
  from sonolus.backend.place import BlockPlace
9
- from sonolus.script.array_like import ArrayLike
8
+ from sonolus.script.array_like import ArrayLike, get_positive_index
10
9
  from sonolus.script.debug import assert_unreachable
11
10
  from sonolus.script.internal.context import ctx
12
11
  from sonolus.script.internal.error import InternalError
@@ -16,14 +15,22 @@ from sonolus.script.internal.value import BackingSource, DataValue, Value
16
15
  from sonolus.script.num import Num
17
16
 
18
17
 
18
+ class ArrayMeta(type):
19
+ @meta_fn
20
+ def __pos__[T](cls: type[T]) -> T:
21
+ """Create a zero-initialized array instance."""
22
+ return cls._zero_()
23
+
24
+
19
25
  @final
20
- class Array[T, Size](GenericValue, ArrayLike[T]):
26
+ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
21
27
  """A fixed size array of values.
22
28
 
23
29
  Usage:
24
30
  ```python
25
31
  array_1 = Array(1, 2, 3)
26
32
  array_2 = Array[int, 0]()
33
+ array_3 = +Array[int, 3] # Create a zero-initialized array
27
34
  ```
28
35
  """
29
36
 
@@ -66,7 +73,7 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
66
73
  else:
67
74
  return parameterized_cls._with_value([value._copy_() for value in values])
68
75
 
69
- def __init__(self, *_args: T):
76
+ def __init__(self, *args: T):
70
77
  super().__init__()
71
78
 
72
79
  @classmethod
@@ -180,7 +187,7 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
180
187
 
181
188
  @meta_fn
182
189
  def __getitem__(self, index: Num) -> T:
183
- index: Num = Num._accept_(index)
190
+ index: Num = Num._accept_(get_positive_index(index, self.size()))
184
191
  if index._is_py_() and 0 <= index._as_py_() < self.size():
185
192
  const_index = index._as_py_()
186
193
  if isinstance(const_index, float) and not const_index.is_integer():
@@ -224,7 +231,7 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
224
231
 
225
232
  @meta_fn
226
233
  def __setitem__(self, index: Num, value: T):
227
- index: Num = Num._accept_(index)
234
+ index: Num = Num._accept_(get_positive_index(index, self.size()))
228
235
  value = self.element_type()._accept_(value)
229
236
  if ctx():
230
237
  if isinstance(self._value, list):
@@ -289,10 +296,15 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
289
296
  if isinstance(self._value, BlockPlace) or callable(self._value):
290
297
  return f"{type(self).__name__}({self._value}...)"
291
298
  else:
292
- return f"{type(self).__name__}({", ".join(str(self[i]) for i in range(self.size()))})"
299
+ return f"{type(self).__name__}({', '.join(str(self[i]) for i in range(self.size()))})"
293
300
 
294
301
  def __repr__(self):
295
302
  if isinstance(self._value, BlockPlace) or callable(self._value):
296
303
  return f"{type(self).__name__}({self._value}...)"
297
304
  else:
298
- return f"{type(self).__name__}({", ".join(repr(self[i]) for i in range(self.size()))})"
305
+ return f"{type(self).__name__}({', '.join(repr(self[i]) for i in range(self.size()))})"
306
+
307
+ @meta_fn
308
+ def __pos__(self) -> Self:
309
+ """Return a copy of the array."""
310
+ return self._copy_()
@@ -1,10 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import random
4
- from abc import ABC, abstractmethod
5
- from collections.abc import Callable, Sequence
4
+ from abc import abstractmethod
5
+ from collections.abc import Callable
6
6
  from typing import Any
7
7
 
8
+ from sonolus.script.internal.context import ctx
9
+ from sonolus.script.internal.impl import meta_fn
8
10
  from sonolus.script.iterator import SonolusIterator
9
11
  from sonolus.script.num import Num
10
12
  from sonolus.script.record import Record
@@ -13,7 +15,7 @@ from sonolus.script.values import copy
13
15
  # Note: we don't use Range in this file because Range itself inherits from ArrayLike
14
16
 
15
17
 
16
- class ArrayLike[T](Sequence, ABC):
18
+ class ArrayLike[T]:
17
19
  """Mixin for array-like objects.
18
20
 
19
21
  Inheritors must implement `__len__`, `__getitem__`, and `__setitem__`.
@@ -32,6 +34,8 @@ class ArrayLike[T](Sequence, ABC):
32
34
  ```
33
35
  """
34
36
 
37
+ _allow_instance_check_ = True
38
+
35
39
  @abstractmethod
36
40
  def __len__(self) -> int:
37
41
  """Return the length of the array."""
@@ -300,3 +304,26 @@ class _ArrayEnumerator[V: ArrayLike](Record, SonolusIterator):
300
304
 
301
305
  def advance(self):
302
306
  self.i += 1
307
+
308
+
309
+ @meta_fn
310
+ def get_positive_index(index: Num, length: Num) -> Num:
311
+ """Get the positive index for the given index in the array of the given length.
312
+
313
+ This is used to convert negative indixes relative to the end of the array to positive indices.
314
+
315
+ Args:
316
+ index: The index to convert.
317
+ length: The length of the array.
318
+
319
+ Returns:
320
+ The positive index.
321
+ """
322
+ if not ctx():
323
+ return index if index >= 0 else index + length
324
+ index = Num._accept_(index)
325
+ length = Num._accept_(length)
326
+ if index._is_py_() and length._is_py_():
327
+ return Num._accept_(index._as_py_() + length._as_py_() if index._as_py_() < 0 else index._as_py_())
328
+ else:
329
+ return index + (index < 0) * length
@@ -2,15 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  from sonolus.backend.visitor import compile_and_call
4
4
  from sonolus.script.array import Array
5
- from sonolus.script.array_like import ArrayLike
5
+ from sonolus.script.array_like import ArrayLike, get_positive_index
6
6
  from sonolus.script.debug import error
7
7
  from sonolus.script.internal.context import ctx
8
8
  from sonolus.script.internal.impl import meta_fn
9
+ from sonolus.script.interval import clamp
9
10
  from sonolus.script.iterator import SonolusIterator
10
11
  from sonolus.script.num import Num
11
12
  from sonolus.script.pointer import _deref
12
13
  from sonolus.script.record import Record
13
- from sonolus.script.values import alloc, copy
14
+ from sonolus.script.values import copy, zeros
14
15
 
15
16
 
16
17
  class Box[T](Record):
@@ -104,7 +105,7 @@ class VarArray[T, Capacity](Record, ArrayLike[T]):
104
105
  """Create a new empty array."""
105
106
  element_type = cls.type_var_value(T)
106
107
  capacity = cls.type_var_value(Capacity)
107
- return cls(0, alloc(Array[element_type, capacity]))
108
+ return cls(0, zeros(Array[element_type, capacity]))
108
109
 
109
110
  def __len__(self) -> int:
110
111
  """Return the number of elements in the array."""
@@ -140,11 +141,11 @@ class VarArray[T, Capacity](Record, ArrayLike[T]):
140
141
  assert p == Pair(5, 6) # The value of p has changed
141
142
  ```
142
143
  """
143
- return self._array[item]
144
+ return self._array[get_positive_index(item, len(self))]
144
145
 
145
146
  def __setitem__(self, key: int, value: T):
146
147
  """Update the element at the given index."""
147
- self._array[key] = value
148
+ self._array[get_positive_index(key, len(self))] = value
148
149
 
149
150
  def __delitem__(self, key: int):
150
151
  """Remove the element at the given index."""
@@ -190,6 +191,7 @@ class VarArray[T, Capacity](Record, ArrayLike[T]):
190
191
  """
191
192
  if index is None:
192
193
  index = self._size - 1
194
+ index = get_positive_index(index, len(self))
193
195
  assert 0 <= index < self._size
194
196
  value = copy(self._array[index])
195
197
  self._size -= 1
@@ -207,7 +209,7 @@ class VarArray[T, Capacity](Record, ArrayLike[T]):
207
209
  index: The index at which to insert the value. Must be in the range [0, size].
208
210
  value: The value to insert.
209
211
  """
210
- assert 0 <= index <= self._size
212
+ index = clamp(get_positive_index(index, len(self)), 0, self._size)
211
213
  assert self._size < len(self._array)
212
214
  self._size += 1
213
215
  for i in range(self._size - 1, index, -1):
@@ -329,6 +331,7 @@ class ArrayPointer[T](Record, ArrayLike[T]):
329
331
 
330
332
  @meta_fn
331
333
  def _get_item(self, item: int) -> T:
334
+ item = get_positive_index(item, self.size)
332
335
  if not ctx():
333
336
  raise TypeError("ArrayPointer values cannot be accessed outside of a context")
334
337
  return _deref(
@@ -457,7 +460,7 @@ class ArrayMap[K, V, Capacity](Record):
457
460
  key_type = cls.type_var_value(K)
458
461
  value_type = cls.type_var_value(V)
459
462
  capacity = cls.type_var_value(Capacity)
460
- return cls(0, alloc(Array[_ArrayMapEntry[key_type, value_type], capacity]))
463
+ return cls(0, zeros(Array[_ArrayMapEntry[key_type, value_type], capacity]))
461
464
 
462
465
  def __len__(self) -> int:
463
466
  """Return the number of key-value pairs in the map."""
@@ -536,6 +539,23 @@ class ArrayMap[K, V, Capacity](Record):
536
539
  self._array[self._size] = _ArrayMapEntry(key, value)
537
540
  self._size += 1
538
541
 
542
+ def __delitem__(self, key: K):
543
+ """Remove the key-value pair associated with the given key.
544
+
545
+ Must be called with a key that is present in the map.
546
+
547
+ Args:
548
+ key: The key to remove
549
+ """
550
+ for i in range(self._size):
551
+ entry = self._array[i]
552
+ if entry.key == key:
553
+ self._size -= 1
554
+ if i < self._size:
555
+ self._array[i] = self._array[self._size]
556
+ return
557
+ error()
558
+
539
559
  def __contains__(self, key: K) -> bool:
540
560
  """Return whether the given key is present in the map.
541
561
 
sonolus/script/debug.py CHANGED
@@ -1,15 +1,25 @@
1
1
  from collections.abc import Callable, Sequence
2
2
  from contextvars import ContextVar
3
- from typing import Any, Never
3
+ from typing import Any, Literal, Never
4
4
 
5
5
  from sonolus.backend.mode import Mode
6
6
  from sonolus.backend.ops import Op
7
+ from sonolus.backend.optimize.constant_evaluation import SparseConditionalConstantPropagation
8
+ from sonolus.backend.optimize.copy_coalesce import CopyCoalesce
9
+ from sonolus.backend.optimize.dead_code import (
10
+ AdvancedDeadCodeElimination,
11
+ DeadCodeElimination,
12
+ UnreachableCodeElimination,
13
+ )
7
14
  from sonolus.backend.optimize.flow import cfg_to_mermaid
15
+ from sonolus.backend.optimize.inlining import InlineVars
8
16
  from sonolus.backend.optimize.passes import CompilerPass, run_passes
9
- from sonolus.backend.optimize.simplify import CoalesceFlow
10
- from sonolus.script.internal.context import GlobalContextState, ctx, set_ctx
17
+ from sonolus.backend.optimize.simplify import CoalesceFlow, NormalizeSwitch, RewriteToSwitch
18
+ from sonolus.backend.optimize.ssa import FromSSA, ToSSA
19
+ from sonolus.script.internal.context import GlobalContextState, ReadOnlyMemory, ctx, set_ctx
11
20
  from sonolus.script.internal.impl import meta_fn, validate_value
12
21
  from sonolus.script.internal.native import native_function
22
+ from sonolus.script.internal.simulation_context import SimulationContext
13
23
  from sonolus.script.num import Num
14
24
 
15
25
  debug_log_callback = ContextVar[Callable[[Num], None]]("debug_log_callback")
@@ -93,12 +103,60 @@ def terminate():
93
103
  raise RuntimeError("Terminated")
94
104
 
95
105
 
96
- def visualize_cfg(fn: Callable[[], Any], passes: Sequence[CompilerPass] | None = None) -> str:
106
+ def visualize_cfg(
107
+ fn: Callable[[], Any] | Callable[[type], Any],
108
+ /,
109
+ *,
110
+ mode: Mode = Mode.PLAY,
111
+ archetype: type | None = None,
112
+ archetypes: list[type] | None,
113
+ passes: Sequence[CompilerPass] | Literal["minimal", "basic", "standard"] = "basic",
114
+ ) -> str:
97
115
  from sonolus.build.compile import callback_to_cfg
98
116
 
99
- if passes is None:
100
- passes = [CoalesceFlow()]
101
-
102
- cfg = callback_to_cfg(GlobalContextState(Mode.PLAY), fn, "")
117
+ match passes:
118
+ case "minimal":
119
+ passes = [
120
+ CoalesceFlow(),
121
+ ]
122
+ case "basic":
123
+ passes = [
124
+ CoalesceFlow(),
125
+ UnreachableCodeElimination(),
126
+ AdvancedDeadCodeElimination(),
127
+ CoalesceFlow(),
128
+ ]
129
+ case "standard":
130
+ passes = [
131
+ CoalesceFlow(),
132
+ UnreachableCodeElimination(),
133
+ DeadCodeElimination(),
134
+ ToSSA(),
135
+ SparseConditionalConstantPropagation(),
136
+ UnreachableCodeElimination(),
137
+ DeadCodeElimination(),
138
+ CoalesceFlow(),
139
+ InlineVars(),
140
+ DeadCodeElimination(),
141
+ RewriteToSwitch(),
142
+ FromSSA(),
143
+ CoalesceFlow(),
144
+ CopyCoalesce(),
145
+ AdvancedDeadCodeElimination(),
146
+ CoalesceFlow(),
147
+ NormalizeSwitch(),
148
+ ]
149
+
150
+ global_state = GlobalContextState(
151
+ mode,
152
+ {a: i for i, a in enumerate(archetypes)} if archetypes is not None else None,
153
+ ReadOnlyMemory(),
154
+ )
155
+
156
+ cfg = callback_to_cfg(global_state, fn, "", archetype=archetype)
103
157
  cfg = run_passes(cfg, passes)
104
158
  return cfg_to_mermaid(cfg)
159
+
160
+
161
+ def simulation_context() -> SimulationContext:
162
+ return SimulationContext()
sonolus/script/globals.py CHANGED
@@ -5,6 +5,7 @@ from sonolus.backend.blocks import Block, PlayBlock, PreviewBlock, TutorialBlock
5
5
  from sonolus.backend.mode import Mode
6
6
  from sonolus.script.internal.descriptor import SonolusDescriptor
7
7
  from sonolus.script.internal.generic import validate_concrete_type
8
+ from sonolus.script.internal.simulation_context import sim_ctx
8
9
  from sonolus.script.internal.value import Value
9
10
 
10
11
 
@@ -27,6 +28,11 @@ class _GlobalField(SonolusDescriptor):
27
28
  if instance is None:
28
29
  return self
29
30
 
31
+ if sim_ctx():
32
+ from sonolus.script.values import zeros
33
+
34
+ return sim_ctx().get_or_put_value((instance, self), lambda: zeros(self.type))
35
+
30
36
  from sonolus.script.internal.context import ctx
31
37
 
32
38
  info = owner._global_info_
@@ -38,6 +44,12 @@ class _GlobalField(SonolusDescriptor):
38
44
  def __set__(self, instance, value):
39
45
  from sonolus.script.internal.context import ctx
40
46
 
47
+ if sim_ctx():
48
+ from sonolus.script.values import zeros
49
+
50
+ sim_ctx().set_or_put_value((instance, self), lambda: zeros(self.type), value)
51
+ return
52
+
41
53
  info = instance._global_info_
42
54
  if not ctx():
43
55
  raise RuntimeError("Global field access outside of compilation")
@@ -64,6 +76,11 @@ class _GlobalPlaceholder:
64
76
  base = ctx().get_global_base(self)
65
77
  return self.type._from_place_(base)
66
78
 
79
+ def _get_sim_replacement_(self):
80
+ from sonolus.script.values import zeros
81
+
82
+ return sim_ctx().get_or_put_value(self, lambda: zeros(self.type))
83
+
67
84
 
68
85
  def _create_global(cls: type, blocks: dict[Mode, Block], offset: int | None):
69
86
  if issubclass(cls, Value):
@@ -1,7 +1,7 @@
1
- from abc import ABC
2
1
  from collections.abc import Iterable
3
2
  from typing import overload
4
3
 
4
+ from sonolus.script.array import Array
5
5
  from sonolus.script.array_like import ArrayLike
6
6
  from sonolus.script.internal.context import ctx
7
7
  from sonolus.script.internal.dict_impl import DictImpl
@@ -32,7 +32,7 @@ def _isinstance(value, type_):
32
32
  return isinstance(value, TupleImpl)
33
33
  if type_ in {_int, _float, _bool}:
34
34
  raise TypeError("Instance check against int, float, or bool is not supported, use Num instead")
35
- if not (isinstance(type_, type) and issubclass(type_, Value | ABC)):
35
+ if not (isinstance(type_, type) and (issubclass(type_, Value) or getattr(type_, "_allow_instance_check_", False))):
36
36
  raise TypeError(f"Unsupported type: {type_} for isinstance")
37
37
  return validate_value(isinstance(value, type_))
38
38
 
@@ -87,7 +87,7 @@ def _zip(*iterables):
87
87
  if any(isinstance(iterable, TupleImpl) for iterable in iterables):
88
88
  if not all(isinstance(iterable, TupleImpl) for iterable in iterables):
89
89
  raise TypeError("Cannot mix tuples with other types in zip")
90
- return TupleImpl._accept_(tuple(zip(*(iterable._as_py_() for iterable in iterables), strict=False)))
90
+ return TupleImpl._accept_(tuple(zip(*(iterable.value for iterable in iterables), strict=False)))
91
91
  iterators = [compile_and_call(iterable.__iter__) for iterable in iterables]
92
92
  if not all(isinstance(iterator, SonolusIterator) for iterator in iterators):
93
93
  raise TypeError("Only subclasses of SonolusIterator are supported as iterators")
@@ -130,6 +130,8 @@ def _max(*args, key: callable = _identity):
130
130
  (iterable,) = args
131
131
  if isinstance(iterable, ArrayLike):
132
132
  return compile_and_call(iterable._max_, key=key)
133
+ elif isinstance(iterable, TupleImpl) and all(_is_num(v) for v in iterable.value):
134
+ return compile_and_call(Array(*iterable.value)._max_, key=key)
133
135
  else:
134
136
  raise TypeError(f"Unsupported type: {type(iterable)} for max")
135
137
  else:
@@ -170,6 +172,8 @@ def _min(*args, key: callable = _identity):
170
172
  (iterable,) = args
171
173
  if isinstance(iterable, ArrayLike):
172
174
  return compile_and_call(iterable._min_, key=key)
175
+ elif isinstance(iterable, TupleImpl) and all(_is_num(v) for v in iterable.value):
176
+ return compile_and_call(Array(*iterable.value)._min_, key=key)
173
177
  else:
174
178
  raise TypeError(f"Unsupported type: {type(iterable)} for min")
175
179
  else:
@@ -222,12 +226,12 @@ def _float(value=0.0):
222
226
  return value
223
227
 
224
228
 
225
- @meta_fn
226
229
  def _bool(value=False):
227
- value = validate_value(value)
228
- if not _is_num(value):
229
- raise TypeError("Only numeric arguments to bool() are supported")
230
- return value != 0
230
+ # Relies on the compiler to perform the conversion in a boolean context
231
+ if value: # noqa: SIM103
232
+ return True
233
+ else:
234
+ return False
231
235
 
232
236
 
233
237
  _int._type_mapping_ = Num