sonolus.py 0.2.0__py3-none-any.whl → 0.3.0__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.
sonolus/backend/ops.py CHANGED
@@ -180,6 +180,12 @@ class Op(StrEnum):
180
180
  StackSetPointer = ("StackSetPointer", True, False, False)
181
181
  StopLooped = ("StopLooped", True, False, False)
182
182
  StopLoopedScheduled = ("StopLoopedScheduled", True, False, False)
183
+ # Streams are immutable when they're readable, so we can treat read operations as pure.
184
+ StreamGetNextKey = ("StreamGetNextKey", False, True, False)
185
+ StreamGetPreviousKey = ("StreamGetPreviousKey", False, True, False)
186
+ StreamGetValue = ("StreamGetValue", False, True, False)
187
+ StreamHas = ("StreamHas", False, True, False)
188
+ StreamSet = ("StreamSet", True, False, False)
183
189
  Subtract = ("Subtract", False, True, False)
184
190
  Switch = ("Switch", False, True, True)
185
191
  SwitchInteger = ("SwitchInteger", False, True, True)
@@ -8,10 +8,9 @@ from enum import Enum, StrEnum
8
8
  from types import FunctionType
9
9
  from typing import Annotated, Any, ClassVar, Self, TypedDict, get_origin
10
10
 
11
- from sonolus.backend.ir import IRConst, IRInstr
11
+ from sonolus.backend.ir import IRConst, IRExpr, IRInstr, IRPureInstr, IRStmt
12
12
  from sonolus.backend.mode import Mode
13
13
  from sonolus.backend.ops import Op
14
- from sonolus.backend.place import BlockPlace
15
14
  from sonolus.script.bucket import Bucket, Judgment
16
15
  from sonolus.script.internal.callbacks import PLAY_CALLBACKS, PREVIEW_CALLBACKS, WATCH_ARCHETYPE_CALLBACKS, CallbackInfo
17
16
  from sonolus.script.internal.context import ctx
@@ -20,9 +19,9 @@ from sonolus.script.internal.generic import validate_concrete_type
20
19
  from sonolus.script.internal.impl import meta_fn, validate_value
21
20
  from sonolus.script.internal.introspection import get_field_specifiers
22
21
  from sonolus.script.internal.native import native_call
23
- from sonolus.script.internal.value import Value
22
+ from sonolus.script.internal.value import BackingValue, DataValue, Value
24
23
  from sonolus.script.num import Num
25
- from sonolus.script.pointer import _deref
24
+ from sonolus.script.pointer import _backing_deref, _deref
26
25
  from sonolus.script.record import Record
27
26
  from sonolus.script.values import zeros
28
27
 
@@ -44,6 +43,17 @@ class _ArchetypeFieldInfo:
44
43
  storage: _StorageType
45
44
 
46
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
+
47
57
  class _ArchetypeField(SonolusDescriptor):
48
58
  def __init__(self, name: str, data_name: str, storage: _StorageType, offset: int, type_: type[Value]):
49
59
  self.name = name
@@ -70,7 +80,20 @@ class _ArchetypeField(SonolusDescriptor):
70
80
  case _ArchetypeLevelData(values=values):
71
81
  result = values[self.name]
72
82
  case _StorageType.EXPORTED:
73
- 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")
74
97
  case _StorageType.MEMORY:
75
98
  match instance._data_:
76
99
  case _ArchetypeSelfData():
@@ -584,6 +607,7 @@ class _BaseArchetype:
584
607
  cls._spawn_signature_ = inspect.Signature(
585
608
  [inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD) for name in cls._memory_fields_]
586
609
  )
610
+ cls._post_init_fields()
587
611
 
588
612
  @property
589
613
  @abstractmethod
@@ -609,6 +633,10 @@ class _BaseArchetype:
609
633
  case _:
610
634
  raise RuntimeError("Invalid entity data")
611
635
 
636
+ @classmethod
637
+ def _post_init_fields(cls):
638
+ pass
639
+
612
640
 
613
641
  class PlayArchetype(_BaseArchetype):
614
642
  """Base class for play mode archetypes.
@@ -651,7 +679,7 @@ class PlayArchetype(_BaseArchetype):
651
679
  def should_spawn(self) -> bool:
652
680
  """Return whether the entity should be spawned.
653
681
 
654
- Runs when this entity is first in the spawn queue.
682
+ Runs each frame while the entity is the first entity in the spawn queue.
655
683
  """
656
684
 
657
685
  def initialize(self):
@@ -885,6 +913,11 @@ class WatchArchetype(_BaseArchetype):
885
913
  case _:
886
914
  raise RuntimeError("Result is only accessible from the entity itself")
887
915
 
916
+ @classmethod
917
+ def _post_init_fields(cls):
918
+ if cls._exported_fields_:
919
+ raise RuntimeError("Watch archetypes cannot have exported fields")
920
+
888
921
 
889
922
  class PreviewArchetype(_BaseArchetype):
890
923
  """Base class for preview mode archetypes.
@@ -934,6 +967,11 @@ class PreviewArchetype(_BaseArchetype):
934
967
  """The index of this entity."""
935
968
  return self._info.index
936
969
 
970
+ @classmethod
971
+ def _post_init_fields(cls):
972
+ if cls._exported_fields_:
973
+ raise RuntimeError("Preview archetypes cannot have exported fields")
974
+
937
975
 
938
976
  @meta_fn
939
977
  def entity_info_at(index: Num) -> PlayEntityInfo | WatchEntityInfo | PreviewEntityInfo:
@@ -1066,7 +1104,7 @@ class EntityRef[A: _BaseArchetype](Record):
1066
1104
  """Check if entity at the index is precisely of the archetype."""
1067
1105
  return self.index >= 0 and self.archetype().is_at(self.index)
1068
1106
 
1069
- 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]:
1070
1108
  ref = getattr(self, "_ref_", None)
1071
1109
  if ref is None:
1072
1110
  return [self.index]
sonolus/script/array.py CHANGED
@@ -12,7 +12,7 @@ from sonolus.script.internal.context import ctx
12
12
  from sonolus.script.internal.error import InternalError
13
13
  from sonolus.script.internal.generic import GenericValue
14
14
  from sonolus.script.internal.impl import meta_fn, validate_value
15
- from sonolus.script.internal.value import Value
15
+ from sonolus.script.internal.value import BackingSource, DataValue, Value
16
16
  from sonolus.script.num import Num
17
17
 
18
18
 
@@ -27,16 +27,14 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
27
27
  ```
28
28
  """
29
29
 
30
- _value: list[T] | BlockPlace
30
+ _value: list[T] | BlockPlace | BackingSource
31
31
 
32
32
  @classmethod
33
- @meta_fn
34
33
  def element_type(cls) -> type[T] | type[Value]:
35
34
  """Return the type of elements in this array type."""
36
35
  return cls.type_var_value(T)
37
36
 
38
37
  @classmethod
39
- @meta_fn
40
38
  def size(cls) -> int:
41
39
  """Return the size of this array type.
42
40
 
@@ -85,6 +83,10 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
85
83
  def _is_value_type_(cls) -> bool:
86
84
  return False
87
85
 
86
+ @classmethod
87
+ def _from_backing_source_(cls, source: BackingSource) -> Self:
88
+ return cls._with_value(source)
89
+
88
90
  @classmethod
89
91
  def _from_place_(cls, place: BlockPlace) -> Self:
90
92
  return cls._with_value(place)
@@ -108,11 +110,11 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
108
110
  return self
109
111
 
110
112
  @classmethod
111
- def _from_list_(cls, values: Iterable[float | BlockPlace]) -> Self:
113
+ def _from_list_(cls, values: Iterable[DataValue]) -> Self:
112
114
  iterator = iter(values)
113
115
  return cls(*(cls.element_type()._from_list_(iterator) for _ in range(cls.size())))
114
116
 
115
- def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[float | str | BlockPlace]:
117
+ def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
116
118
  match self._value:
117
119
  case list():
118
120
  return [entry for value in self._value for entry in value._to_list_(level_refs)]
@@ -124,6 +126,8 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
124
126
  ._from_place_(self._value.add_offset(i * self.element_type()._size_()))
125
127
  ._to_list_()
126
128
  ]
129
+ case backing_source if callable(backing_source):
130
+ return [backing_source(IRConst(i)) for i in range(self.size() * self.element_type()._size_())]
127
131
  case _:
128
132
  assert_unreachable()
129
133
 
@@ -135,7 +139,7 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
135
139
  return self
136
140
 
137
141
  def _set_(self, value: Self):
138
- raise TypeError("Array does not support set_")
142
+ raise TypeError("Array does not support _set_")
139
143
 
140
144
  def _copy_from_(self, value: Self):
141
145
  if not isinstance(value, type(self)):
@@ -150,6 +154,7 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
150
154
  result._copy_from_(self)
151
155
  return result
152
156
  else:
157
+ assert isinstance(self._value, list)
153
158
  return self._with_value([value._copy_() for value in self._value])
154
159
 
155
160
  @classmethod
@@ -186,22 +191,36 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
186
191
  return self._value[const_index]._get_()
187
192
  else:
188
193
  return self._value[const_index]._get_()._as_py_()
189
- else:
194
+ elif isinstance(self._value, BlockPlace):
190
195
  return (
191
196
  self.element_type()
192
197
  ._from_place_(self._value.add_offset(const_index * self.element_type()._size_()))
193
198
  ._get_()
194
199
  )
200
+ elif callable(self._value):
201
+ return self.element_type()._from_backing_source_(
202
+ lambda offset: self._value((Num(offset) + Num(const_index * self.element_type()._size_())).ir())
203
+ )
204
+ else:
205
+ raise InternalError("Unexpected array value")
195
206
  else:
196
207
  if not ctx():
197
208
  raise InternalError("Unexpected non-constant index")
198
- base = ctx().rom[tuple(self._to_list_())] if isinstance(self._value, list) else self._value
199
- place = BlockPlace(
200
- block=base.block,
201
- index=(Num(base.index) + index * self.element_type()._size_()).index(),
202
- offset=base.offset,
203
- )
204
- return self.element_type()._from_place_(place)._get_()
209
+ if isinstance(self._value, list | BlockPlace):
210
+ base = ctx().rom[tuple(self._to_list_())] if isinstance(self._value, list) else self._value
211
+ place = BlockPlace(
212
+ block=base.block,
213
+ index=(Num(base.index) + index * self.element_type()._size_()).index(),
214
+ offset=base.offset,
215
+ )
216
+ return self.element_type()._from_place_(place)._get_()
217
+ elif callable(self._value):
218
+ base_offset = index * Num(self.element_type()._size_())
219
+ return self.element_type()._from_backing_source_(
220
+ lambda offset: self._value((Num(offset) + base_offset).ir())
221
+ )
222
+ else:
223
+ raise InternalError("Unexpected array value")
205
224
 
206
225
  @meta_fn
207
226
  def __setitem__(self, index: Num, value: T):
@@ -210,17 +229,25 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
210
229
  if ctx():
211
230
  if isinstance(self._value, list):
212
231
  raise ValueError("Cannot mutate a compile time constant array")
213
- base = self._value
214
- place = (
215
- base.add_offset(int(index._as_py_()) * self.element_type()._size_())
216
- if index._is_py_()
217
- else BlockPlace(
218
- block=base.block,
219
- index=(Num(base.index) + index * self.element_type()._size_()).index(),
220
- offset=base.offset,
232
+ elif isinstance(self._value, BlockPlace):
233
+ base = self._value
234
+ place = (
235
+ base.add_offset(int(index._as_py_()) * self.element_type()._size_())
236
+ if index._is_py_()
237
+ else BlockPlace(
238
+ block=base.block,
239
+ index=(Num(base.index) + index * self.element_type()._size_()).index(),
240
+ offset=base.offset,
241
+ )
242
+ )
243
+ dst = self.element_type()._from_place_(place)
244
+ elif callable(self._value):
245
+ base_offset = index * Num(self.element_type()._size_())
246
+ dst = self.element_type()._from_backing_source_(
247
+ lambda offset: self._value((Num(offset) + base_offset).ir())
221
248
  )
222
- )
223
- dst = self.element_type()._from_place_(place)
249
+ else:
250
+ raise InternalError("Unexpected array value")
224
251
  if self.element_type()._is_value_type_():
225
252
  dst._set_(value)
226
253
  else:
@@ -259,11 +286,13 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
259
286
  return hash(tuple(self[i] for i in range(self.size())))
260
287
 
261
288
  def __str__(self):
262
- if isinstance(self._value, BlockPlace):
289
+ if isinstance(self._value, BlockPlace) or callable(self._value):
263
290
  return f"{type(self).__name__}({self._value}...)"
264
- return f"{type(self).__name__}({", ".join(str(self[i]) for i in range(self.size()))})"
291
+ else:
292
+ return f"{type(self).__name__}({", ".join(str(self[i]) for i in range(self.size()))})"
265
293
 
266
294
  def __repr__(self):
267
- if isinstance(self._value, BlockPlace):
295
+ if isinstance(self._value, BlockPlace) or callable(self._value):
268
296
  return f"{type(self).__name__}({self._value}...)"
269
- return f"{type(self).__name__}({", ".join(repr(self[i]) for i in range(self.size()))})"
297
+ else:
298
+ return f"{type(self).__name__}({", ".join(repr(self[i]) for i in range(self.size()))})"
@@ -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.
@@ -184,6 +188,7 @@ class ArrayLike[T](Sequence, ABC):
184
188
  if len(self) < 15 or key is not None:
185
189
  if key is None:
186
190
  key = _identity
191
+ # May be worth adding a block sort variant for better performance on large arrays in the future
187
192
  _insertion_sort(self, 0, len(self), key, reverse)
188
193
  else:
189
194
  # Heap sort is unstable, so if there's a key, we can't rely on it
sonolus/script/bucket.py CHANGED
@@ -20,7 +20,7 @@ class JudgmentWindow(Record):
20
20
  """The window for judging the accuracy of a hit.
21
21
 
22
22
  Usage:
23
- ```
23
+ ```python
24
24
  JudgmentWindow(perfect: Interval, great: Interval, good: Interval)
25
25
  ```
26
26
  """
@@ -62,7 +62,7 @@ class JudgmentWindow(Record):
62
62
  target: The target time of the hit.
63
63
 
64
64
  Returns:
65
- The judgment of the hit.
65
+ The [`Judgment`][sonolus.script.bucket.Judgment] of the hit.
66
66
  """
67
67
  return _judge(
68
68
  actual,
@@ -133,7 +133,9 @@ class Bucket(Record):
133
133
  """A bucket for entity judgment results.
134
134
 
135
135
  Usage:
136
- ```Bucket(id: int)```
136
+ ```python
137
+ Bucket(id: int)
138
+ ```
137
139
  """
138
140
 
139
141
  id: int
@@ -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
@@ -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."""
sonolus/script/engine.py CHANGED
@@ -4,7 +4,7 @@ import json
4
4
  from collections.abc import Callable
5
5
  from os import PathLike
6
6
  from pathlib import Path
7
- from typing import Any
7
+ from typing import Any, Literal
8
8
 
9
9
  from sonolus.build.collection import Asset, load_asset
10
10
  from sonolus.script.archetype import PlayArchetype, PreviewArchetype, WatchArchetype, _BaseArchetype
@@ -81,7 +81,7 @@ class Engine:
81
81
  meta: Additional metadata of the engine.
82
82
  """
83
83
 
84
- version = 12
84
+ version: Literal[13] = 13
85
85
 
86
86
  def __init__(
87
87
  self,
@@ -3,7 +3,7 @@ from typing import Any, ClassVar, Self
3
3
 
4
4
  from sonolus.backend.place import BlockPlace
5
5
  from sonolus.script.internal.impl import meta_fn
6
- from sonolus.script.internal.value import Value
6
+ from sonolus.script.internal.value import DataValue, Value
7
7
 
8
8
 
9
9
  class _Missing:
@@ -18,6 +18,8 @@ _MISSING = _Missing()
18
18
 
19
19
 
20
20
  class ConstantValue(Value):
21
+ """Wraps a python constant value usable in Sonolus scripts."""
22
+
21
23
  _parameterized_: ClassVar[dict[Any, type[Self]]] = {}
22
24
  _value: ClassVar[Any] = _MISSING
23
25
  instance: ClassVar[Self | _Missing] = _MISSING
@@ -94,10 +96,10 @@ class ConstantValue(Value):
94
96
  return self.value()
95
97
 
96
98
  @classmethod
97
- def _from_list_(cls, values: Iterable[float | BlockPlace]) -> Self:
99
+ def _from_list_(cls, values: Iterable[DataValue]) -> Self:
98
100
  return cls()
99
101
 
100
- def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[float | str | BlockPlace]:
102
+ def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
101
103
  return []
102
104
 
103
105
  @classmethod
@@ -139,5 +141,5 @@ class ConstantValue(Value):
139
141
  return hash(self.value())
140
142
 
141
143
 
142
- class MiscConstantValue(ConstantValue):
144
+ class BasicConstantValue(ConstantValue):
143
145
  """For constants without any special behavior."""
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import typing
3
4
  from enum import Enum
5
+ from types import UnionType
4
6
  from typing import Annotated, Any, ClassVar, Literal, Self, TypeVar, get_origin
5
7
 
6
8
  from sonolus.script.internal.impl import meta_fn, validate_value
@@ -31,6 +33,11 @@ def validate_type_spec(spec: Any) -> PartialGeneric | TypeVar | type[Value]:
31
33
  spec = validate_type_spec(spec.__mro__[1])
32
34
  if isinstance(spec, PartialGeneric | TypeVar) or (isinstance(spec, type) and issubclass(spec, Value)):
33
35
  return spec
36
+ if typing.get_origin(spec) is UnionType:
37
+ args = typing.get_args(spec)
38
+ validated_args = {validate_type_arg(arg) for arg in args}
39
+ if len(validated_args) == 1:
40
+ return validated_args.pop()
34
41
  raise TypeError(f"Invalid type spec: {spec}")
35
42
 
36
43