sonolus.py 0.3.4__py3-none-any.whl → 0.4.1__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 (64) 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 +44 -16
  18. sonolus/backend/visitor.py +288 -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 +53 -107
  45. sonolus/script/level.py +2 -2
  46. sonolus/script/maybe.py +241 -0
  47. sonolus/script/num.py +29 -14
  48. sonolus/script/options.py +1 -1
  49. sonolus/script/particle.py +1 -1
  50. sonolus/script/project.py +24 -5
  51. sonolus/script/quad.py +15 -15
  52. sonolus/script/record.py +48 -44
  53. sonolus/script/runtime.py +22 -18
  54. sonolus/script/sprite.py +1 -1
  55. sonolus/script/stream.py +66 -82
  56. sonolus/script/transform.py +35 -34
  57. sonolus/script/values.py +10 -10
  58. sonolus/script/vec.py +21 -18
  59. {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.1.dist-info}/METADATA +1 -1
  60. sonolus_py-0.4.1.dist-info/RECORD +93 -0
  61. sonolus_py-0.3.4.dist-info/RECORD +0 -92
  62. {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.1.dist-info}/WHEEL +0 -0
  63. {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.1.dist-info}/entry_points.txt +0 -0
  64. {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.1.dist-info}/licenses/LICENSE +0 -0
sonolus/build/engine.py CHANGED
@@ -6,7 +6,6 @@ from collections.abc import Callable
6
6
  from concurrent.futures import Executor
7
7
  from concurrent.futures.thread import ThreadPoolExecutor
8
8
  from dataclasses import dataclass
9
- from os import process_cpu_count
10
9
  from pathlib import Path
11
10
 
12
11
  from sonolus.backend.mode import Mode
@@ -74,6 +73,9 @@ def package_engine(
74
73
  rom = ReadOnlyMemory()
75
74
  configuration = build_engine_configuration(engine.options, engine.ui)
76
75
  if no_gil():
76
+ # process_cpu_count is available in Python 3.13+
77
+ from os import process_cpu_count
78
+
77
79
  thread_pool = ThreadPoolExecutor(process_cpu_count() or 1)
78
80
  else:
79
81
  thread_pool = None
@@ -189,6 +191,65 @@ def package_engine(
189
191
  )
190
192
 
191
193
 
194
+ def validate_engine(
195
+ engine: EngineData,
196
+ config: BuildConfig | None = None,
197
+ ):
198
+ config = config or BuildConfig()
199
+ rom = ReadOnlyMemory()
200
+
201
+ play_mode = engine.play if config.build_play else empty_play_mode()
202
+ watch_mode = engine.watch if config.build_watch else empty_watch_mode()
203
+ preview_mode = engine.preview if config.build_preview else empty_preview_mode()
204
+ tutorial_mode = engine.tutorial if config.build_tutorial else empty_tutorial_mode()
205
+
206
+ build_play_mode(
207
+ archetypes=play_mode.archetypes,
208
+ skin=play_mode.skin,
209
+ effects=play_mode.effects,
210
+ particles=play_mode.particles,
211
+ buckets=play_mode.buckets,
212
+ rom=rom,
213
+ config=config,
214
+ thread_pool=None,
215
+ validate_only=True,
216
+ )
217
+ build_watch_mode(
218
+ archetypes=watch_mode.archetypes,
219
+ skin=watch_mode.skin,
220
+ effects=watch_mode.effects,
221
+ particles=watch_mode.particles,
222
+ buckets=watch_mode.buckets,
223
+ rom=rom,
224
+ update_spawn=watch_mode.update_spawn,
225
+ config=config,
226
+ thread_pool=None,
227
+ validate_only=True,
228
+ )
229
+ build_preview_mode(
230
+ archetypes=preview_mode.archetypes,
231
+ skin=preview_mode.skin,
232
+ rom=rom,
233
+ config=config,
234
+ thread_pool=None,
235
+ validate_only=True,
236
+ )
237
+ build_tutorial_mode(
238
+ skin=tutorial_mode.skin,
239
+ effects=tutorial_mode.effects,
240
+ particles=tutorial_mode.particles,
241
+ instructions=tutorial_mode.instructions,
242
+ instruction_icons=tutorial_mode.instruction_icons,
243
+ preprocess=tutorial_mode.preprocess,
244
+ navigate=tutorial_mode.navigate,
245
+ update=tutorial_mode.update,
246
+ rom=rom,
247
+ config=config,
248
+ thread_pool=None,
249
+ validate_only=True,
250
+ )
251
+
252
+
192
253
  def build_engine_configuration(
193
254
  options: Options,
194
255
  ui: UiConfig,
@@ -208,6 +269,7 @@ def build_play_mode(
208
269
  rom: ReadOnlyMemory,
209
270
  config: BuildConfig,
210
271
  thread_pool: Executor | None = None,
272
+ validate_only: bool = False,
211
273
  ):
212
274
  return {
213
275
  **compile_mode(
@@ -217,6 +279,7 @@ def build_play_mode(
217
279
  global_callbacks=None,
218
280
  passes=config.passes,
219
281
  thread_pool=thread_pool,
282
+ validate_only=validate_only,
220
283
  ),
221
284
  "skin": build_skin(skin),
222
285
  "effect": build_effects(effects),
@@ -235,6 +298,7 @@ def build_watch_mode(
235
298
  update_spawn: Callable[[], float],
236
299
  config: BuildConfig,
237
300
  thread_pool: Executor | None = None,
301
+ validate_only: bool = False,
238
302
  ):
239
303
  return {
240
304
  **compile_mode(
@@ -244,6 +308,7 @@ def build_watch_mode(
244
308
  global_callbacks=[(update_spawn_callback, update_spawn)],
245
309
  passes=config.passes,
246
310
  thread_pool=thread_pool,
311
+ validate_only=validate_only,
247
312
  ),
248
313
  "skin": build_skin(skin),
249
314
  "effect": build_effects(effects),
@@ -258,6 +323,7 @@ def build_preview_mode(
258
323
  rom: ReadOnlyMemory,
259
324
  config: BuildConfig,
260
325
  thread_pool: Executor | None = None,
326
+ validate_only: bool = False,
261
327
  ):
262
328
  return {
263
329
  **compile_mode(
@@ -267,6 +333,7 @@ def build_preview_mode(
267
333
  global_callbacks=None,
268
334
  passes=config.passes,
269
335
  thread_pool=thread_pool,
336
+ validate_only=validate_only,
270
337
  ),
271
338
  "skin": build_skin(skin),
272
339
  }
@@ -284,6 +351,7 @@ def build_tutorial_mode(
284
351
  rom: ReadOnlyMemory,
285
352
  config: BuildConfig,
286
353
  thread_pool: Executor | None = None,
354
+ validate_only: bool = False,
287
355
  ):
288
356
  return {
289
357
  **compile_mode(
@@ -297,6 +365,7 @@ def build_tutorial_mode(
297
365
  ],
298
366
  passes=config.passes,
299
367
  thread_pool=thread_pool,
368
+ validate_only=validate_only,
300
369
  ),
301
370
  "skin": build_skin(skin),
302
371
  "effect": build_effects(effects),
sonolus/build/level.py CHANGED
@@ -1,16 +1,16 @@
1
- from sonolus.build.engine import package_output
1
+ from sonolus.build.engine import JsonValue, package_output
2
2
  from sonolus.script.level import LevelData
3
3
 
4
4
 
5
5
  def package_level_data(
6
6
  level_data: LevelData,
7
- ):
7
+ ) -> bytes:
8
8
  return package_output(build_level_data(level_data))
9
9
 
10
10
 
11
11
  def build_level_data(
12
12
  level_data: LevelData,
13
- ):
13
+ ) -> JsonValue:
14
14
  level_refs = {entity: f"{i}_{entity.name}" for i, entity in enumerate(level_data.entities)}
15
15
  return {
16
16
  "bgmOffset": level_data.bgm_offset,
sonolus/build/project.py CHANGED
@@ -17,7 +17,7 @@ BLANK_AUDIO = (
17
17
  )
18
18
 
19
19
 
20
- def build_project_to_collection(project: Project, config: BuildConfig):
20
+ def build_project_to_collection(project: Project, config: BuildConfig | None):
21
21
  collection = load_resources_files_to_collection(project.resources)
22
22
  add_engine_to_collection(collection, project, project.engine, config)
23
23
  for level in project.levels:
@@ -26,7 +26,7 @@ def build_project_to_collection(project: Project, config: BuildConfig):
26
26
  return collection
27
27
 
28
28
 
29
- def add_engine_to_collection(collection: Collection, project: Project, engine: Engine, config: BuildConfig):
29
+ def add_engine_to_collection(collection: Collection, project: Project, engine: Engine, config: BuildConfig | None):
30
30
  packaged_engine = package_engine(engine.data, config)
31
31
  item = {
32
32
  "name": engine.name,
@@ -1,3 +1,4 @@
1
+ # type: ignore
1
2
  from __future__ import annotations
2
3
 
3
4
  import inspect
@@ -117,7 +118,7 @@ class _ArchetypeField(SonolusDescriptor):
117
118
  if result is None:
118
119
  raise RuntimeError("Invalid storage type")
119
120
  if ctx():
120
- return result._get_()
121
+ return result._get_readonly_()
121
122
  else:
122
123
  return result._as_py_()
123
124
 
@@ -327,7 +328,7 @@ def callback[T: Callable](*, order: int = 0) -> Callable[[T], T]:
327
328
  """
328
329
 
329
330
  def decorator(func: T) -> T:
330
- func._callback_order_ = order
331
+ func._callback_order_ = order # type: ignore
331
332
  return func
332
333
 
333
334
  return decorator
@@ -635,11 +636,11 @@ class _BaseArchetype:
635
636
  """
636
637
  match self._data_:
637
638
  case _ArchetypeSelfData():
638
- return EntityRef[type(self)](index=self.index)
639
+ return EntityRef[type(self)](index=self.index) # type: ignore
639
640
  case _ArchetypeReferenceData(index=index):
640
- return EntityRef[type(self)](index=index)
641
+ return EntityRef[type(self)](index=index) # type: ignore
641
642
  case _ArchetypeLevelData():
642
- result = EntityRef[type(self)](index=-1)
643
+ result = EntityRef[type(self)](index=-1) # type: ignore
643
644
  result._ref_ = self
644
645
  return result
645
646
  case _:
@@ -1016,8 +1017,8 @@ def archetype_life_of(archetype: type[_BaseArchetype] | _BaseArchetype) -> Arche
1016
1017
 
1017
1018
  Available in play and watch mode.
1018
1019
  """
1019
- archetype = validate_value(archetype)
1020
- archetype = archetype._as_py_()
1020
+ archetype = validate_value(archetype) # type: ignore
1021
+ archetype = archetype._as_py_() # type: ignore
1021
1022
  if not ctx():
1022
1023
  raise RuntimeError("Calling archetype_life_of is only allowed within a callback")
1023
1024
  match ctx().global_state.mode:
@@ -1110,7 +1111,7 @@ class EntityRef[A: _BaseArchetype](Record):
1110
1111
  """Get the archetype type."""
1111
1112
  return cls.type_var_value(A)
1112
1113
 
1113
- def with_archetype(self, archetype: type[A]) -> EntityRef[A]:
1114
+ def with_archetype[T: _BaseArchetype](self, archetype: type[T]) -> EntityRef[T]:
1114
1115
  """Return a new reference with the given archetype type."""
1115
1116
  return EntityRef[archetype](index=self.index)
1116
1117
 
@@ -1137,11 +1138,13 @@ class EntityRef[A: _BaseArchetype](Record):
1137
1138
  if ref is None:
1138
1139
  return Num._accept_(self.index)._to_list_()
1139
1140
  else:
1141
+ if level_refs is None:
1142
+ raise RuntimeError("Unexpected missing level_refs")
1140
1143
  if ref not in level_refs:
1141
1144
  raise KeyError("Reference to entity not in level data")
1142
1145
  return [level_refs[ref]]
1143
1146
 
1144
- def _copy_from_(self, value: Self):
1147
+ def _copy_from_(self, value: Any):
1145
1148
  super()._copy_from_(value)
1146
1149
  if hasattr(value, "_ref_"):
1147
1150
  self._ref_ = value._ref_
sonolus/script/array.py CHANGED
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from abc import ABCMeta
3
4
  from collections.abc import Iterable
4
- from typing import Any, Self, final
5
+ from typing import Any, Literal, Self, final
5
6
 
6
7
  from sonolus.backend.ir import IRConst, IRSet
7
8
  from sonolus.backend.place import BlockPlace
@@ -14,8 +15,11 @@ from sonolus.script.internal.impl import meta_fn, validate_value
14
15
  from sonolus.script.internal.value import BackingSource, DataValue, Value
15
16
  from sonolus.script.num import Num
16
17
 
18
+ Dim = Literal
19
+ """Shorthand for `Literal` intended for use in array dimensions for type checker compatibility."""
17
20
 
18
- class ArrayMeta(type):
21
+
22
+ class ArrayMeta(ABCMeta):
19
23
  @meta_fn
20
24
  def __pos__[T](cls: type[T]) -> T:
21
25
  """Create a zero-initialized array instance."""
@@ -31,7 +35,8 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
31
35
  array_1 = Array(1, 2, 3)
32
36
  array_2 = Array[int, 0]()
33
37
  array_3 = +Array[int, 3] # Create a zero-initialized array
34
- array_4 = +array_1 # Create a copy of array_1
38
+ array_4 = +Array[int, Dim[3]] # Alternative syntax for compliance with type checkers
39
+ array_5 = +array_1 # Create a copy of array_1
35
40
  ```
36
41
  """
37
42
 
@@ -78,7 +83,7 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
78
83
  super().__init__()
79
84
 
80
85
  @classmethod
81
- def _with_value(cls, value) -> Self:
86
+ def _with_value(cls, value) -> Array[T, Size]:
82
87
  result = object.__new__(cls)
83
88
  result._value = value
84
89
  return result
@@ -92,11 +97,11 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
92
97
  return False
93
98
 
94
99
  @classmethod
95
- def _from_backing_source_(cls, source: BackingSource) -> Self:
100
+ def _from_backing_source_(cls, source: BackingSource) -> Array[T, Size]:
96
101
  return cls._with_value(source)
97
102
 
98
103
  @classmethod
99
- def _from_place_(cls, place: BlockPlace) -> Self:
104
+ def _from_place_(cls, place: BlockPlace) -> Array[T, Size]:
100
105
  return cls._with_value(place)
101
106
 
102
107
  @classmethod
@@ -104,7 +109,7 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
104
109
  return isinstance(value, cls)
105
110
 
106
111
  @classmethod
107
- def _accept_(cls, value: Any) -> Self:
112
+ def _accept_(cls, value: Any) -> Array[T, Size]:
108
113
  if not cls._accepts_(value):
109
114
  raise TypeError(f"Cannot accept value {value} as {cls.__name__}")
110
115
  return value
@@ -118,7 +123,7 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
118
123
  return self
119
124
 
120
125
  @classmethod
121
- def _from_list_(cls, values: Iterable[DataValue]) -> Self:
126
+ def _from_list_(cls, values: Iterable[DataValue]) -> Array[T, Size]:
122
127
  iterator = iter(values)
123
128
  return cls(*(cls.element_type()._from_list_(iterator) for _ in range(cls.size())))
124
129
 
@@ -143,19 +148,19 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
143
148
  def _flat_keys_(cls, prefix: str) -> list[str]:
144
149
  return [entry for i in range(cls.size()) for entry in cls.element_type()._flat_keys_(f"{prefix}[{i}]")]
145
150
 
146
- def _get_(self) -> Self:
151
+ def _get_(self) -> Array[T, Size]:
147
152
  return self
148
153
 
149
- def _set_(self, value: Self):
154
+ def _set_(self, value: Any):
150
155
  raise TypeError("Array does not support _set_")
151
156
 
152
- def _copy_from_(self, value: Self):
157
+ def _copy_from_(self, value: Any):
153
158
  if not isinstance(value, type(self)):
154
159
  raise TypeError("Cannot copy from different type")
155
160
  for i in range(self.size()):
156
161
  self[i] = value[i]
157
162
 
158
- def _copy_(self) -> Self:
163
+ def _copy_(self) -> Array[T, Size]:
159
164
  if ctx():
160
165
  place = ctx().alloc(size=self._size_())
161
166
  result: Self = self._from_place_(place)
@@ -166,7 +171,7 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
166
171
  return self._with_value([value._copy_() for value in self._value])
167
172
 
168
173
  @classmethod
169
- def _alloc_(cls) -> Self:
174
+ def _alloc_(cls) -> Array[T, Size]:
170
175
  if ctx():
171
176
  place = ctx().alloc(size=cls._size_())
172
177
  return cls._from_place_(place)
@@ -174,7 +179,7 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
174
179
  return cls._with_value([cls.element_type()._alloc_() for _ in range(cls.size())])
175
180
 
176
181
  @classmethod
177
- def _zero_(cls) -> Self:
182
+ def _zero_(cls) -> Array[T, Size]:
178
183
  if ctx():
179
184
  place = ctx().alloc(size=cls._size_())
180
185
  result: Self = cls._from_place_(place)
@@ -207,7 +212,7 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
207
212
  )
208
213
  elif callable(self._value):
209
214
  return self.element_type()._from_backing_source_(
210
- lambda offset: self._value((Num(offset) + Num(const_index * self.element_type()._size_())).ir())
215
+ lambda offset: self._value((Num(offset) + Num(const_index * self.element_type()._size_())).ir()) # type: ignore
211
216
  )
212
217
  else:
213
218
  raise InternalError("Unexpected array value")
@@ -225,7 +230,7 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
225
230
  elif callable(self._value):
226
231
  base_offset = index * Num(self.element_type()._size_())
227
232
  return self.element_type()._from_backing_source_(
228
- lambda offset: self._value((Num(offset) + base_offset).ir())
233
+ lambda offset: self._value((Num(offset) + base_offset).ir()) # type: ignore
229
234
  )
230
235
  else:
231
236
  raise InternalError("Unexpected array value")
@@ -252,7 +257,7 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
252
257
  elif callable(self._value):
253
258
  base_offset = index * Num(self.element_type()._size_())
254
259
  dst = self.element_type()._from_backing_source_(
255
- lambda offset: self._value((Num(offset) + base_offset).ir())
260
+ lambda offset: self._value((Num(offset) + base_offset).ir()) # type: ignore
256
261
  )
257
262
  else:
258
263
  raise InternalError("Unexpected array value")
@@ -306,6 +311,6 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
306
311
  return f"{type(self).__name__}({', '.join(repr(self[i]) for i in range(self.size()))})"
307
312
 
308
313
  @meta_fn
309
- def __pos__(self) -> Self:
314
+ def __pos__(self) -> Array[T, Size]:
310
315
  """Return a copy of the array."""
311
316
  return self._copy_()
@@ -2,12 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import random
4
4
  from abc import abstractmethod
5
- from collections.abc import Callable
5
+ from collections.abc import Callable, Sequence
6
6
  from typing import Any
7
7
 
8
8
  from sonolus.script.internal.context import ctx
9
9
  from sonolus.script.internal.impl import meta_fn
10
10
  from sonolus.script.iterator import SonolusIterator
11
+ from sonolus.script.maybe import Maybe, Nothing, Some
11
12
  from sonolus.script.num import Num
12
13
  from sonolus.script.record import Record
13
14
  from sonolus.script.values import copy
@@ -15,7 +16,7 @@ from sonolus.script.values import copy
15
16
  # Note: we don't use Range in this file because Range itself inherits from ArrayLike
16
17
 
17
18
 
18
- class ArrayLike[T]:
19
+ class ArrayLike[T](Sequence):
19
20
  """Mixin for array-like objects.
20
21
 
21
22
  Inheritors must implement `__len__`, `__getitem__`, and `__setitem__`.
@@ -134,11 +135,11 @@ class ArrayLike[T]:
134
135
  if len(self) == 0:
135
136
  return -1
136
137
  if key is None:
137
- key = _identity
138
+ key = _identity # type: ignore
138
139
  max_index = 0
139
140
  i = 1
140
141
  while i < len(self):
141
- if key(self[i]) > key(self[max_index]):
142
+ if key(self[i]) > key(self[max_index]): # type: ignore
142
143
  max_index = i
143
144
  i += 1
144
145
  return max_index
@@ -152,11 +153,11 @@ class ArrayLike[T]:
152
153
  if len(self) == 0:
153
154
  return -1
154
155
  if key is None:
155
- key = _identity
156
+ key = _identity # type: ignore
156
157
  min_index = 0
157
158
  i = 1
158
159
  while i < len(self):
159
- if key(self[i]) < key(self[min_index]):
160
+ if key(self[i]) < key(self[min_index]): # type: ignore
160
161
  min_index = i
161
162
  i += 1
162
163
  return min_index
@@ -191,12 +192,12 @@ class ArrayLike[T]:
191
192
  """
192
193
  if len(self) < 15 or key is not None:
193
194
  if key is None:
194
- key = _identity
195
+ key = _identity # type: ignore
195
196
  # May be worth adding a block sort variant for better performance on large arrays in the future
196
- _insertion_sort(self, 0, len(self), key, reverse)
197
+ _insertion_sort(self, 0, len(self), key, reverse) # type: ignore
197
198
  else:
198
199
  # Heap sort is unstable, so if there's a key, we can't rely on it
199
- _heap_sort(self, 0, len(self), reverse)
200
+ _heap_sort(self, 0, len(self), reverse) # type: ignore
200
201
 
201
202
  def shuffle(self):
202
203
  """Shuffle the values in the array in place."""
@@ -221,14 +222,14 @@ def _insertion_sort[T](array: ArrayLike[T], start: int, end: int, key: Callable[
221
222
  if reverse:
222
223
  while i < end:
223
224
  j = i
224
- while j > start and key(array[j - 1]) < key(array[j]):
225
+ while j > start and key(array[j - 1]) < key(array[j]): # type: ignore
225
226
  array.swap(j - 1, j)
226
227
  j -= 1
227
228
  i += 1
228
229
  else:
229
230
  while i < end:
230
231
  j = i
231
- while j > start and key(array[j - 1]) > key(array[j]):
232
+ while j > start and key(array[j - 1]) > key(array[j]): # type: ignore
232
233
  array.swap(j - 1, j)
233
234
  j -= 1
234
235
  i += 1
@@ -239,9 +240,9 @@ def _heapify[T](array: ArrayLike[T], end: int, index: int, reverse: bool):
239
240
  left = index * 2 + 1
240
241
  right = left + 1
241
242
  largest = index
242
- if left < end and (array[left] > array[largest]) != reverse:
243
+ if left < end and (array[left] > array[largest]) != reverse: # type: ignore
243
244
  largest = left
244
- if right < end and (array[right] > array[largest]) != reverse:
245
+ if right < end and (array[right] > array[largest]) != reverse: # type: ignore
245
246
  largest = right
246
247
  if largest == index:
247
248
  break
@@ -265,14 +266,12 @@ class _ArrayIterator[V: ArrayLike](Record, SonolusIterator):
265
266
  i: int
266
267
  array: V
267
268
 
268
- def has_next(self) -> bool:
269
- return self.i < len(self.array)
270
-
271
- def get(self) -> V:
272
- return self.array[self.i]
273
-
274
- def advance(self):
275
- self.i += 1
269
+ def next(self) -> Maybe[V]:
270
+ if self.i < len(self.array):
271
+ value = self.array[self.i]
272
+ self.i += 1
273
+ return Some(value)
274
+ return Nothing
276
275
 
277
276
 
278
277
  class _ArrayReverser[V: ArrayLike](Record, ArrayLike):
@@ -296,14 +295,12 @@ class _ArrayEnumerator[V: ArrayLike](Record, SonolusIterator):
296
295
  offset: int
297
296
  array: V
298
297
 
299
- def has_next(self) -> bool:
300
- return self.i < len(self.array)
301
-
302
- def get(self) -> tuple[int, Any]:
303
- return self.i + self.offset, self.array[self.i]
304
-
305
- def advance(self):
306
- self.i += 1
298
+ def next(self) -> Maybe[tuple[int, Any]]:
299
+ if self.i < len(self.array):
300
+ result = (self.i + self.offset, self.array[self.i])
301
+ self.i += 1
302
+ return Some(result)
303
+ return Nothing
307
304
 
308
305
 
309
306
  @meta_fn
sonolus/script/bucket.py CHANGED
@@ -220,7 +220,7 @@ def bucket(*, sprites: list[_BucketSprite], unit: str | None = None) -> Any:
220
220
  return _BucketInfo(sprites, unit)
221
221
 
222
222
 
223
- type Buckets = NewType("Buckets", Any)
223
+ type Buckets = NewType("Buckets", Any) # type: ignore
224
224
 
225
225
 
226
226
  @dataclass_transform()
@@ -8,6 +8,7 @@ from sonolus.script.internal.context import ctx
8
8
  from sonolus.script.internal.impl import meta_fn
9
9
  from sonolus.script.interval import clamp
10
10
  from sonolus.script.iterator import SonolusIterator
11
+ from sonolus.script.maybe import Maybe, Nothing, Some
11
12
  from sonolus.script.num import Num
12
13
  from sonolus.script.pointer import _deref
13
14
  from sonolus.script.record import Record
@@ -335,7 +336,8 @@ class ArrayPointer[T](Record, ArrayLike[T]):
335
336
  if not ctx():
336
337
  raise TypeError("ArrayPointer values cannot be accessed outside of a context")
337
338
  return _deref(
338
- self.block,
339
+ # Allows a compile time constant block so we can warn based on callback read/write access
340
+ (self._value_["block"]._is_py_() and self._value_["block"]._as_py_()) or self.block,
339
341
  self.offset + Num._accept_(item) * Num._accept_(self.element_type()._size_()),
340
342
  self.element_type(),
341
343
  )
@@ -600,40 +602,34 @@ class _ArrayMapKeyIterator[K, V, Capacity](Record, SonolusIterator):
600
602
  _map: ArrayMap[K, V, Capacity]
601
603
  _index: int
602
604
 
603
- def has_next(self) -> bool:
604
- return self._index < len(self._map)
605
-
606
- def get(self) -> K:
607
- return self._map._array[self._index].key
608
-
609
- def advance(self):
610
- self._index += 1
605
+ def next(self) -> Maybe[K]:
606
+ if self._index < len(self._map):
607
+ key = self._map._array[self._index].key
608
+ self._index += 1
609
+ return Some(key)
610
+ return Nothing
611
611
 
612
612
 
613
613
  class _ArrayMapValueIterator[K, V, Capacity](Record, SonolusIterator):
614
614
  _map: ArrayMap[K, V, Capacity]
615
615
  _index: int
616
616
 
617
- def has_next(self) -> bool:
618
- return self._index < len(self._map)
619
-
620
- def get(self) -> V:
621
- return self._map._array[self._index].value
622
-
623
- def advance(self):
624
- self._index += 1
617
+ def next(self) -> Maybe[V]:
618
+ if self._index < len(self._map):
619
+ value = self._map._array[self._index].value
620
+ self._index += 1
621
+ return Some(value)
622
+ return Nothing
625
623
 
626
624
 
627
625
  class _ArrayMapEntryIterator[K, V, Capacity](Record, SonolusIterator):
628
626
  _map: ArrayMap[K, V, Capacity]
629
627
  _index: int
630
628
 
631
- def has_next(self) -> bool:
632
- return self._index < len(self._map)
633
-
634
- def get(self) -> tuple[K, V]:
635
- entry = self._map._array[self._index]
636
- return entry.key, entry.value
637
-
638
- def advance(self):
639
- self._index += 1
629
+ def next(self) -> Maybe[tuple[K, V]]:
630
+ if self._index < len(self._map):
631
+ entry = self._map._array[self._index]
632
+ result = (entry.key, entry.value)
633
+ self._index += 1
634
+ return Some(result)
635
+ return Nothing