sonolus.py 0.1.9__py3-none-any.whl → 0.2.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 (47) hide show
  1. sonolus/backend/optimize/constant_evaluation.py +2 -2
  2. sonolus/backend/optimize/optimize.py +12 -4
  3. sonolus/backend/optimize/passes.py +2 -1
  4. sonolus/backend/place.py +95 -14
  5. sonolus/backend/visitor.py +51 -3
  6. sonolus/build/cli.py +60 -9
  7. sonolus/build/collection.py +68 -26
  8. sonolus/build/compile.py +85 -27
  9. sonolus/build/engine.py +166 -40
  10. sonolus/build/node.py +8 -1
  11. sonolus/build/project.py +30 -11
  12. sonolus/script/archetype.py +154 -32
  13. sonolus/script/array.py +14 -3
  14. sonolus/script/array_like.py +6 -2
  15. sonolus/script/containers.py +166 -0
  16. sonolus/script/debug.py +22 -4
  17. sonolus/script/effect.py +2 -2
  18. sonolus/script/engine.py +125 -17
  19. sonolus/script/internal/builtin_impls.py +21 -2
  20. sonolus/script/internal/constant.py +8 -4
  21. sonolus/script/internal/context.py +30 -25
  22. sonolus/script/internal/math_impls.py +2 -1
  23. sonolus/script/internal/transient.py +7 -3
  24. sonolus/script/internal/value.py +33 -11
  25. sonolus/script/interval.py +8 -3
  26. sonolus/script/iterator.py +17 -0
  27. sonolus/script/level.py +113 -10
  28. sonolus/script/metadata.py +32 -0
  29. sonolus/script/num.py +35 -15
  30. sonolus/script/options.py +23 -6
  31. sonolus/script/pointer.py +11 -1
  32. sonolus/script/project.py +41 -5
  33. sonolus/script/quad.py +55 -1
  34. sonolus/script/record.py +10 -5
  35. sonolus/script/runtime.py +78 -16
  36. sonolus/script/sprite.py +18 -1
  37. sonolus/script/text.py +9 -0
  38. sonolus/script/ui.py +20 -7
  39. sonolus/script/values.py +8 -5
  40. sonolus/script/vec.py +28 -0
  41. sonolus_py-0.2.1.dist-info/METADATA +10 -0
  42. {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.1.dist-info}/RECORD +46 -45
  43. {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.1.dist-info}/WHEEL +1 -1
  44. sonolus_py-0.1.9.dist-info/METADATA +0 -9
  45. /sonolus/script/{print.py → printing.py} +0 -0
  46. {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.1.dist-info}/entry_points.txt +0 -0
  47. {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,16 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ from abc import abstractmethod
4
5
  from collections.abc import Callable
5
6
  from dataclasses import dataclass
6
7
  from enum import Enum, StrEnum
7
8
  from types import FunctionType
8
9
  from typing import Annotated, Any, ClassVar, Self, TypedDict, get_origin
9
10
 
10
- from sonolus.backend.ir import IRConst, IRInstr
11
+ from sonolus.backend.ir import IRConst, IRExpr, IRInstr, IRPureInstr, IRStmt
11
12
  from sonolus.backend.mode import Mode
12
13
  from sonolus.backend.ops import Op
13
- from sonolus.backend.place import BlockPlace
14
14
  from sonolus.script.bucket import Bucket, Judgment
15
15
  from sonolus.script.internal.callbacks import PLAY_CALLBACKS, PREVIEW_CALLBACKS, WATCH_ARCHETYPE_CALLBACKS, CallbackInfo
16
16
  from sonolus.script.internal.context import ctx
@@ -19,9 +19,9 @@ from sonolus.script.internal.generic import validate_concrete_type
19
19
  from sonolus.script.internal.impl import meta_fn, validate_value
20
20
  from sonolus.script.internal.introspection import get_field_specifiers
21
21
  from sonolus.script.internal.native import native_call
22
- from sonolus.script.internal.value import Value
22
+ from sonolus.script.internal.value import BackingValue, DataValue, Value
23
23
  from sonolus.script.num import Num
24
- from sonolus.script.pointer import _deref
24
+ from sonolus.script.pointer import _backing_deref, _deref
25
25
  from sonolus.script.record import Record
26
26
  from sonolus.script.values import zeros
27
27
 
@@ -43,6 +43,17 @@ class _ArchetypeFieldInfo:
43
43
  storage: _StorageType
44
44
 
45
45
 
46
+ class _ExportBackingValue(BackingValue):
47
+ def __init__(self, index: IRExpr):
48
+ self.index = index
49
+
50
+ def read(self) -> IRExpr:
51
+ raise NotImplementedError("Exported fields are write-only")
52
+
53
+ def write(self, value: IRExpr) -> IRStmt:
54
+ return IRInstr(Op.ExportValue, [self.index, value])
55
+
56
+
46
57
  class _ArchetypeField(SonolusDescriptor):
47
58
  def __init__(self, name: str, data_name: str, storage: _StorageType, offset: int, type_: type[Value]):
48
59
  self.name = name
@@ -69,7 +80,20 @@ class _ArchetypeField(SonolusDescriptor):
69
80
  case _ArchetypeLevelData(values=values):
70
81
  result = values[self.name]
71
82
  case _StorageType.EXPORTED:
72
- raise RuntimeError("Exported fields are write-only")
83
+ match instance._data_:
84
+ case _ArchetypeSelfData():
85
+
86
+ def backing_source(i: IRExpr):
87
+ return _ExportBackingValue(IRPureInstr(Op.Add, [i, IRConst(self.offset)]))
88
+
89
+ result = _backing_deref(
90
+ backing_source,
91
+ self.type,
92
+ )
93
+ case _ArchetypeReferenceData():
94
+ raise RuntimeError("Exported fields of other entities are not accessible")
95
+ case _ArchetypeLevelData():
96
+ raise RuntimeError("Exported fields are not available in level data")
73
97
  case _StorageType.MEMORY:
74
98
  match instance._data_:
75
99
  case _ArchetypeSelfData():
@@ -177,6 +201,23 @@ def imported(*, name: str | None = None) -> Any:
177
201
  return _ArchetypeFieldInfo(name, _StorageType.IMPORTED)
178
202
 
179
203
 
204
+ def entity_data() -> Any:
205
+ """Declare a field as entity data.
206
+
207
+ Entity data is accessible from other entities, but may only be updated in the `preprocess` callback
208
+ and is read-only in other callbacks.
209
+
210
+ It functions like `imported`, except that it is not loaded from the level data.
211
+
212
+ Usage:
213
+ ```
214
+ class MyArchetype(PlayArchetype):
215
+ field: int = entity_data()
216
+ ```
217
+ """
218
+ return _ArchetypeFieldInfo(None, _StorageType.IMPORTED)
219
+
220
+
180
221
  def exported(*, name: str | None = None) -> Any:
181
222
  """Declare a field as exported.
182
223
 
@@ -418,7 +459,11 @@ class _BaseArchetype:
418
459
  bound.apply_defaults()
419
460
  data = []
420
461
  for field in cls._memory_fields_.values():
421
- data.extend(field.type._accept_(bound.arguments[field.name] or zeros(field.type))._to_list_())
462
+ data.extend(
463
+ field.type._accept_(
464
+ bound.arguments[field.name] if field.name in bound.arguments else zeros(field.type)
465
+ )._to_list_()
466
+ )
422
467
  native_call(Op.Spawn, archetype_id, *(Num(x) for x in data))
423
468
 
424
469
  @classmethod
@@ -446,9 +491,7 @@ class _BaseArchetype:
446
491
  raise TypeError("Cannot directly subclass Archetype, use the Archetype subclass for your mode")
447
492
  cls._default_callbacks_ = {getattr(cls, cb_info.py_name) for cb_info in cls._supported_callbacks_.values()}
448
493
  return
449
- if getattr(cls, "_callbacks_", None) is not None:
450
- raise TypeError("Cannot subclass Archetypes")
451
- if cls.name is None:
494
+ if cls.name is None or cls.name in {getattr(mro_entry, "name", None) for mro_entry in cls.mro()[1:]}:
452
495
  cls.name = cls.__name__
453
496
  cls._callbacks_ = []
454
497
  for name in cls._supported_callbacks_:
@@ -463,17 +506,32 @@ class _BaseArchetype:
463
506
  if cls._field_init_done:
464
507
  return
465
508
  cls._field_init_done = True
509
+ for mro_entry in cls.mro()[1:]:
510
+ if hasattr(mro_entry, "_field_init_done"):
511
+ mro_entry._init_fields()
466
512
  field_specifiers = get_field_specifiers(
467
513
  cls, skip={"name", "is_scored", "_callbacks_", "_field_init_done"}
468
514
  ).items()
469
- cls._imported_fields_ = {}
470
- cls._exported_fields_ = {}
471
- cls._memory_fields_ = {}
472
- cls._shared_memory_fields_ = {}
473
- imported_offset = 0
474
- exported_offset = 0
475
- memory_offset = 0
476
- shared_memory_offset = 0
515
+ if not hasattr(cls, "_imported_fields_"):
516
+ cls._imported_fields_ = {}
517
+ else:
518
+ cls._imported_fields_ = {**cls._imported_fields_}
519
+ if not hasattr(cls, "_exported_fields_"):
520
+ cls._exported_fields_ = {}
521
+ else:
522
+ cls._exported_fields_ = {**cls._exported_fields_}
523
+ if not hasattr(cls, "_memory_fields_"):
524
+ cls._memory_fields_ = {}
525
+ else:
526
+ cls._memory_fields_ = {**cls._memory_fields_}
527
+ if not hasattr(cls, "_shared_memory_fields_"):
528
+ cls._shared_memory_fields_ = {}
529
+ else:
530
+ cls._shared_memory_fields_ = {**cls._shared_memory_fields_}
531
+ imported_offset = sum(field.type._size_() for field in cls._imported_fields_.values())
532
+ exported_offset = sum(field.type._size_() for field in cls._exported_fields_.values())
533
+ memory_offset = sum(field.type._size_() for field in cls._memory_fields_.values())
534
+ shared_memory_offset = sum(field.type._size_() for field in cls._shared_memory_fields_.values())
477
535
  for name, value in field_specifiers:
478
536
  if value is ClassVar or get_origin(value) is ClassVar:
479
537
  continue
@@ -504,24 +562,32 @@ class _BaseArchetype:
504
562
  name, field_info.name or name, field_info.storage, imported_offset, field_type
505
563
  )
506
564
  imported_offset += field_type._size_()
565
+ if imported_offset > _ENTITY_DATA_SIZE:
566
+ raise ValueError("Imported fields exceed entity data size")
507
567
  setattr(cls, name, cls._imported_fields_[name])
508
568
  case _StorageType.EXPORTED:
509
569
  cls._exported_fields_[name] = _ArchetypeField(
510
570
  name, field_info.name or name, field_info.storage, exported_offset, field_type
511
571
  )
512
572
  exported_offset += field_type._size_()
573
+ if exported_offset > _ENTITY_DATA_SIZE:
574
+ raise ValueError("Exported fields exceed entity data size")
513
575
  setattr(cls, name, cls._exported_fields_[name])
514
576
  case _StorageType.MEMORY:
515
577
  cls._memory_fields_[name] = _ArchetypeField(
516
578
  name, field_info.name or name, field_info.storage, memory_offset, field_type
517
579
  )
518
580
  memory_offset += field_type._size_()
581
+ if memory_offset > _ENTITY_MEMORY_SIZE:
582
+ raise ValueError("Memory fields exceed entity memory size")
519
583
  setattr(cls, name, cls._memory_fields_[name])
520
584
  case _StorageType.SHARED:
521
585
  cls._shared_memory_fields_[name] = _ArchetypeField(
522
586
  name, field_info.name or name, field_info.storage, shared_memory_offset, field_type
523
587
  )
524
588
  shared_memory_offset += field_type._size_()
589
+ if shared_memory_offset > _ENTITY_SHARED_MEMORY_SIZE:
590
+ raise ValueError("Shared memory fields exceed entity shared memory size")
525
591
  setattr(cls, name, cls._shared_memory_fields_[name])
526
592
  cls._imported_keys_ = {
527
593
  name: i
@@ -541,6 +607,35 @@ class _BaseArchetype:
541
607
  cls._spawn_signature_ = inspect.Signature(
542
608
  [inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD) for name in cls._memory_fields_]
543
609
  )
610
+ cls._post_init_fields()
611
+
612
+ @property
613
+ @abstractmethod
614
+ def index(self) -> int:
615
+ """The index of this entity."""
616
+ raise NotImplementedError
617
+
618
+ @meta_fn
619
+ def ref(self) -> EntityRef[Self]:
620
+ """Get a reference to this entity.
621
+
622
+ Valid both in level data and in callbacks.
623
+ """
624
+ match self._data_:
625
+ case _ArchetypeSelfData():
626
+ return EntityRef[type(self)](index=self.index)
627
+ case _ArchetypeReferenceData(index=index):
628
+ return EntityRef[type(self)](index=index)
629
+ case _ArchetypeLevelData():
630
+ result = EntityRef[type(self)](index=-1)
631
+ result._ref_ = self
632
+ return result
633
+ case _:
634
+ raise RuntimeError("Invalid entity data")
635
+
636
+ @classmethod
637
+ def _post_init_fields(cls):
638
+ pass
544
639
 
545
640
 
546
641
  class PlayArchetype(_BaseArchetype):
@@ -682,6 +777,7 @@ class PlayArchetype(_BaseArchetype):
682
777
  return self._info.state == 2
683
778
 
684
779
  @property
780
+ @meta_fn
685
781
  def life(self) -> ArchetypeLife:
686
782
  """How this entity contributes to life."""
687
783
  if not ctx():
@@ -693,6 +789,7 @@ class PlayArchetype(_BaseArchetype):
693
789
  raise RuntimeError("Life is not available in level data")
694
790
 
695
791
  @property
792
+ @meta_fn
696
793
  def result(self) -> PlayEntityInput:
697
794
  """The result of this entity.
698
795
 
@@ -706,17 +803,6 @@ class PlayArchetype(_BaseArchetype):
706
803
  case _:
707
804
  raise RuntimeError("Result is only accessible from the entity itself")
708
805
 
709
- def ref(self):
710
- """Get a reference to this entity for creating level data.
711
-
712
- Not valid elsewhere.
713
- """
714
- if not isinstance(self._data_, _ArchetypeLevelData):
715
- raise RuntimeError("Entity is not level data")
716
- result = EntityRef[type(self)](index=-1)
717
- result._ref_ = self
718
- return result
719
-
720
806
 
721
807
  class WatchArchetype(_BaseArchetype):
722
808
  """Base class for watch mode archetypes.
@@ -801,6 +887,7 @@ class WatchArchetype(_BaseArchetype):
801
887
  return self._info.state == 1
802
888
 
803
889
  @property
890
+ @meta_fn
804
891
  def life(self) -> ArchetypeLife:
805
892
  """How this entity contributes to life."""
806
893
  if not ctx():
@@ -812,6 +899,7 @@ class WatchArchetype(_BaseArchetype):
812
899
  raise RuntimeError("Life is not available in level data")
813
900
 
814
901
  @property
902
+ @meta_fn
815
903
  def result(self) -> WatchEntityInput:
816
904
  """The result of this entity.
817
905
 
@@ -825,6 +913,11 @@ class WatchArchetype(_BaseArchetype):
825
913
  case _:
826
914
  raise RuntimeError("Result is only accessible from the entity itself")
827
915
 
916
+ @classmethod
917
+ def _post_init_fields(cls):
918
+ if cls._exported_fields_:
919
+ raise RuntimeError("Watch archetypes cannot have exported fields")
920
+
828
921
 
829
922
  class PreviewArchetype(_BaseArchetype):
830
923
  """Base class for preview mode archetypes.
@@ -857,6 +950,7 @@ class PreviewArchetype(_BaseArchetype):
857
950
  """
858
951
 
859
952
  @property
953
+ @meta_fn
860
954
  def _info(self) -> PreviewEntityInfo:
861
955
  if not ctx():
862
956
  raise RuntimeError("Calling info is only allowed within a callback")
@@ -873,6 +967,11 @@ class PreviewArchetype(_BaseArchetype):
873
967
  """The index of this entity."""
874
968
  return self._info.index
875
969
 
970
+ @classmethod
971
+ def _post_init_fields(cls):
972
+ if cls._exported_fields_:
973
+ raise RuntimeError("Preview archetypes cannot have exported fields")
974
+
876
975
 
877
976
  @meta_fn
878
977
  def entity_info_at(index: Num) -> PlayEntityInfo | WatchEntityInfo | PreviewEntityInfo:
@@ -974,24 +1073,38 @@ class WatchEntityInput(Record):
974
1073
 
975
1074
 
976
1075
  class EntityRef[A: _BaseArchetype](Record):
977
- """Reference to another entity."""
1076
+ """Reference to another entity.
1077
+
1078
+ May be used with `Any` to reference an unknown archetype.
1079
+
1080
+ Usage:
1081
+ ```
1082
+ class MyArchetype(PlayArchetype):
1083
+ ref_1: EntityRef[OtherArchetype] = imported()
1084
+ ref_2: EntityRef[Any] = imported()
1085
+ ```
1086
+ """
978
1087
 
979
1088
  index: int
980
1089
 
981
1090
  @classmethod
982
1091
  def archetype(cls) -> type[A]:
1092
+ """Get the archetype type."""
983
1093
  return cls.type_var_value(A)
984
1094
 
985
1095
  def with_archetype(self, archetype: type[A]) -> EntityRef[A]:
1096
+ """Return a new reference with the given archetype type."""
986
1097
  return EntityRef[archetype](index=self.index)
987
1098
 
988
1099
  def get(self) -> A:
1100
+ """Get the entity."""
989
1101
  return self.archetype().at(self.index)
990
1102
 
991
- def is_valid(self) -> bool:
1103
+ def archetype_matches(self) -> bool:
1104
+ """Check if entity at the index is precisely of the archetype."""
992
1105
  return self.index >= 0 and self.archetype().is_at(self.index)
993
1106
 
994
- def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[float | str | BlockPlace]:
1107
+ def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
995
1108
  ref = getattr(self, "_ref_", None)
996
1109
  if ref is None:
997
1110
  return [self.index]
@@ -1000,9 +1113,18 @@ class EntityRef[A: _BaseArchetype](Record):
1000
1113
  raise KeyError("Reference to entity not in level data")
1001
1114
  return [level_refs[ref]]
1002
1115
 
1116
+ def _copy_from_(self, value: Self):
1117
+ super()._copy_from_(value)
1118
+ if hasattr(value, "_ref_"):
1119
+ self._ref_ = value._ref_
1120
+
1003
1121
  @classmethod
1004
1122
  def _accepts_(cls, value: Any) -> bool:
1005
- return super()._accepts_(value) or (cls._type_args_ and cls.archetype() is Any and isinstance(value, EntityRef))
1123
+ return (
1124
+ super()._accepts_(value)
1125
+ or (cls._type_args_ and cls.archetype() is Any and isinstance(value, EntityRef))
1126
+ or (issubclass(type(value), EntityRef) and issubclass(value.archetype(), cls.archetype()))
1127
+ )
1006
1128
 
1007
1129
  @classmethod
1008
1130
  def _accept_(cls, value: Any) -> Self:
sonolus/script/array.py CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
  from collections.abc import Iterable
5
5
  from typing import Any, Self, final
6
6
 
7
+ from sonolus.backend.ir import IRConst, IRSet
7
8
  from sonolus.backend.place import BlockPlace
8
9
  from sonolus.script.array_like import ArrayLike
9
10
  from sonolus.script.debug import assert_unreachable
@@ -11,7 +12,7 @@ from sonolus.script.internal.context import ctx
11
12
  from sonolus.script.internal.error import InternalError
12
13
  from sonolus.script.internal.generic import GenericValue
13
14
  from sonolus.script.internal.impl import meta_fn, validate_value
14
- from sonolus.script.internal.value import Value
15
+ from sonolus.script.internal.value import DataValue, Value
15
16
  from sonolus.script.num import Num
16
17
 
17
18
 
@@ -107,11 +108,11 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
107
108
  return self
108
109
 
109
110
  @classmethod
110
- def _from_list_(cls, values: Iterable[float | BlockPlace]) -> Self:
111
+ def _from_list_(cls, values: Iterable[DataValue]) -> Self:
111
112
  iterator = iter(values)
112
113
  return cls(*(cls.element_type()._from_list_(iterator) for _ in range(cls.size())))
113
114
 
114
- def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[float | str | BlockPlace]:
115
+ def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
115
116
  match self._value:
116
117
  case list():
117
118
  return [entry for value in self._value for entry in value._to_list_(level_refs)]
@@ -159,6 +160,16 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
159
160
  else:
160
161
  return cls._with_value([cls.element_type()._alloc_() for _ in range(cls.size())])
161
162
 
163
+ @classmethod
164
+ def _zero_(cls) -> Self:
165
+ if ctx():
166
+ place = ctx().alloc(size=cls._size_())
167
+ result: Self = cls._from_place_(place)
168
+ ctx().add_statements(*[IRSet(place.add_offset(i), IRConst(0)) for i in range(cls._size_())])
169
+ return result
170
+ else:
171
+ return cls._with_value([cls.element_type()._zero_() for _ in range(cls.size())])
172
+
162
173
  def __len__(self):
163
174
  return self.size()
164
175
 
@@ -158,10 +158,14 @@ class ArrayLike[T](Sequence, ABC):
158
158
  return min_index
159
159
 
160
160
  def _max_(self, key: Callable[T, Any] | None = None) -> T:
161
- return self[self.index_of_max(key=key)]
161
+ index = self.index_of_max(key=key)
162
+ assert index != -1
163
+ return self[index]
162
164
 
163
165
  def _min_(self, key: Callable[T, Any] | None = None) -> T:
164
- return self[self.index_of_min(key=key)]
166
+ index = self.index_of_min(key=key)
167
+ assert index != -1
168
+ return self[index]
165
169
 
166
170
  def swap(self, i: Num, j: Num, /):
167
171
  """Swap the values at the given indices.
@@ -1,13 +1,44 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from sonolus.backend.visitor import compile_and_call
3
4
  from sonolus.script.array import Array
4
5
  from sonolus.script.array_like import ArrayLike
5
6
  from sonolus.script.debug import error
7
+ from sonolus.script.internal.context import ctx
8
+ from sonolus.script.internal.impl import meta_fn
6
9
  from sonolus.script.iterator import SonolusIterator
10
+ from sonolus.script.num import Num
11
+ from sonolus.script.pointer import _deref
7
12
  from sonolus.script.record import Record
8
13
  from sonolus.script.values import alloc, copy
9
14
 
10
15
 
16
+ class Box[T](Record):
17
+ """A box that contains a value.
18
+
19
+ This can be helpful for generic code that can handle both Num and non-Num types.
20
+
21
+ Usage:
22
+ ```python
23
+ Box[T](value: T)
24
+ ```
25
+
26
+ Examples:
27
+ ```python
28
+ box = Box(1)
29
+ box = Box[int](2)
30
+
31
+ x: T = ...
32
+ y: T = ...
33
+ box = Box(x)
34
+ box.value = y # Works regardless of whether x is a Num or not
35
+ ```
36
+ """
37
+
38
+ value: T
39
+ """The value contained in the box."""
40
+
41
+
11
42
  class Pair[T, U](Record):
12
43
  """A generic pair of values.
13
44
 
@@ -129,6 +160,17 @@ class VarArray[T, Capacity](Record, ArrayLike[T]):
129
160
  self._array[self._size] = value
130
161
  self._size += 1
131
162
 
163
+ def append_unchecked(self, value: T):
164
+ """Append the given value to the end of the array without checking the capacity.
165
+
166
+ Use with caution as this may cause hard to debug issues if the array is full.
167
+
168
+ Args:
169
+ value: The value to append.
170
+ """
171
+ self._array[self._size] = value
172
+ self._size += 1
173
+
132
174
  def extend(self, values: ArrayLike[T]):
133
175
  """Appends copies of the values in the given array to the end of the array.
134
176
 
@@ -258,6 +300,130 @@ class VarArray[T, Capacity](Record, ArrayLike[T]):
258
300
  raise TypeError("unhashable type: 'VarArray'")
259
301
 
260
302
 
303
+ class ArrayPointer[T](Record, ArrayLike[T]):
304
+ """An array defined by a size and pointer to the first element.
305
+
306
+ This is intended to be created internally and improper use may result in hard to debug issues.
307
+
308
+ Usage:
309
+ ```python
310
+ ArrayPointer[T](size: int, block: int, offset: int)
311
+ ```
312
+ """
313
+
314
+ size: int
315
+ block: int
316
+ offset: int
317
+
318
+ def __len__(self) -> int:
319
+ """Return the number of elements in the array."""
320
+ return self.size
321
+
322
+ @classmethod
323
+ def element_type(cls) -> type[T]:
324
+ """Return the type of the elements in the array."""
325
+ return cls.type_var_value(T)
326
+
327
+ def _check_index(self, index: int):
328
+ assert 0 <= index < self.size
329
+
330
+ @meta_fn
331
+ def _get_item(self, item: int) -> T:
332
+ if not ctx():
333
+ raise TypeError("ArrayPointer values cannot be accessed outside of a context")
334
+ return _deref(
335
+ self.block,
336
+ self.offset + Num._accept_(item) * Num._accept_(self.element_type()._size_()),
337
+ self.element_type(),
338
+ )
339
+
340
+ @meta_fn
341
+ def __getitem__(self, item: int) -> T:
342
+ compile_and_call(self._check_index, item)
343
+ return self._get_item(item)._get_()
344
+
345
+ @meta_fn
346
+ def __setitem__(self, key: int, value: T):
347
+ compile_and_call(self._check_index, key)
348
+ dst = self._get_item(key)
349
+ if self.element_type()._is_value_type_():
350
+ dst._set_(value)
351
+ else:
352
+ dst._copy_from__(value)
353
+
354
+
355
+ class ArraySet[T, Capacity](Record):
356
+ """A set implemented as an array with a fixed maximum capacity.
357
+
358
+ Usage:
359
+ ```python
360
+ ArraySet[T, Capacity].new() # Create a new empty set
361
+ ```
362
+
363
+ Examples:
364
+ ```python
365
+ s = ArraySet[int, 10].new()
366
+ s.add(1)
367
+ s.add(2)
368
+ assert 1 in s
369
+ assert 3 not in s
370
+ s.remove(1)
371
+ assert 1 not in s
372
+ ```
373
+ """
374
+
375
+ _values: VarArray[T, Capacity]
376
+
377
+ @classmethod
378
+ def new(cls):
379
+ """Create a new empty set."""
380
+ element_type = cls.type_var_value(T)
381
+ capacity = cls.type_var_value(Capacity)
382
+ return cls(VarArray[element_type, capacity].new())
383
+
384
+ def __len__(self):
385
+ """Return the number of elements in the set."""
386
+ return len(self._values)
387
+
388
+ def __contains__(self, value):
389
+ """Return whether the given value is present in the set."""
390
+ return value in self._values
391
+
392
+ def __iter__(self):
393
+ """Return an iterator over the values in the set."""
394
+ return self._values.__iter__()
395
+
396
+ def add(self, value: T) -> bool:
397
+ """Add a copy of the given value to the set.
398
+
399
+ This has no effect and returns False if the value is already present or if the set is full.
400
+
401
+ Args:
402
+ value: The value to add.
403
+
404
+ Returns:
405
+ True if the value was added, False otherwise.
406
+ """
407
+ return self._values.set_add(value)
408
+
409
+ def remove(self, value: T) -> bool:
410
+ """Remove the given value from the set.
411
+
412
+ This has no effect and returns False if the value is not present.
413
+
414
+ Args:
415
+ value: The value to remove.
416
+
417
+ Returns:
418
+ True if the value was removed, False otherwise.
419
+ """
420
+ return self._values.set_remove(value)
421
+
422
+ def clear(self):
423
+ """Clear the set, removing all elements."""
424
+ self._values.clear()
425
+
426
+
261
427
  class _ArrayMapEntry[K, V](Record):
262
428
  key: K
263
429
  value: V
sonolus/script/debug.py CHANGED
@@ -1,4 +1,4 @@
1
- from collections.abc import Callable
1
+ from collections.abc import Callable, Sequence
2
2
  from contextvars import ContextVar
3
3
  from typing import Any, Never
4
4
 
@@ -16,8 +16,13 @@ debug_log_callback = ContextVar[Callable[[Num], None]]("debug_log_callback")
16
16
 
17
17
 
18
18
  @meta_fn
19
- def error(message: str | None = None) -> None:
20
- message = message._as_py_() if message is not None else "Error"
19
+ def error(message: str | None = None) -> Never:
20
+ """Raise an error.
21
+
22
+ This function is used to raise an error during runtime.
23
+ When this happens, the game will pause in debug mode. The current callback will also immediately return 0.
24
+ """
25
+ message = validate_value(message)._as_py_() if message is not None else "Error"
21
26
  if not isinstance(message, str):
22
27
  raise ValueError("Expected a string")
23
28
  if ctx():
@@ -28,6 +33,19 @@ def error(message: str | None = None) -> None:
28
33
  raise RuntimeError(message)
29
34
 
30
35
 
36
+ @meta_fn
37
+ def static_error(message: str | None = None) -> Never:
38
+ """Raise a static error.
39
+
40
+ This function is used to raise an error during compile-time if the compiler cannot guarantee that
41
+ this function will not be called during runtime.
42
+ """
43
+ message = validate_value(message)._as_py_() if message is not None else "Error"
44
+ if not isinstance(message, str):
45
+ raise ValueError("Expected a string")
46
+ raise RuntimeError(message)
47
+
48
+
31
49
  @meta_fn
32
50
  def debug_log(value: Num):
33
51
  """Log a value in debug mode."""
@@ -75,7 +93,7 @@ def terminate():
75
93
  raise RuntimeError("Terminated")
76
94
 
77
95
 
78
- def visualize_cfg(fn: Callable[[], Any], passes: list[CompilerPass] | None = None) -> str:
96
+ def visualize_cfg(fn: Callable[[], Any], passes: Sequence[CompilerPass] | None = None) -> str:
79
97
  from sonolus.build.compile import callback_to_cfg
80
98
 
81
99
  if passes is None:
sonolus/script/effect.py CHANGED
@@ -25,7 +25,7 @@ class Effect(Record):
25
25
  """Return whether the effect clip is available."""
26
26
  return _has_effect_clip(self.id)
27
27
 
28
- def play(self, distance: float) -> None:
28
+ def play(self, distance: float = 0) -> None:
29
29
  """Play the effect clip.
30
30
 
31
31
  If the clip was already played within the specified distance, it will be skipped.
@@ -35,7 +35,7 @@ class Effect(Record):
35
35
  """
36
36
  _play(self.id, distance)
37
37
 
38
- def schedule(self, time: float, distance: float) -> None:
38
+ def schedule(self, time: float, distance: float = 0) -> None:
39
39
  """Schedule the effect clip to play at a specific time.
40
40
 
41
41
  This is not suitable for real-time effects such as responses to user input. Use `play` instead.