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/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
- rom = ReadOnlyMemory()
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
- rom=rom,
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
- rom=rom,
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
- rom=rom,
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
- rom=rom,
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
- rom=rom,
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
- rom=rom,
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
- rom=rom,
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
- rom=rom,
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
- rom = ReadOnlyMemory()
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
- rom=rom,
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
- rom=rom,
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
- rom=rom,
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
- rom=rom,
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
- rom: ReadOnlyMemory,
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
- rom=rom,
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
- rom: ReadOnlyMemory,
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
- rom=rom,
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
- rom: ReadOnlyMemory,
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
- rom=rom,
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
- rom: ReadOnlyMemory,
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
- rom=rom,
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, config: BuildConfig | None, cache: CompileCache | None = None
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,
@@ -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().global_state.is_scored_by_archetype_id[instance.id]
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().global_state.archetypes.get(owner)
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().global_state.keys_by_archetype_id[instance.id]
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().global_state.mode not in {Mode.PLAY, Mode.WATCH}:
253
- raise RuntimeError(f"Archetype life is not available in mode '{ctx().global_state.mode.value}'")
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().global_state.archetypes_by_name
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().global_state.mode:
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().global_state.mode}'")
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
- index: Num = Num._accept_(get_positive_index(index, self.size()))
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()) # type: ignore
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
- index: Num = Num._accept_(get_positive_index(index, self.size()))
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[i] != other[i]:
320
+ if self.get_unchecked(i) != other.get_unchecked(i):
292
321
  return False
293
322
  i += 1
294
323
  return True
@@ -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[i] == value:
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
- i = start
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[i] == value:
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[i] == value:
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[i] == value:
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[i]) > key(self[max_index]): # type: ignore
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[i]) < key(self[min_index]): # type: ignore
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[index]
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[index]
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
- temp = copy(self[i])
183
- self[i] = self[j]
184
- self[j] = temp
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[self.i]
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[self.i])
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(index: int, length: int) -> int:
308
- """Get the positive index for the given index in the array of the given length.
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 indixes relative to the end of the array to positive indices.
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
- return index if index >= 0 else index + length
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 index._is_py_() and length._is_py_():
324
- return Num._accept_(index._as_py_() + length._as_py_() if index._as_py_() < 0 else index._as_py_())
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
- return index + (index < 0) * length
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)