sonolus.py 0.9.3__py3-none-any.whl → 0.10.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.
- sonolus/backend/optimize/constant_evaluation.py +4 -2
- sonolus/backend/optimize/optimize.py +2 -0
- sonolus/backend/optimize/simplify.py +15 -0
- sonolus/backend/visitor.py +46 -21
- sonolus/build/cli.py +10 -8
- sonolus/build/compile.py +12 -11
- sonolus/build/dev_server.py +143 -32
- sonolus/build/engine.py +26 -24
- sonolus/build/project.py +8 -3
- sonolus/script/archetype.py +8 -8
- sonolus/script/array.py +33 -4
- sonolus/script/array_like.py +141 -26
- sonolus/script/bucket.py +1 -1
- sonolus/script/containers.py +40 -38
- sonolus/script/debug.py +56 -8
- sonolus/script/effect.py +5 -2
- sonolus/script/internal/builtin_impls.py +4 -4
- sonolus/script/internal/context.py +58 -27
- sonolus/script/internal/math_impls.py +19 -19
- sonolus/script/internal/native.py +7 -1
- sonolus/script/internal/range.py +3 -0
- sonolus/script/interval.py +6 -4
- sonolus/script/options.py +2 -2
- sonolus/script/particle.py +5 -2
- sonolus/script/project.py +2 -5
- sonolus/script/runtime.py +37 -37
- sonolus/script/sprite.py +6 -2
- sonolus/script/stream.py +3 -3
- sonolus/script/transform.py +1 -0
- sonolus/script/vec.py +1 -0
- {sonolus_py-0.9.3.dist-info → sonolus_py-0.10.1.dist-info}/METADATA +1 -1
- {sonolus_py-0.9.3.dist-info → sonolus_py-0.10.1.dist-info}/RECORD +35 -35
- {sonolus_py-0.9.3.dist-info → sonolus_py-0.10.1.dist-info}/WHEEL +0 -0
- {sonolus_py-0.9.3.dist-info → sonolus_py-0.10.1.dist-info}/entry_points.txt +0 -0
- {sonolus_py-0.9.3.dist-info → sonolus_py-0.10.1.dist-info}/licenses/LICENSE +0 -0
sonolus/build/engine.py
CHANGED
|
@@ -24,7 +24,7 @@ from sonolus.script.internal.callbacks import (
|
|
|
24
24
|
update_callback,
|
|
25
25
|
update_spawn_callback,
|
|
26
26
|
)
|
|
27
|
-
from sonolus.script.internal.context import ReadOnlyMemory
|
|
27
|
+
from sonolus.script.internal.context import ProjectContextState, ReadOnlyMemory
|
|
28
28
|
from sonolus.script.options import Options
|
|
29
29
|
from sonolus.script.particle import Particles
|
|
30
30
|
from sonolus.script.project import BuildConfig
|
|
@@ -69,12 +69,14 @@ def package_engine(
|
|
|
69
69
|
engine: EngineData,
|
|
70
70
|
config: BuildConfig | None = None,
|
|
71
71
|
cache: CompileCache | None = None,
|
|
72
|
+
project_state: ProjectContextState | None = None,
|
|
72
73
|
):
|
|
73
74
|
if cache is None:
|
|
74
75
|
cache = CompileCache()
|
|
75
76
|
|
|
76
77
|
config = config or BuildConfig()
|
|
77
|
-
|
|
78
|
+
if project_state is None:
|
|
79
|
+
project_state = ProjectContextState()
|
|
78
80
|
configuration = build_engine_configuration(engine.options, engine.ui)
|
|
79
81
|
if no_gil():
|
|
80
82
|
# process_cpu_count is available in Python 3.13+
|
|
@@ -99,7 +101,7 @@ def package_engine(
|
|
|
99
101
|
effects=play_mode.effects,
|
|
100
102
|
particles=play_mode.particles,
|
|
101
103
|
buckets=play_mode.buckets,
|
|
102
|
-
|
|
104
|
+
project_state=project_state,
|
|
103
105
|
config=config,
|
|
104
106
|
thread_pool=thread_pool,
|
|
105
107
|
cache=cache,
|
|
@@ -111,7 +113,7 @@ def package_engine(
|
|
|
111
113
|
effects=watch_mode.effects,
|
|
112
114
|
particles=watch_mode.particles,
|
|
113
115
|
buckets=watch_mode.buckets,
|
|
114
|
-
|
|
116
|
+
project_state=project_state,
|
|
115
117
|
update_spawn=watch_mode.update_spawn,
|
|
116
118
|
config=config,
|
|
117
119
|
thread_pool=thread_pool,
|
|
@@ -121,7 +123,7 @@ def package_engine(
|
|
|
121
123
|
build_preview_mode,
|
|
122
124
|
archetypes=preview_mode.archetypes,
|
|
123
125
|
skin=preview_mode.skin,
|
|
124
|
-
|
|
126
|
+
project_state=project_state,
|
|
125
127
|
config=config,
|
|
126
128
|
thread_pool=thread_pool,
|
|
127
129
|
cache=cache,
|
|
@@ -136,7 +138,7 @@ def package_engine(
|
|
|
136
138
|
preprocess=tutorial_mode.preprocess,
|
|
137
139
|
navigate=tutorial_mode.navigate,
|
|
138
140
|
update=tutorial_mode.update,
|
|
139
|
-
|
|
141
|
+
project_state=project_state,
|
|
140
142
|
config=config,
|
|
141
143
|
thread_pool=thread_pool,
|
|
142
144
|
cache=cache,
|
|
@@ -154,7 +156,7 @@ def package_engine(
|
|
|
154
156
|
effects=play_mode.effects,
|
|
155
157
|
particles=play_mode.particles,
|
|
156
158
|
buckets=play_mode.buckets,
|
|
157
|
-
|
|
159
|
+
project_state=project_state,
|
|
158
160
|
config=config,
|
|
159
161
|
thread_pool=None,
|
|
160
162
|
cache=cache,
|
|
@@ -165,7 +167,7 @@ def package_engine(
|
|
|
165
167
|
effects=watch_mode.effects,
|
|
166
168
|
particles=watch_mode.particles,
|
|
167
169
|
buckets=watch_mode.buckets,
|
|
168
|
-
|
|
170
|
+
project_state=project_state,
|
|
169
171
|
update_spawn=watch_mode.update_spawn,
|
|
170
172
|
config=config,
|
|
171
173
|
thread_pool=None,
|
|
@@ -174,7 +176,7 @@ def package_engine(
|
|
|
174
176
|
preview_data = build_preview_mode(
|
|
175
177
|
archetypes=preview_mode.archetypes,
|
|
176
178
|
skin=preview_mode.skin,
|
|
177
|
-
|
|
179
|
+
project_state=project_state,
|
|
178
180
|
config=config,
|
|
179
181
|
thread_pool=None,
|
|
180
182
|
cache=cache,
|
|
@@ -188,7 +190,7 @@ def package_engine(
|
|
|
188
190
|
preprocess=tutorial_mode.preprocess,
|
|
189
191
|
navigate=tutorial_mode.navigate,
|
|
190
192
|
update=tutorial_mode.update,
|
|
191
|
-
|
|
193
|
+
project_state=project_state,
|
|
192
194
|
config=config,
|
|
193
195
|
thread_pool=None,
|
|
194
196
|
cache=cache,
|
|
@@ -200,7 +202,7 @@ def package_engine(
|
|
|
200
202
|
watch_data=package_data(watch_data),
|
|
201
203
|
preview_data=package_data(preview_data),
|
|
202
204
|
tutorial_data=package_data(tutorial_data),
|
|
203
|
-
rom=package_rom(rom),
|
|
205
|
+
rom=package_rom(project_state.rom),
|
|
204
206
|
)
|
|
205
207
|
|
|
206
208
|
|
|
@@ -209,7 +211,7 @@ def validate_engine(
|
|
|
209
211
|
config: BuildConfig | None = None,
|
|
210
212
|
):
|
|
211
213
|
config = config or BuildConfig()
|
|
212
|
-
|
|
214
|
+
project_state = ProjectContextState()
|
|
213
215
|
|
|
214
216
|
play_mode = engine.play if config.build_play else empty_play_mode()
|
|
215
217
|
watch_mode = engine.watch if config.build_watch else empty_watch_mode()
|
|
@@ -222,7 +224,7 @@ def validate_engine(
|
|
|
222
224
|
effects=play_mode.effects,
|
|
223
225
|
particles=play_mode.particles,
|
|
224
226
|
buckets=play_mode.buckets,
|
|
225
|
-
|
|
227
|
+
project_state=project_state,
|
|
226
228
|
config=config,
|
|
227
229
|
thread_pool=None,
|
|
228
230
|
validate_only=True,
|
|
@@ -233,7 +235,7 @@ def validate_engine(
|
|
|
233
235
|
effects=watch_mode.effects,
|
|
234
236
|
particles=watch_mode.particles,
|
|
235
237
|
buckets=watch_mode.buckets,
|
|
236
|
-
|
|
238
|
+
project_state=project_state,
|
|
237
239
|
update_spawn=watch_mode.update_spawn,
|
|
238
240
|
config=config,
|
|
239
241
|
thread_pool=None,
|
|
@@ -242,7 +244,7 @@ def validate_engine(
|
|
|
242
244
|
build_preview_mode(
|
|
243
245
|
archetypes=preview_mode.archetypes,
|
|
244
246
|
skin=preview_mode.skin,
|
|
245
|
-
|
|
247
|
+
project_state=project_state,
|
|
246
248
|
config=config,
|
|
247
249
|
thread_pool=None,
|
|
248
250
|
validate_only=True,
|
|
@@ -256,7 +258,7 @@ def validate_engine(
|
|
|
256
258
|
preprocess=tutorial_mode.preprocess,
|
|
257
259
|
navigate=tutorial_mode.navigate,
|
|
258
260
|
update=tutorial_mode.update,
|
|
259
|
-
|
|
261
|
+
project_state=project_state,
|
|
260
262
|
config=config,
|
|
261
263
|
thread_pool=None,
|
|
262
264
|
validate_only=True,
|
|
@@ -279,7 +281,7 @@ def build_play_mode(
|
|
|
279
281
|
effects: Effects,
|
|
280
282
|
particles: Particles,
|
|
281
283
|
buckets: Buckets,
|
|
282
|
-
|
|
284
|
+
project_state: ProjectContextState,
|
|
283
285
|
config: BuildConfig,
|
|
284
286
|
thread_pool: Executor | None = None,
|
|
285
287
|
validate_only: bool = False,
|
|
@@ -288,7 +290,7 @@ def build_play_mode(
|
|
|
288
290
|
return {
|
|
289
291
|
**compile_mode(
|
|
290
292
|
mode=Mode.PLAY,
|
|
291
|
-
|
|
293
|
+
project_state=project_state,
|
|
292
294
|
archetypes=archetypes,
|
|
293
295
|
global_callbacks=None,
|
|
294
296
|
passes=config.passes,
|
|
@@ -309,7 +311,7 @@ def build_watch_mode(
|
|
|
309
311
|
effects: Effects,
|
|
310
312
|
particles: Particles,
|
|
311
313
|
buckets: Buckets,
|
|
312
|
-
|
|
314
|
+
project_state: ProjectContextState,
|
|
313
315
|
update_spawn: Callable[[], float],
|
|
314
316
|
config: BuildConfig,
|
|
315
317
|
thread_pool: Executor | None = None,
|
|
@@ -319,7 +321,7 @@ def build_watch_mode(
|
|
|
319
321
|
return {
|
|
320
322
|
**compile_mode(
|
|
321
323
|
mode=Mode.WATCH,
|
|
322
|
-
|
|
324
|
+
project_state=project_state,
|
|
323
325
|
archetypes=archetypes,
|
|
324
326
|
global_callbacks=[(update_spawn_callback, update_spawn)],
|
|
325
327
|
passes=config.passes,
|
|
@@ -337,7 +339,7 @@ def build_watch_mode(
|
|
|
337
339
|
def build_preview_mode(
|
|
338
340
|
archetypes: list[type[_BaseArchetype]],
|
|
339
341
|
skin: Skin,
|
|
340
|
-
|
|
342
|
+
project_state: ProjectContextState,
|
|
341
343
|
config: BuildConfig,
|
|
342
344
|
thread_pool: Executor | None = None,
|
|
343
345
|
validate_only: bool = False,
|
|
@@ -346,7 +348,7 @@ def build_preview_mode(
|
|
|
346
348
|
return {
|
|
347
349
|
**compile_mode(
|
|
348
350
|
mode=Mode.PREVIEW,
|
|
349
|
-
|
|
351
|
+
project_state=project_state,
|
|
350
352
|
archetypes=archetypes,
|
|
351
353
|
global_callbacks=None,
|
|
352
354
|
passes=config.passes,
|
|
@@ -367,7 +369,7 @@ def build_tutorial_mode(
|
|
|
367
369
|
preprocess: Callable[[], None],
|
|
368
370
|
navigate: Callable[[], None],
|
|
369
371
|
update: Callable[[], None],
|
|
370
|
-
|
|
372
|
+
project_state: ProjectContextState,
|
|
371
373
|
config: BuildConfig,
|
|
372
374
|
thread_pool: Executor | None = None,
|
|
373
375
|
validate_only: bool = False,
|
|
@@ -376,7 +378,7 @@ def build_tutorial_mode(
|
|
|
376
378
|
return {
|
|
377
379
|
**compile_mode(
|
|
378
380
|
mode=Mode.TUTORIAL,
|
|
379
|
-
|
|
381
|
+
project_state=project_state,
|
|
380
382
|
archetypes=[],
|
|
381
383
|
global_callbacks=[
|
|
382
384
|
(preprocess_callback, preprocess),
|
sonolus/build/project.py
CHANGED
|
@@ -7,6 +7,7 @@ from sonolus.build.compile import CompileCache
|
|
|
7
7
|
from sonolus.build.engine import package_engine, unpackage_data
|
|
8
8
|
from sonolus.build.level import package_level_data
|
|
9
9
|
from sonolus.script.engine import Engine
|
|
10
|
+
from sonolus.script.internal.context import ProjectContextState
|
|
10
11
|
from sonolus.script.level import ExternalLevelData, ExternalLevelDataDict, Level, LevelData, parse_external_level_data
|
|
11
12
|
from sonolus.script.project import BuildConfig, Project, ProjectSchema
|
|
12
13
|
|
|
@@ -21,7 +22,10 @@ BLANK_AUDIO = (
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def build_project_to_collection(
|
|
24
|
-
project: Project,
|
|
25
|
+
project: Project,
|
|
26
|
+
config: BuildConfig | None,
|
|
27
|
+
cache: CompileCache | None = None,
|
|
28
|
+
project_state: ProjectContextState | None = None,
|
|
25
29
|
) -> Collection:
|
|
26
30
|
collection = load_resources_files_to_collection(project.resources)
|
|
27
31
|
for src_engine, converter in project.converters.items():
|
|
@@ -34,7 +38,7 @@ def build_project_to_collection(
|
|
|
34
38
|
if config.override_resource_level_engines:
|
|
35
39
|
for level in collection.categories.get("levels", {}).values():
|
|
36
40
|
level["item"]["engine"] = project.engine.name
|
|
37
|
-
add_engine_to_collection(collection, project, project.engine, config, cache=cache)
|
|
41
|
+
add_engine_to_collection(collection, project, project.engine, config, cache=cache, project_state=project_state)
|
|
38
42
|
for level in project.levels:
|
|
39
43
|
add_level_to_collection(collection, project, level)
|
|
40
44
|
collection.name = f"{project.engine.name}"
|
|
@@ -72,8 +76,9 @@ def add_engine_to_collection(
|
|
|
72
76
|
engine: Engine,
|
|
73
77
|
config: BuildConfig | None,
|
|
74
78
|
cache: CompileCache | None = None,
|
|
79
|
+
project_state: ProjectContextState | None = None,
|
|
75
80
|
):
|
|
76
|
-
packaged_engine = package_engine(engine.data, config, cache=cache)
|
|
81
|
+
packaged_engine = package_engine(engine.data, config, cache=cache, project_state=project_state)
|
|
77
82
|
item = {
|
|
78
83
|
"name": engine.name,
|
|
79
84
|
"version": engine.version,
|
sonolus/script/archetype.py
CHANGED
|
@@ -207,7 +207,7 @@ class _IsScoredDescriptor(SonolusDescriptor):
|
|
|
207
207
|
if instance is None:
|
|
208
208
|
return self.value
|
|
209
209
|
elif ctx():
|
|
210
|
-
return ctx().
|
|
210
|
+
return ctx().mode_state.is_scored_by_archetype_id[instance.id]
|
|
211
211
|
else:
|
|
212
212
|
return self.value
|
|
213
213
|
|
|
@@ -220,7 +220,7 @@ class _IdDescriptor(SonolusDescriptor):
|
|
|
220
220
|
if not ctx():
|
|
221
221
|
raise RuntimeError("Archetype id is only available during compilation")
|
|
222
222
|
if instance is None:
|
|
223
|
-
result = ctx().
|
|
223
|
+
result = ctx().mode_state.archetypes.get(owner)
|
|
224
224
|
if result is None:
|
|
225
225
|
raise RuntimeError("Archetype is not registered")
|
|
226
226
|
return result
|
|
@@ -237,7 +237,7 @@ class _KeyDescriptor(SonolusDescriptor):
|
|
|
237
237
|
|
|
238
238
|
def __get__(self, instance, owner):
|
|
239
239
|
if instance is not None and ctx():
|
|
240
|
-
return ctx().
|
|
240
|
+
return ctx().mode_state.keys_by_archetype_id[instance.id]
|
|
241
241
|
else:
|
|
242
242
|
return self.value
|
|
243
243
|
|
|
@@ -249,8 +249,8 @@ class _ArchetypeLifeDescriptor(SonolusDescriptor):
|
|
|
249
249
|
def __get__(self, instance, owner):
|
|
250
250
|
if not ctx():
|
|
251
251
|
raise RuntimeError("Archetype life is only available during compilation")
|
|
252
|
-
if ctx().
|
|
253
|
-
raise RuntimeError(f"Archetype life is not available in mode '{ctx().
|
|
252
|
+
if ctx().mode_state.mode not in {Mode.PLAY, Mode.WATCH}:
|
|
253
|
+
raise RuntimeError(f"Archetype life is not available in mode '{ctx().mode_state.mode.value}'")
|
|
254
254
|
if instance is not None:
|
|
255
255
|
return _deref(ctx().blocks.ArchetypeLife, instance.id * ArchetypeLife._size_(), ArchetypeLife)
|
|
256
256
|
else:
|
|
@@ -1115,7 +1115,7 @@ def get_archetype_by_name(name: str) -> AnyArchetype:
|
|
|
1115
1115
|
name = name._as_py_() # type: ignore
|
|
1116
1116
|
if not isinstance(name, str):
|
|
1117
1117
|
raise TypeError(f"Invalid name: '{name}'")
|
|
1118
|
-
archetypes_by_name = ctx().
|
|
1118
|
+
archetypes_by_name = ctx().mode_state.archetypes_by_name
|
|
1119
1119
|
if name not in archetypes_by_name:
|
|
1120
1120
|
raise KeyError(f"Unknown archetype: '{name}'")
|
|
1121
1121
|
return archetypes_by_name[name] # type: ignore
|
|
@@ -1129,7 +1129,7 @@ def entity_info_at(index: int) -> PlayEntityInfo | WatchEntityInfo | PreviewEnti
|
|
|
1129
1129
|
"""
|
|
1130
1130
|
if not ctx():
|
|
1131
1131
|
raise RuntimeError("Calling entity_info_at is only allowed within a callback")
|
|
1132
|
-
match ctx().
|
|
1132
|
+
match ctx().mode_state.mode:
|
|
1133
1133
|
case Mode.PLAY:
|
|
1134
1134
|
return _deref(ctx().blocks.EntityInfoArray, index * PlayEntityInfo._size_(), PlayEntityInfo)
|
|
1135
1135
|
case Mode.WATCH:
|
|
@@ -1137,7 +1137,7 @@ def entity_info_at(index: int) -> PlayEntityInfo | WatchEntityInfo | PreviewEnti
|
|
|
1137
1137
|
case Mode.PREVIEW:
|
|
1138
1138
|
return _deref(ctx().blocks.EntityInfoArray, index * PreviewEntityInfo._size_(), PreviewEntityInfo)
|
|
1139
1139
|
case _:
|
|
1140
|
-
raise RuntimeError(f"Entity info is not available in mode '{ctx().
|
|
1140
|
+
raise RuntimeError(f"Entity info is not available in mode '{ctx().mode_state.mode}'")
|
|
1141
1141
|
|
|
1142
1142
|
|
|
1143
1143
|
class PlayEntityInfo(Record):
|
sonolus/script/array.py
CHANGED
|
@@ -194,7 +194,22 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
|
|
|
194
194
|
|
|
195
195
|
@meta_fn
|
|
196
196
|
def __getitem__(self, index: int) -> T:
|
|
197
|
-
|
|
197
|
+
return self.get_unchecked(get_positive_index(index, self.size()))
|
|
198
|
+
|
|
199
|
+
@meta_fn
|
|
200
|
+
def get_unchecked(self, index: Num) -> T:
|
|
201
|
+
"""Get the element at the given index possibly without bounds checking.
|
|
202
|
+
|
|
203
|
+
The compiler may still determine that the index is out of bounds and throw an error, but it may skip these
|
|
204
|
+
checks at runtime.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
index: The index to get.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
The element at the given index.
|
|
211
|
+
"""
|
|
212
|
+
index = Num._accept_(index)
|
|
198
213
|
if index._is_py_() and 0 <= index._as_py_() < self.size():
|
|
199
214
|
const_index = index._as_py_()
|
|
200
215
|
if isinstance(const_index, float) and not const_index.is_integer():
|
|
@@ -213,7 +228,8 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
|
|
|
213
228
|
)
|
|
214
229
|
elif callable(self._value):
|
|
215
230
|
return self.element_type()._from_backing_source_(
|
|
216
|
-
lambda offset: self._value((Num(offset) + Num(const_index * self.element_type()._size_())).ir())
|
|
231
|
+
lambda offset: self._value((Num(offset) + Num(const_index * self.element_type()._size_())).ir())
|
|
232
|
+
# type: ignore
|
|
217
233
|
)
|
|
218
234
|
else:
|
|
219
235
|
raise InternalError("Unexpected array value")
|
|
@@ -238,7 +254,20 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
|
|
|
238
254
|
|
|
239
255
|
@meta_fn
|
|
240
256
|
def __setitem__(self, index: int, value: T):
|
|
241
|
-
|
|
257
|
+
self.set_unchecked(get_positive_index(index, self.size()), value)
|
|
258
|
+
|
|
259
|
+
@meta_fn
|
|
260
|
+
def set_unchecked(self, index: Num, value: T):
|
|
261
|
+
"""Set the element at the given index possibly without bounds checking.
|
|
262
|
+
|
|
263
|
+
The compiler may still determine that the index is out of bounds and throw an error, but it may skip these
|
|
264
|
+
checks at runtime.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
index: The index to set.
|
|
268
|
+
value: The value to set.
|
|
269
|
+
"""
|
|
270
|
+
index = Num._accept_(index)
|
|
242
271
|
value = self.element_type()._accept_(value)
|
|
243
272
|
if ctx():
|
|
244
273
|
if isinstance(self._value, list):
|
|
@@ -288,7 +317,7 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
|
|
|
288
317
|
return False
|
|
289
318
|
i = 0
|
|
290
319
|
while i < self.size():
|
|
291
|
-
if self
|
|
320
|
+
if self.get_unchecked(i) != other.get_unchecked(i):
|
|
292
321
|
return False
|
|
293
322
|
i += 1
|
|
294
323
|
return True
|
sonolus/script/array_like.py
CHANGED
|
@@ -5,8 +5,10 @@ from abc import abstractmethod
|
|
|
5
5
|
from collections.abc import Callable, Sequence
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
+
from sonolus.script.debug import assert_true
|
|
8
9
|
from sonolus.script.internal.context import ctx
|
|
9
10
|
from sonolus.script.internal.impl import meta_fn
|
|
11
|
+
from sonolus.script.internal.math_impls import _trunc
|
|
10
12
|
from sonolus.script.iterator import SonolusIterator
|
|
11
13
|
from sonolus.script.maybe import Maybe, Nothing, Some
|
|
12
14
|
from sonolus.script.num import Num
|
|
@@ -58,9 +60,37 @@ class ArrayLike[T](Sequence[T]):
|
|
|
58
60
|
value: The value to set.
|
|
59
61
|
"""
|
|
60
62
|
|
|
63
|
+
@meta_fn
|
|
64
|
+
def get_unchecked(self, index: Num) -> T:
|
|
65
|
+
"""Get the element at the given index possibly without bounds checking or conversion of negative indexes.
|
|
66
|
+
|
|
67
|
+
The compiler may still determine that the index is out of bounds and throw an error, but it may skip these
|
|
68
|
+
checks at runtime.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
index: The index to get.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The element at the given index.
|
|
75
|
+
"""
|
|
76
|
+
return self[index]
|
|
77
|
+
|
|
78
|
+
@meta_fn
|
|
79
|
+
def set_unchecked(self, index: Num, value: T):
|
|
80
|
+
"""Set the element at the given index possibly without bounds checking or conversion of negative indexes.
|
|
81
|
+
|
|
82
|
+
The compiler may still determine that the index is out of bounds and throw an error, but it may skip these
|
|
83
|
+
checks at runtime.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
index: The index to set.
|
|
87
|
+
value: The value to set.
|
|
88
|
+
"""
|
|
89
|
+
self[index] = value
|
|
90
|
+
|
|
61
91
|
def __iter__(self) -> SonolusIterator[T]:
|
|
62
92
|
"""Return an iterator over the array."""
|
|
63
|
-
return _ArrayIterator(0, self)
|
|
93
|
+
return _ArrayIterator(0, self.unchecked())
|
|
64
94
|
|
|
65
95
|
def __contains__(self, value: Any) -> bool:
|
|
66
96
|
"""Return whether any element in the array is equal to the given value.
|
|
@@ -70,7 +100,7 @@ class ArrayLike[T](Sequence[T]):
|
|
|
70
100
|
"""
|
|
71
101
|
i = 0
|
|
72
102
|
while i < len(self):
|
|
73
|
-
if self
|
|
103
|
+
if self.get_unchecked(i) == value:
|
|
74
104
|
return True
|
|
75
105
|
i += 1
|
|
76
106
|
return False
|
|
@@ -92,9 +122,13 @@ class ArrayLike[T](Sequence[T]):
|
|
|
92
122
|
"""
|
|
93
123
|
if stop is None:
|
|
94
124
|
stop = len(self)
|
|
95
|
-
|
|
125
|
+
else:
|
|
126
|
+
stop = get_positive_index(stop, len(self), check=False)
|
|
127
|
+
stop = min(stop, len(self))
|
|
128
|
+
start = get_positive_index(start, len(self), check=False)
|
|
129
|
+
i = max(start, 0)
|
|
96
130
|
while i < stop:
|
|
97
|
-
if self
|
|
131
|
+
if self.get_unchecked(i) == value:
|
|
98
132
|
return i
|
|
99
133
|
i += 1
|
|
100
134
|
return -1
|
|
@@ -108,7 +142,7 @@ class ArrayLike[T](Sequence[T]):
|
|
|
108
142
|
count = 0
|
|
109
143
|
i = 0
|
|
110
144
|
while i < len(self):
|
|
111
|
-
if self
|
|
145
|
+
if self.get_unchecked(i) == value:
|
|
112
146
|
count += 1
|
|
113
147
|
i += 1
|
|
114
148
|
return count
|
|
@@ -121,7 +155,7 @@ class ArrayLike[T](Sequence[T]):
|
|
|
121
155
|
"""
|
|
122
156
|
i = len(self) - 1
|
|
123
157
|
while i >= 0:
|
|
124
|
-
if self
|
|
158
|
+
if self.get_unchecked(i) == value:
|
|
125
159
|
return i
|
|
126
160
|
i -= 1
|
|
127
161
|
return -1
|
|
@@ -139,7 +173,7 @@ class ArrayLike[T](Sequence[T]):
|
|
|
139
173
|
max_index = 0
|
|
140
174
|
i = 1
|
|
141
175
|
while i < len(self):
|
|
142
|
-
if key(self
|
|
176
|
+
if key(self.get_unchecked(i)) > key(self.get_unchecked(max_index)): # type: ignore
|
|
143
177
|
max_index = i
|
|
144
178
|
i += 1
|
|
145
179
|
return max_index
|
|
@@ -157,7 +191,7 @@ class ArrayLike[T](Sequence[T]):
|
|
|
157
191
|
min_index = 0
|
|
158
192
|
i = 1
|
|
159
193
|
while i < len(self):
|
|
160
|
-
if key(self
|
|
194
|
+
if key(self.get_unchecked(i)) < key(self.get_unchecked(min_index)): # type: ignore
|
|
161
195
|
min_index = i
|
|
162
196
|
i += 1
|
|
163
197
|
return min_index
|
|
@@ -165,23 +199,25 @@ class ArrayLike[T](Sequence[T]):
|
|
|
165
199
|
def _max_(self, key: Callable[[T], Any] | None = None) -> T:
|
|
166
200
|
index = self.index_of_max(key=key)
|
|
167
201
|
assert index != -1
|
|
168
|
-
return self
|
|
202
|
+
return self.get_unchecked(index)
|
|
169
203
|
|
|
170
204
|
def _min_(self, key: Callable[[T], Any] | None = None) -> T:
|
|
171
205
|
index = self.index_of_min(key=key)
|
|
172
206
|
assert index != -1
|
|
173
|
-
return self
|
|
207
|
+
return self.get_unchecked(index)
|
|
174
208
|
|
|
175
209
|
def swap(self, i: int, j: int, /):
|
|
176
|
-
"""Swap the values at the given indices.
|
|
210
|
+
"""Swap the values at the given positive indices.
|
|
177
211
|
|
|
178
212
|
Args:
|
|
179
213
|
i: The first index.
|
|
180
214
|
j: The second index.
|
|
181
215
|
"""
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
216
|
+
check_positive_index(i, len(self))
|
|
217
|
+
check_positive_index(j, len(self))
|
|
218
|
+
temp = copy(self.get_unchecked(i))
|
|
219
|
+
self.set_unchecked(i, self.get_unchecked(j))
|
|
220
|
+
self.set_unchecked(j, temp)
|
|
185
221
|
|
|
186
222
|
def sort(self, *, key: Callable[[T], Any] | None = None, reverse: bool = False):
|
|
187
223
|
"""Sort the values in the array in place.
|
|
@@ -194,10 +230,10 @@ class ArrayLike[T](Sequence[T]):
|
|
|
194
230
|
if key is None:
|
|
195
231
|
key = _identity # type: ignore
|
|
196
232
|
# May be worth adding a block sort variant for better performance on large arrays in the future
|
|
197
|
-
_insertion_sort(self, 0, len(self), key, reverse) # type: ignore
|
|
233
|
+
_insertion_sort(self.unchecked(), 0, len(self), key, reverse) # type: ignore
|
|
198
234
|
else:
|
|
199
235
|
# Heap sort is unstable, so if there's a key, we can't rely on it
|
|
200
|
-
_heap_sort(self, 0, len(self), reverse) # type: ignore
|
|
236
|
+
_heap_sort(self.unchecked(), 0, len(self), reverse) # type: ignore
|
|
201
237
|
|
|
202
238
|
def shuffle(self):
|
|
203
239
|
"""Shuffle the values in the array in place."""
|
|
@@ -212,6 +248,10 @@ class ArrayLike[T](Sequence[T]):
|
|
|
212
248
|
i += 1
|
|
213
249
|
j -= 1
|
|
214
250
|
|
|
251
|
+
def unchecked(self) -> ArrayLike[T]:
|
|
252
|
+
"""Return a proxy object that may skip bounds checking and may not support negative indexes."""
|
|
253
|
+
return UncheckedArrayProxy(self)
|
|
254
|
+
|
|
215
255
|
|
|
216
256
|
def _identity[T](value: T) -> T:
|
|
217
257
|
return value
|
|
@@ -268,7 +308,7 @@ class _ArrayIterator[V: ArrayLike](Record, SonolusIterator):
|
|
|
268
308
|
|
|
269
309
|
def next(self) -> Maybe[V]:
|
|
270
310
|
if self.i < len(self.array):
|
|
271
|
-
value = self.array
|
|
311
|
+
value = self.array.get_unchecked(self.i)
|
|
272
312
|
self.i += 1
|
|
273
313
|
return Some(value)
|
|
274
314
|
return Nothing
|
|
@@ -286,6 +326,12 @@ class _ArrayReverser[V: ArrayLike](Record, ArrayLike):
|
|
|
286
326
|
def __setitem__(self, index: int, value: V):
|
|
287
327
|
self.array[len(self) - 1 - index] = value
|
|
288
328
|
|
|
329
|
+
def get_unchecked(self, index: Num) -> V:
|
|
330
|
+
return self.array.get_unchecked(len(self) - 1 - index)
|
|
331
|
+
|
|
332
|
+
def set_unchecked(self, index: Num, value: V):
|
|
333
|
+
self.array.set_unchecked(len(self) - 1 - index, value)
|
|
334
|
+
|
|
289
335
|
def reversed(self) -> ArrayLike[V]:
|
|
290
336
|
return self.array
|
|
291
337
|
|
|
@@ -297,30 +343,99 @@ class _ArrayEnumerator[V: ArrayLike](Record, SonolusIterator):
|
|
|
297
343
|
|
|
298
344
|
def next(self) -> Maybe[tuple[int, Any]]:
|
|
299
345
|
if self.i < len(self.array):
|
|
300
|
-
result = (self.i + self.offset, self.array
|
|
346
|
+
result = (self.i + self.offset, self.array.get_unchecked(self.i))
|
|
301
347
|
self.i += 1
|
|
302
348
|
return Some(result)
|
|
303
349
|
return Nothing
|
|
304
350
|
|
|
305
351
|
|
|
306
352
|
@meta_fn
|
|
307
|
-
def get_positive_index(
|
|
308
|
-
|
|
353
|
+
def get_positive_index(
|
|
354
|
+
index: int | float, length: int | float, *, include_end: bool = False, check: bool = True
|
|
355
|
+
) -> int:
|
|
356
|
+
"""Get the positive index for the given index in the array of the given length, and also perform bounds checking.
|
|
309
357
|
|
|
310
|
-
This is used to convert negative
|
|
358
|
+
This is used to convert negative indices relative to the end of the array to positive indices.
|
|
311
359
|
|
|
312
360
|
Args:
|
|
313
361
|
index: The index to convert.
|
|
314
362
|
length: The length of the array.
|
|
363
|
+
include_end: Whether to allow the index to be equal to the length of the array (i.e., one past the end).
|
|
364
|
+
check: Whether to perform bounds checking. Must be a compile-time constant.
|
|
315
365
|
|
|
316
366
|
Returns:
|
|
317
|
-
The positive index.
|
|
367
|
+
The positive integer index.
|
|
318
368
|
"""
|
|
319
369
|
if not ctx():
|
|
320
|
-
|
|
370
|
+
if check:
|
|
371
|
+
if (include_end and not -length <= index <= length) or (not include_end and not -length <= index < length):
|
|
372
|
+
raise IndexError("Index out of range")
|
|
373
|
+
if int(index) != index:
|
|
374
|
+
raise ValueError("Index must be an integer")
|
|
375
|
+
if int(length) != length:
|
|
376
|
+
raise ValueError("Length must be an integer")
|
|
377
|
+
if length < 0:
|
|
378
|
+
raise ValueError("Length must be non-negative")
|
|
379
|
+
return int(index + (index < 0) * length)
|
|
321
380
|
index = Num._accept_(index)
|
|
322
381
|
length = Num._accept_(length)
|
|
323
|
-
if
|
|
324
|
-
|
|
382
|
+
if Num._accept_(check)._as_py_():
|
|
383
|
+
include_end = Num._accept_(include_end)
|
|
384
|
+
if not include_end._is_py_():
|
|
385
|
+
is_in_bounds = Num.and_(index >= -length, index < (length + include_end))
|
|
386
|
+
elif include_end._as_py_():
|
|
387
|
+
is_in_bounds = Num.and_(index >= -length, index <= length)
|
|
388
|
+
else:
|
|
389
|
+
is_in_bounds = Num.and_(index >= -length, index < length)
|
|
390
|
+
assert_true(Num.and_(is_in_bounds, _trunc(index) == index), "Invalid index")
|
|
391
|
+
# Skipping length check since typically these are managed by the library and unlikely to be wrong
|
|
392
|
+
return index + (index < 0) * length
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@meta_fn
|
|
396
|
+
def check_positive_index(index: int, length: int, include_end: bool = False) -> int | float:
|
|
397
|
+
"""Check that the given index is a valid index for the array of the given length and convert it to an integer.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
index: The index to check.
|
|
401
|
+
length: The length of the array.
|
|
402
|
+
include_end: Whether to allow the index to be equal to the length of the array (i.e., one past the end).
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
The index as an integer.
|
|
406
|
+
"""
|
|
407
|
+
if not ctx():
|
|
408
|
+
if (include_end and not 0 <= index <= length) or (not include_end and not 0 <= index < length):
|
|
409
|
+
raise IndexError("Index out of range")
|
|
410
|
+
if int(index) != index:
|
|
411
|
+
raise ValueError("Index must be an integer")
|
|
412
|
+
if int(length) != length:
|
|
413
|
+
raise ValueError("Length must be an integer")
|
|
414
|
+
if length < 0:
|
|
415
|
+
raise ValueError("Length must be non-negative")
|
|
416
|
+
return int(index)
|
|
417
|
+
index = Num._accept_(index)
|
|
418
|
+
length = Num._accept_(length)
|
|
419
|
+
include_end = Num._accept_(include_end)
|
|
420
|
+
if not include_end._is_py_():
|
|
421
|
+
is_in_bounds = Num.and_(index >= 0, index < (length + include_end))
|
|
422
|
+
elif include_end._as_py_():
|
|
423
|
+
is_in_bounds = Num.and_(index >= 0, index <= length)
|
|
325
424
|
else:
|
|
326
|
-
|
|
425
|
+
is_in_bounds = Num.and_(index >= 0, index < length)
|
|
426
|
+
assert_true(Num.and_(is_in_bounds, _trunc(index) == index), "Invalid index")
|
|
427
|
+
# Skipping length check since typically these are managed by the library and unlikely to be wrong
|
|
428
|
+
return index
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class UncheckedArrayProxy[T](Record, ArrayLike):
|
|
432
|
+
array: T
|
|
433
|
+
|
|
434
|
+
def __len__(self) -> int:
|
|
435
|
+
return len(self.array)
|
|
436
|
+
|
|
437
|
+
def __getitem__(self, index: int) -> Any:
|
|
438
|
+
return self.array.get_unchecked(index)
|
|
439
|
+
|
|
440
|
+
def __setitem__(self, index: int, value: Any):
|
|
441
|
+
self.array.set_unchecked(index, value)
|