sonolus.py 0.3.2__py3-none-any.whl → 0.3.4__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.

@@ -1,3 +1,5 @@
1
+ from math import isfinite, isinf, isnan
2
+
1
3
  from sonolus.backend.ir import IRConst, IRGet, IRInstr, IRPureInstr, IRSet
2
4
  from sonolus.backend.node import ConstantNode, EngineNode, FunctionNode
3
5
  from sonolus.backend.ops import Op
@@ -54,10 +56,20 @@ def cfg_to_engine_node(entry: BasicBlock):
54
56
 
55
57
  def ir_to_engine_node(stmt) -> EngineNode:
56
58
  match stmt:
57
- case int() | float():
58
- return ConstantNode(value=float(stmt))
59
- case IRConst(value=int(value) | float(value)):
60
- return ConstantNode(value=value)
59
+ case int(value) | float(value) | IRConst(value=int(value) | float(value)):
60
+ value = float(value)
61
+ if value.is_integer():
62
+ return ConstantNode(value=int(value))
63
+ elif isfinite(value):
64
+ return ConstantNode(value=value)
65
+ elif isinf(value):
66
+ # Read values from ROM
67
+ return FunctionNode(Op.Get, args=[ConstantNode(value=3000), ConstantNode(value=1 if value > 0 else 2)])
68
+ elif isnan(value):
69
+ # Read value from ROM
70
+ return FunctionNode(Op.Get, args=[ConstantNode(value=3000), ConstantNode(value=0)])
71
+ else:
72
+ raise ValueError(f"Invalid constant value: {value}")
61
73
  case IRPureInstr(op=op, args=args) | IRInstr(op=op, args=args):
62
74
  return FunctionNode(func=op, args=[ir_to_engine_node(arg) for arg in args])
63
75
  case IRGet(place=place):
@@ -359,7 +359,7 @@ class Visitor(ast.NodeVisitor):
359
359
  self.loop_head_ctxs.append(header_ctx)
360
360
  self.break_ctxs.append([])
361
361
  set_ctx(header_ctx)
362
- has_next = self.ensure_boolean_num(self.handle_call(node, iterator.has_next))
362
+ has_next = self.convert_to_boolean_num(node, self.handle_call(node, iterator.has_next))
363
363
  if has_next._is_py_() and not has_next._as_py_():
364
364
  # The loop will never run, continue after evaluating the condition
365
365
  self.loop_head_ctxs.pop()
@@ -400,7 +400,7 @@ class Visitor(ast.NodeVisitor):
400
400
  self.loop_head_ctxs.append(header_ctx)
401
401
  self.break_ctxs.append([])
402
402
  set_ctx(header_ctx)
403
- test = self.ensure_boolean_num(self.visit(node.test))
403
+ test = self.convert_to_boolean_num(node.test, self.visit(node.test))
404
404
  if test._is_py_():
405
405
  if test._as_py_():
406
406
  # The loop will run until a break / return
@@ -454,7 +454,7 @@ class Visitor(ast.NodeVisitor):
454
454
  set_ctx(after_ctx)
455
455
 
456
456
  def visit_If(self, node):
457
- test = self.ensure_boolean_num(self.visit(node.test))
457
+ test = self.convert_to_boolean_num(node.test, self.visit(node.test))
458
458
 
459
459
  if test._is_py_():
460
460
  if test._as_py_():
@@ -507,7 +507,9 @@ class Visitor(ast.NodeVisitor):
507
507
  set_ctx(false_ctx)
508
508
  continue
509
509
  set_ctx(true_ctx)
510
- guard = self.ensure_boolean_num(self.visit(case.guard)) if case.guard else validate_value(True)
510
+ guard = (
511
+ self.convert_to_boolean_num(case.guard, self.visit(case.guard)) if case.guard else validate_value(True)
512
+ )
511
513
  if guard._is_py_():
512
514
  if guard._as_py_():
513
515
  for stmt in case.body:
@@ -544,7 +546,7 @@ class Visitor(ast.NodeVisitor):
544
546
  match pattern:
545
547
  case ast.MatchValue(value=value):
546
548
  value = self.visit(value)
547
- test = self.ensure_boolean_num(validate_value(subject == value))
549
+ test = self.convert_to_boolean_num(pattern, validate_value(subject == value))
548
550
  if test._is_py_():
549
551
  if test._as_py_():
550
552
  return ctx(), ctx().into_dead()
@@ -574,7 +576,7 @@ class Visitor(ast.NodeVisitor):
574
576
  target_len = len(patterns)
575
577
  if not (isinstance(subject, Sequence | TupleImpl)):
576
578
  return ctx().into_dead(), ctx()
577
- length_test = self.ensure_boolean_num(validate_value(_len(subject) == target_len))
579
+ length_test = self.convert_to_boolean_num(pattern, validate_value(_len(subject) == target_len))
578
580
  ctx_init = ctx()
579
581
  if not length_test._is_py_():
580
582
  ctx_init.test = length_test.ir()
@@ -739,7 +741,7 @@ class Visitor(ast.NodeVisitor):
739
741
  def visit_UnaryOp(self, node):
740
742
  operand = self.visit(node.operand)
741
743
  if isinstance(node.op, ast.Not):
742
- return self.ensure_boolean_num(operand).not_()
744
+ return self.convert_to_boolean_num(node, operand).not_()
743
745
  op = unary_ops[type(node.op)]
744
746
  if operand._is_py_():
745
747
  operand_py = operand._as_py_()
@@ -768,7 +770,7 @@ class Visitor(ast.NodeVisitor):
768
770
  return validate_value(fn)
769
771
 
770
772
  def visit_IfExp(self, node):
771
- test = self.ensure_boolean_num(self.visit(node.test))
773
+ test = self.convert_to_boolean_num(node.test, self.visit(node.test))
772
774
 
773
775
  if test._is_py_():
774
776
  if test._as_py_():
@@ -1076,7 +1078,7 @@ class Visitor(ast.NodeVisitor):
1076
1078
 
1077
1079
  def handle_call[**P, R](
1078
1080
  self, node: ast.stmt | ast.expr, fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs
1079
- ) -> R:
1081
+ ) -> R | Value:
1080
1082
  """Handles a call to the given callable."""
1081
1083
  self.active_ctx = ctx()
1082
1084
  if (
@@ -1127,6 +1129,20 @@ class Visitor(ast.NodeVisitor):
1127
1129
  raise TypeError(f"Invalid type where a bool (Num) was expected: {type(value).__name__}")
1128
1130
  return value
1129
1131
 
1132
+ def convert_to_boolean_num(self, node, value: Value) -> Num:
1133
+ if _is_num(value):
1134
+ return value
1135
+ if hasattr(type(value), "__bool__"):
1136
+ return self.ensure_boolean_num(self.handle_call(node, type(value).__bool__, validate_value(value)))
1137
+ if hasattr(type(value), "__len__"):
1138
+ length = self.handle_call(node, type(value).__len__, validate_value(value))
1139
+ if not _is_num(length):
1140
+ raise TypeError(f"Invalid type for __len__: {type(length).__name__}")
1141
+ if length._is_py_():
1142
+ return Num._accept_(length._as_py_() > 0)
1143
+ return length > Num._accept_(0)
1144
+ raise TypeError(f"Converting {type(value).__name__} to bool is not supported")
1145
+
1130
1146
  def arguments_to_signature(self, arguments: ast.arguments) -> inspect.Signature:
1131
1147
  parameters: list[inspect.Parameter] = []
1132
1148
  pos_only_count = len(arguments.posonlyargs)
@@ -312,7 +312,7 @@ class StandardImport:
312
312
  def callback[T: Callable](*, order: int = 0) -> Callable[[T], T]:
313
313
  """Annotate a callback with its order.
314
314
 
315
- Callbacks are execute from lowest to highest order. By default, callbacks have an order of 0.
315
+ Callbacks are executed from lowest to highest order. By default, callbacks have an order of 0.
316
316
 
317
317
  Usage:
318
318
  ```python
@@ -338,9 +338,9 @@ class _ArchetypeSelfData:
338
338
 
339
339
 
340
340
  class _ArchetypeReferenceData:
341
- index: Num
341
+ index: int
342
342
 
343
- def __init__(self, index: Num):
343
+ def __init__(self, index: int):
344
344
  self.index = index
345
345
 
346
346
 
@@ -416,21 +416,21 @@ class _BaseArchetype:
416
416
 
417
417
  @classmethod
418
418
  @meta_fn
419
- def at(cls, index: Num) -> Self:
419
+ def at(cls, index: int) -> Self:
420
420
  result = cls._new()
421
421
  result._data_ = _ArchetypeReferenceData(index=Num._accept_(index))
422
422
  return result
423
423
 
424
424
  @classmethod
425
425
  @meta_fn
426
- def is_at(cls, index: Num) -> bool:
426
+ def is_at(cls, index: int) -> bool:
427
427
  if not ctx():
428
428
  raise RuntimeError("is_at is only available during compilation")
429
429
  return entity_info_at(index).archetype_id == cls.id()
430
430
 
431
431
  @classmethod
432
432
  @meta_fn
433
- def id(cls):
433
+ def id(cls) -> int:
434
434
  if not ctx():
435
435
  raise RuntimeError("Archetype id is only available during compilation")
436
436
  result = ctx().global_state.archetypes.get(cls)
@@ -549,11 +549,19 @@ class _BaseArchetype:
549
549
  metadata = _annotation_defaults.get(metadata, metadata)
550
550
  if isinstance(metadata, _ArchetypeFieldInfo):
551
551
  if field_info is not None:
552
- raise TypeError(
553
- f"Unexpected multiple field annotations for '{name}', "
554
- f"expected exactly one of imported, exported, entity_memory, or shared_memory"
555
- )
556
- 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
557
565
  if field_info is None:
558
566
  raise TypeError(
559
567
  f"Missing field annotation for '{name}', "
@@ -880,7 +888,7 @@ class WatchArchetype(_BaseArchetype):
880
888
  case _ArchetypeSelfData():
881
889
  return _deref(ctx().blocks.EntityInfo, 0, WatchEntityInfo)
882
890
  case _ArchetypeReferenceData(index=index):
883
- return _deref(ctx().blocks.EntityInfoArray, index * WatchEntityInfo._size_(), PlayEntityInfo)
891
+ return _deref(ctx().blocks.EntityInfoArray, index * WatchEntityInfo._size_(), WatchEntityInfo)
884
892
  case _:
885
893
  raise RuntimeError("Info is only accessible from the entity itself")
886
894
 
@@ -984,7 +992,7 @@ class PreviewArchetype(_BaseArchetype):
984
992
 
985
993
 
986
994
  @meta_fn
987
- def entity_info_at(index: Num) -> PlayEntityInfo | WatchEntityInfo | PreviewEntityInfo:
995
+ def entity_info_at(index: int) -> PlayEntityInfo | WatchEntityInfo | PreviewEntityInfo:
988
996
  """Retrieve entity info of the entity at the given index.
989
997
 
990
998
  Available in play, watch, and preview mode.
@@ -1039,24 +1047,24 @@ class PreviewEntityInfo(Record):
1039
1047
  class ArchetypeLife(Record):
1040
1048
  """How an entity contributes to life."""
1041
1049
 
1042
- perfect_increment: Num
1050
+ perfect_increment: int
1043
1051
  """Life increment for a perfect judgment."""
1044
1052
 
1045
- great_increment: Num
1053
+ great_increment: int
1046
1054
  """Life increment for a great judgment."""
1047
1055
 
1048
- good_increment: Num
1056
+ good_increment: int
1049
1057
  """Life increment for a good judgment."""
1050
1058
 
1051
- miss_increment: Num
1059
+ miss_increment: int
1052
1060
  """Life increment for a miss judgment."""
1053
1061
 
1054
1062
  def update(
1055
1063
  self,
1056
- perfect_increment: Num | None = None,
1057
- great_increment: Num | None = None,
1058
- good_increment: Num | None = None,
1059
- miss_increment: Num | None = None,
1064
+ perfect_increment: int | None = None,
1065
+ great_increment: int | None = None,
1066
+ good_increment: int | None = None,
1067
+ miss_increment: int | None = None,
1060
1068
  ):
1061
1069
  """Update the life increments."""
1062
1070
  if perfect_increment is not None:
@@ -1106,10 +1114,20 @@ class EntityRef[A: _BaseArchetype](Record):
1106
1114
  """Return a new reference with the given archetype type."""
1107
1115
  return EntityRef[archetype](index=self.index)
1108
1116
 
1117
+ @meta_fn
1109
1118
  def get(self) -> A:
1110
1119
  """Get the entity."""
1120
+ if ref := getattr(self, "_ref_", None):
1121
+ return ref
1111
1122
  return self.archetype().at(self.index)
1112
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
+
1113
1131
  def archetype_matches(self) -> bool:
1114
1132
  """Check if entity at the index is precisely of the archetype."""
1115
1133
  return self.index >= 0 and self.archetype().is_at(self.index)
@@ -1117,7 +1135,7 @@ class EntityRef[A: _BaseArchetype](Record):
1117
1135
  def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
1118
1136
  ref = getattr(self, "_ref_", None)
1119
1137
  if ref is None:
1120
- return [self.index]
1138
+ return Num._accept_(self.index)._to_list_()
1121
1139
  else:
1122
1140
  if ref not in level_refs:
1123
1141
  raise KeyError("Reference to entity not in level data")
sonolus/script/array.py CHANGED
@@ -5,7 +5,7 @@ from typing import Any, Self, final
5
5
 
6
6
  from sonolus.backend.ir import IRConst, IRSet
7
7
  from sonolus.backend.place import BlockPlace
8
- from sonolus.script.array_like import ArrayLike
8
+ from sonolus.script.array_like import ArrayLike, get_positive_index
9
9
  from sonolus.script.debug import assert_unreachable
10
10
  from sonolus.script.internal.context import ctx
11
11
  from sonolus.script.internal.error import InternalError
@@ -18,6 +18,7 @@ from sonolus.script.num import Num
18
18
  class ArrayMeta(type):
19
19
  @meta_fn
20
20
  def __pos__[T](cls: type[T]) -> T:
21
+ """Create a zero-initialized array instance."""
21
22
  return cls._zero_()
22
23
 
23
24
 
@@ -30,6 +31,7 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
30
31
  array_1 = Array(1, 2, 3)
31
32
  array_2 = Array[int, 0]()
32
33
  array_3 = +Array[int, 3] # Create a zero-initialized array
34
+ array_4 = +array_1 # Create a copy of array_1
33
35
  ```
34
36
  """
35
37
 
@@ -185,8 +187,8 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
185
187
  return self.size()
186
188
 
187
189
  @meta_fn
188
- def __getitem__(self, index: Num) -> T:
189
- index: Num = Num._accept_(index)
190
+ def __getitem__(self, index: int) -> T:
191
+ index: Num = Num._accept_(get_positive_index(index, self.size()))
190
192
  if index._is_py_() and 0 <= index._as_py_() < self.size():
191
193
  const_index = index._as_py_()
192
194
  if isinstance(const_index, float) and not const_index.is_integer():
@@ -229,8 +231,8 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
229
231
  raise InternalError("Unexpected array value")
230
232
 
231
233
  @meta_fn
232
- def __setitem__(self, index: Num, value: T):
233
- index: Num = Num._accept_(index)
234
+ def __setitem__(self, index: int, value: T):
235
+ index: Num = Num._accept_(get_positive_index(index, self.size()))
234
236
  value = self.element_type()._accept_(value)
235
237
  if ctx():
236
238
  if isinstance(self._value, list):
@@ -303,6 +305,7 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
303
305
  else:
304
306
  return f"{type(self).__name__}({', '.join(repr(self[i]) for i in range(self.size()))})"
305
307
 
308
+ @meta_fn
306
309
  def __pos__(self) -> Self:
307
310
  """Return a copy of the array."""
308
311
  return self._copy_()
@@ -5,6 +5,8 @@ from abc import abstractmethod
5
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
@@ -24,10 +26,10 @@ class ArrayLike[T]:
24
26
  def __len__(self) -> int:
25
27
  ...
26
28
 
27
- def __getitem__(self, index: Num) -> T:
29
+ def __getitem__(self, index: int) -> T:
28
30
  ...
29
31
 
30
- def __setitem__(self, index: Num, value: T):
32
+ def __setitem__(self, index: int, value: T):
31
33
  ...
32
34
  ```
33
35
  """
@@ -39,7 +41,7 @@ class ArrayLike[T]:
39
41
  """Return the length of the array."""
40
42
 
41
43
  @abstractmethod
42
- def __getitem__(self, index: Num) -> T:
44
+ def __getitem__(self, index: int) -> T:
43
45
  """Return the item at the given index.
44
46
 
45
47
  Args:
@@ -47,7 +49,7 @@ class ArrayLike[T]:
47
49
  """
48
50
 
49
51
  @abstractmethod
50
- def __setitem__(self, index: Num, value: T):
52
+ def __setitem__(self, index: int, value: T):
51
53
  """Set the value of the item at the given index.
52
54
 
53
55
  Args:
@@ -76,10 +78,10 @@ class ArrayLike[T]:
76
78
  """Return a reversed view of the array."""
77
79
  return _ArrayReverser(self)
78
80
 
79
- def _enumerate_(self, start: Num = 0) -> SonolusIterator[T]:
81
+ def _enumerate_(self, start: int = 0) -> SonolusIterator[T]:
80
82
  return _ArrayEnumerator(0, start, self)
81
83
 
82
- def index(self, value: T, start: Num = 0, stop: Num | None = None) -> Num:
84
+ def index(self, value: T, start: int = 0, stop: int | None = None) -> int:
83
85
  """Return the index of the value in the array equal to the given value.
84
86
 
85
87
  Args:
@@ -96,7 +98,7 @@ class ArrayLike[T]:
96
98
  i += 1
97
99
  return -1
98
100
 
99
- def count(self, value: T) -> Num:
101
+ def count(self, value: T) -> int:
100
102
  """Return the number of elements in the array equal to the given value.
101
103
 
102
104
  Args:
@@ -110,7 +112,7 @@ class ArrayLike[T]:
110
112
  i += 1
111
113
  return count
112
114
 
113
- def last_index(self, value: T) -> Num:
115
+ def last_index(self, value: T) -> int:
114
116
  """Return the last index of the value in the array equal to the given value.
115
117
 
116
118
  Args:
@@ -123,7 +125,7 @@ class ArrayLike[T]:
123
125
  i -= 1
124
126
  return -1
125
127
 
126
- def index_of_max(self, *, key: Callable[T, Any] | None = None) -> Num:
128
+ def index_of_max(self, *, key: Callable[[T], Any] | None = None) -> int:
127
129
  """Return the index of the maximum value in the array.
128
130
 
129
131
  Args:
@@ -141,7 +143,7 @@ class ArrayLike[T]:
141
143
  i += 1
142
144
  return max_index
143
145
 
144
- def index_of_min(self, *, key: Callable[T, Any] | None = None) -> Num:
146
+ def index_of_min(self, *, key: Callable[[T], Any] | None = None) -> int:
145
147
  """Return the index of the minimum value in the array.
146
148
 
147
149
  Args:
@@ -159,17 +161,17 @@ class ArrayLike[T]:
159
161
  i += 1
160
162
  return min_index
161
163
 
162
- def _max_(self, key: Callable[T, Any] | None = None) -> T:
164
+ def _max_(self, key: Callable[[T], Any] | None = None) -> T:
163
165
  index = self.index_of_max(key=key)
164
166
  assert index != -1
165
167
  return self[index]
166
168
 
167
- def _min_(self, key: Callable[T, Any] | None = None) -> T:
169
+ def _min_(self, key: Callable[[T], Any] | None = None) -> T:
168
170
  index = self.index_of_min(key=key)
169
171
  assert index != -1
170
172
  return self[index]
171
173
 
172
- def swap(self, i: Num, j: Num, /):
174
+ def swap(self, i: int, j: int, /):
173
175
  """Swap the values at the given indices.
174
176
 
175
177
  Args:
@@ -180,7 +182,7 @@ class ArrayLike[T]:
180
182
  self[i] = self[j]
181
183
  self[j] = temp
182
184
 
183
- def sort(self, *, key: Callable[T, Any] | None = None, reverse: bool = False):
185
+ def sort(self, *, key: Callable[[T], Any] | None = None, reverse: bool = False):
184
186
  """Sort the values in the array in place.
185
187
 
186
188
  Args:
@@ -214,7 +216,7 @@ def _identity[T](value: T) -> T:
214
216
  return value
215
217
 
216
218
 
217
- def _insertion_sort[T](array: ArrayLike[T], start: Num, end: Num, key: Callable[T, Any], reverse: bool):
219
+ def _insertion_sort[T](array: ArrayLike[T], start: int, end: int, key: Callable[[T], Any], reverse: bool):
218
220
  i = start + 1
219
221
  if reverse:
220
222
  while i < end:
@@ -232,7 +234,7 @@ def _insertion_sort[T](array: ArrayLike[T], start: Num, end: Num, key: Callable[
232
234
  i += 1
233
235
 
234
236
 
235
- def _heapify[T](array: ArrayLike[T], end: Num, index: Num, reverse: bool):
237
+ def _heapify[T](array: ArrayLike[T], end: int, index: int, reverse: bool):
236
238
  while True:
237
239
  left = index * 2 + 1
238
240
  right = left + 1
@@ -247,7 +249,7 @@ def _heapify[T](array: ArrayLike[T], end: Num, index: Num, reverse: bool):
247
249
  index = largest
248
250
 
249
251
 
250
- def _heap_sort[T](array: ArrayLike[T], start: Num, end: Num, reverse: bool):
252
+ def _heap_sort[T](array: ArrayLike[T], start: int, end: int, reverse: bool):
251
253
  i = end // 2 - 1
252
254
  while i >= start:
253
255
  _heapify(array, end, i, reverse)
@@ -279,10 +281,10 @@ class _ArrayReverser[V: ArrayLike](Record, ArrayLike):
279
281
  def __len__(self) -> int:
280
282
  return len(self.array)
281
283
 
282
- def __getitem__(self, index: Num) -> V:
284
+ def __getitem__(self, index: int) -> V:
283
285
  return self.array[len(self) - 1 - index]
284
286
 
285
- def __setitem__(self, index: Num, value: V):
287
+ def __setitem__(self, index: int, value: V):
286
288
  self.array[len(self) - 1 - index] = value
287
289
 
288
290
  def reversed(self) -> ArrayLike[V]:
@@ -302,3 +304,26 @@ class _ArrayEnumerator[V: ArrayLike](Record, SonolusIterator):
302
304
 
303
305
  def advance(self):
304
306
  self.i += 1
307
+
308
+
309
+ @meta_fn
310
+ def get_positive_index(index: int, length: int) -> int:
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,10 +2,11 @@ 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
@@ -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(
@@ -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
@@ -57,7 +57,7 @@ def static_error(message: str | None = None) -> Never:
57
57
 
58
58
 
59
59
  @meta_fn
60
- def debug_log(value: Num):
60
+ def debug_log(value: int | float | bool):
61
61
  """Log a value in debug mode."""
62
62
  if debug_log_callback.get(None):
63
63
  return debug_log_callback.get()(value)
@@ -66,7 +66,7 @@ def debug_log(value: Num):
66
66
 
67
67
 
68
68
  @native_function(Op.DebugLog)
69
- def _debug_log(value: Num):
69
+ def _debug_log(value: int | float | bool):
70
70
  print(f"[DEBUG] {value}")
71
71
  return 0
72
72
 
@@ -77,13 +77,13 @@ def debug_pause():
77
77
  input("[DEBUG] Paused")
78
78
 
79
79
 
80
- def assert_true(value: Num, message: str | None = None):
80
+ def assert_true(value: int | float | bool, message: str | None = None):
81
81
  message = message if message is not None else "Assertion failed"
82
82
  if not value:
83
83
  error(message)
84
84
 
85
85
 
86
- def assert_false(value: Num, message: str | None = None):
86
+ def assert_false(value: int | float | bool, message: str | None = None):
87
87
  message = message if message is not None else "Assertion failed"
88
88
  if value:
89
89
  error(message)
sonolus/script/engine.py CHANGED
@@ -217,7 +217,7 @@ class WatchMode:
217
217
 
218
218
  for archetype in self.archetypes:
219
219
  if not issubclass(archetype, WatchArchetype):
220
- raise ValueError(f"archetype {archetype} is not a PlayArchetype")
220
+ raise ValueError(f"archetype {archetype} is not a WatchArchetype")
221
221
 
222
222
 
223
223
  class PreviewMode:
@@ -239,7 +239,7 @@ class PreviewMode:
239
239
 
240
240
  for archetype in self.archetypes:
241
241
  if not issubclass(archetype, PreviewArchetype):
242
- raise ValueError(f"archetype {archetype} is not a BaseArchetype")
242
+ raise ValueError(f"archetype {archetype} is not a PreviewArchetype")
243
243
 
244
244
 
245
245
  class TutorialMode:
@@ -97,7 +97,7 @@ def instructions[T](cls: type[T]) -> T | TutorialInstructions:
97
97
  annotation_values = annotation.__metadata__
98
98
  if annotation_type is not Instruction:
99
99
  raise TypeError(
100
- f"Invalid annotation for instruction: {annotation}, expected annotation of type InstructionText"
100
+ f"Invalid annotation for instruction: {annotation}, expected annotation of type Instruction"
101
101
  )
102
102
  if len(annotation_values) != 1 or not isinstance(annotation_values[0], _InstructionTextInfo):
103
103
  raise TypeError(f"Invalid annotation for instruction: {annotation}, expected a single annotation value")