sonolus.py 0.3.4__py3-none-any.whl → 0.4.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 (64) hide show
  1. sonolus/backend/excepthook.py +30 -0
  2. sonolus/backend/finalize.py +15 -1
  3. sonolus/backend/ops.py +4 -0
  4. sonolus/backend/optimize/allocate.py +5 -5
  5. sonolus/backend/optimize/constant_evaluation.py +124 -19
  6. sonolus/backend/optimize/copy_coalesce.py +15 -12
  7. sonolus/backend/optimize/dead_code.py +7 -6
  8. sonolus/backend/optimize/dominance.py +2 -2
  9. sonolus/backend/optimize/flow.py +54 -8
  10. sonolus/backend/optimize/inlining.py +137 -30
  11. sonolus/backend/optimize/liveness.py +2 -2
  12. sonolus/backend/optimize/optimize.py +15 -1
  13. sonolus/backend/optimize/passes.py +11 -3
  14. sonolus/backend/optimize/simplify.py +137 -8
  15. sonolus/backend/optimize/ssa.py +47 -13
  16. sonolus/backend/place.py +5 -4
  17. sonolus/backend/utils.py +44 -16
  18. sonolus/backend/visitor.py +288 -17
  19. sonolus/build/cli.py +47 -19
  20. sonolus/build/compile.py +12 -5
  21. sonolus/build/engine.py +70 -1
  22. sonolus/build/level.py +3 -3
  23. sonolus/build/project.py +2 -2
  24. sonolus/script/archetype.py +12 -9
  25. sonolus/script/array.py +23 -18
  26. sonolus/script/array_like.py +26 -29
  27. sonolus/script/bucket.py +1 -1
  28. sonolus/script/containers.py +22 -26
  29. sonolus/script/debug.py +20 -43
  30. sonolus/script/effect.py +1 -1
  31. sonolus/script/globals.py +3 -3
  32. sonolus/script/instruction.py +2 -2
  33. sonolus/script/internal/builtin_impls.py +155 -28
  34. sonolus/script/internal/constant.py +13 -3
  35. sonolus/script/internal/context.py +46 -15
  36. sonolus/script/internal/impl.py +9 -3
  37. sonolus/script/internal/introspection.py +8 -1
  38. sonolus/script/internal/native.py +2 -2
  39. sonolus/script/internal/range.py +8 -11
  40. sonolus/script/internal/simulation_context.py +1 -1
  41. sonolus/script/internal/transient.py +2 -2
  42. sonolus/script/internal/value.py +41 -3
  43. sonolus/script/interval.py +13 -13
  44. sonolus/script/iterator.py +53 -107
  45. sonolus/script/level.py +2 -2
  46. sonolus/script/maybe.py +241 -0
  47. sonolus/script/num.py +29 -14
  48. sonolus/script/options.py +1 -1
  49. sonolus/script/particle.py +1 -1
  50. sonolus/script/project.py +24 -5
  51. sonolus/script/quad.py +15 -15
  52. sonolus/script/record.py +48 -44
  53. sonolus/script/runtime.py +22 -18
  54. sonolus/script/sprite.py +1 -1
  55. sonolus/script/stream.py +66 -82
  56. sonolus/script/transform.py +35 -34
  57. sonolus/script/values.py +10 -10
  58. sonolus/script/vec.py +21 -18
  59. {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.1.dist-info}/METADATA +1 -1
  60. sonolus_py-0.4.1.dist-info/RECORD +93 -0
  61. sonolus_py-0.3.4.dist-info/RECORD +0 -92
  62. {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.1.dist-info}/WHEEL +0 -0
  63. {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.1.dist-info}/entry_points.txt +0 -0
  64. {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- from abc import abstractmethod
3
+ from collections.abc import Iterator
4
4
  from typing import Any
5
5
 
6
+ from sonolus.script.internal.context import ctx
6
7
  from sonolus.script.internal.impl import meta_fn
8
+ from sonolus.script.maybe import Maybe, Nothing, Some
7
9
  from sonolus.script.record import Record
8
10
 
9
11
 
@@ -12,57 +14,28 @@ class SonolusIterator[T]:
12
14
 
13
15
  This class is used to define custom iterators that can be used in Sonolus.py.
14
16
 
15
- Inheritors must implement the `has_next`, `get`, and `advance` methods.
16
- The `__next__` and `__iter__` methods are implemented by default.
17
+ Inheritors must implement the `next` method, which should return a `Maybe[T]`.
17
18
 
18
19
  Usage:
19
20
  ```python
20
21
  class MyIterator(Record, SonolusIterator):
21
- def has_next(self) -> bool:
22
- ...
23
-
24
- def get(self) -> Any:
25
- ...
26
-
27
- def advance(self):
22
+ def next(self) -> Maybe[T]:
28
23
  ...
29
24
  ```
30
25
  """
31
26
 
32
27
  _allow_instance_check_ = True
33
28
 
34
- def next(self) -> T:
35
- result = self.get()
36
- self.advance()
37
- return result
38
-
39
- @abstractmethod
40
- def has_next(self) -> bool:
41
- """Return whether the iterator has more elements."""
42
- raise NotImplementedError
43
-
44
- @abstractmethod
45
- def get(self) -> T:
46
- """Return the next element of the iterator.
47
-
48
- May be called multiple times before calling `advance`.
49
-
50
- Must not be called if `has_next` returns `False`.
51
- """
52
- raise NotImplementedError
53
-
54
- @abstractmethod
55
- def advance(self):
56
- """Advance the iterator to the next element.
57
-
58
- Must not be called if `has_next` returns `False`.
59
- """
29
+ @meta_fn
30
+ def next(self) -> Maybe[T]:
60
31
  raise NotImplementedError
61
32
 
62
33
  def __next__(self) -> T:
63
- if not self.has_next():
34
+ result = self.next()
35
+ if result.is_some:
36
+ return result.get_unsafe()
37
+ else:
64
38
  raise StopIteration
65
- return self.next()
66
39
 
67
40
  def __iter__(self) -> SonolusIterator[T]:
68
41
  return self
@@ -73,15 +46,13 @@ class _Enumerator[V: SonolusIterator](Record, SonolusIterator):
73
46
  offset: int
74
47
  iterator: V
75
48
 
76
- def has_next(self) -> bool:
77
- return self.iterator.has_next()
78
-
79
- def get(self) -> tuple[int, Any]:
80
- return self.i + self.offset, self.iterator.get()
81
-
82
- def advance(self):
49
+ def next(self) -> Maybe[tuple[int, Any]]:
50
+ value = self.iterator.next()
51
+ if value.is_nothing:
52
+ return Nothing
53
+ result = (self.i + self.offset, value.get_unsafe())
83
54
  self.i += 1
84
- self.iterator.advance()
55
+ return Some(result)
85
56
 
86
57
 
87
58
  class _Zipper[T](Record, SonolusIterator):
@@ -89,11 +60,6 @@ class _Zipper[T](Record, SonolusIterator):
89
60
  iterators: T
90
61
 
91
62
  @meta_fn
92
- def has_next(self) -> bool:
93
- from sonolus.backend.visitor import compile_and_call
94
-
95
- return compile_and_call(self._has_next, self._get_iterators())
96
-
97
63
  def _get_iterators(self) -> tuple[SonolusIterator, ...]:
98
64
  from sonolus.script.containers import Pair
99
65
 
@@ -105,81 +71,61 @@ class _Zipper[T](Record, SonolusIterator):
105
71
  iterators.append(v)
106
72
  return tuple(iterators)
107
73
 
108
- def _has_next(self, iterators: tuple[SonolusIterator, ...]) -> bool:
109
- for iterator in iterators: # noqa: SIM110
110
- if not iterator.has_next():
111
- return False
112
- return True
113
-
114
74
  @meta_fn
115
- def get(self) -> tuple[Any, ...]:
75
+ def _get_next_values(self) -> tuple[Any, ...]:
116
76
  from sonolus.backend.visitor import compile_and_call
117
77
 
118
- return tuple(compile_and_call(iterator.get) for iterator in self._get_iterators())
78
+ return tuple(compile_and_call(iterator.next) for iterator in self._get_iterators())
119
79
 
120
80
  @meta_fn
121
- def advance(self):
81
+ def _values_to_tuple(self, values: tuple[Any, ...]) -> tuple[Any, ...]:
122
82
  from sonolus.backend.visitor import compile_and_call
123
83
 
124
- for iterator in self._get_iterators():
125
- compile_and_call(iterator.advance)
126
-
84
+ return tuple(compile_and_call(value.get_unsafe) for value in values)
127
85
 
128
- class _EmptyIterator(Record, SonolusIterator):
129
- def has_next(self) -> bool:
130
- return False
86
+ def next(self) -> Maybe[tuple[Any, ...]]:
87
+ values = self._get_next_values()
88
+ for value in values:
89
+ if value.is_nothing:
90
+ return Nothing
91
+ return Some(self._values_to_tuple(values))
131
92
 
132
- def get(self) -> Any:
133
- return None
134
93
 
135
- def advance(self):
136
- pass
94
+ class _EmptyIterator(Record, SonolusIterator):
95
+ def next(self) -> Maybe[Any]:
96
+ return Nothing
137
97
 
138
98
 
139
99
  class _MappingIterator[T, Fn](Record, SonolusIterator):
140
100
  fn: Fn
141
101
  iterator: T
142
102
 
143
- def has_next(self) -> bool:
144
- return self.iterator.has_next()
145
-
146
- def get(self) -> Any:
147
- return self.fn(self.iterator.get())
148
-
149
- def advance(self):
150
- self.iterator.advance()
103
+ def next(self) -> Maybe[Any]:
104
+ return self.iterator.next().map(self.fn)
151
105
 
152
106
 
153
107
  class _FilteringIterator[T, Fn](Record, SonolusIterator):
154
108
  fn: Fn
155
109
  iterator: T
156
110
 
157
- def has_next(self) -> bool:
158
- while self.iterator.has_next():
159
- if self.fn(self.iterator.get()):
160
- return True
161
- self.iterator.advance()
162
- return False
163
-
164
- def get(self) -> Any:
165
- return self.iterator.get()
166
-
167
- def advance(self):
168
- self.iterator.advance()
169
-
170
-
171
- class _ChainingIterator[T](Record, SonolusIterator):
172
- iterator: T
173
-
174
- def has_next(self) -> bool:
175
- return self.iterator.has_next()
176
-
177
- def get(self) -> Any:
178
- return self.iterator.get().get()
179
-
180
- def advance(self):
181
- self.iterator.get().advance()
182
- while not self.iterator.get().has_next():
183
- self.iterator.advance()
184
- if not self.iterator.has_next():
185
- break
111
+ def next(self) -> Maybe[T]:
112
+ while True:
113
+ value = self.iterator.next()
114
+ if value.is_nothing:
115
+ return Nothing
116
+ inside = value.get_unsafe()
117
+ if self.fn(inside):
118
+ return Some(inside)
119
+
120
+
121
+ @meta_fn
122
+ def maybe_next[T](iterator: Iterator[T]) -> Maybe[T]:
123
+ """Get the next item from an iterator as a `Maybe` if it exists or `Nothing` otherwise."""
124
+ from sonolus.backend.visitor import compile_and_call
125
+
126
+ if not isinstance(iterator, SonolusIterator):
127
+ raise TypeError("Iterator must be an instance of SonolusIterator.")
128
+ if ctx():
129
+ return compile_and_call(iterator.next)
130
+ else:
131
+ return iterator.next()
sonolus/script/level.py CHANGED
@@ -145,7 +145,7 @@ class Level:
145
145
  )
146
146
 
147
147
 
148
- type EntityListArg = list[PlayArchetype | EntityListArg]
148
+ type EntityListArg = list[list[PlayArchetype] | PlayArchetype] | PlayArchetype
149
149
 
150
150
 
151
151
  def flatten_entities(entities: EntityListArg) -> Iterator[PlayArchetype]:
@@ -175,7 +175,7 @@ class LevelData:
175
175
  bgm_offset: float
176
176
  entities: list[PlayArchetype]
177
177
 
178
- def __init__(self, bgm_offset: float, entities: list[PlayArchetype]) -> None:
178
+ def __init__(self, bgm_offset: float, entities: EntityListArg) -> None:
179
179
  self.bgm_offset = bgm_offset
180
180
  self.entities = [*flatten_entities(entities)]
181
181
 
@@ -0,0 +1,241 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from sonolus.script.internal.context import ctx
7
+ from sonolus.script.internal.impl import meta_fn, validate_value
8
+ from sonolus.script.internal.transient import TransientValue
9
+ from sonolus.script.internal.value import Value
10
+ from sonolus.script.num import Num
11
+ from sonolus.script.values import copy, zeros
12
+
13
+
14
+ class Maybe[T](TransientValue):
15
+ """A type that either has a value or is empty.
16
+
17
+ Maybe has special behavior when returned from a function: it may be returned from multiple places
18
+ in a function, provided that all but one return statement returns the literal `Nothing`.
19
+
20
+ This type is not intended for other uses, such as being stored in a Record, Array, or Archetype.
21
+
22
+ Usage:
23
+ ```python
24
+ def fn(a, b):
25
+ if a:
26
+ return Some(b)
27
+ else:
28
+ return Nothing
29
+
30
+ result = fn(..., ...)
31
+ if result.is_some:
32
+ value = result.get()
33
+ ...
34
+ ```
35
+ """
36
+
37
+ _present: Num
38
+ _value: T
39
+
40
+ def __init__(self, *, present: bool, value: T):
41
+ self._present = Num._accept_(present)
42
+ self._value = validate_value(value)
43
+
44
+ @property
45
+ @meta_fn
46
+ def is_some(self) -> bool:
47
+ """Check if the value is present."""
48
+ if ctx():
49
+ if self._present._is_py_():
50
+ # Makes this a compile time constant.
51
+ return self._present
52
+ return self._present._get_readonly_()
53
+ else:
54
+ return self._present._as_py_()
55
+
56
+ @property
57
+ def is_nothing(self) -> bool:
58
+ """Check if the value is empty."""
59
+ return not self.is_some
60
+
61
+ def get(self) -> T:
62
+ """Get the value if present, otherwise raise an error."""
63
+ assert self.is_some
64
+ return self.get_unsafe()
65
+
66
+ @meta_fn
67
+ def get_unsafe(self) -> T:
68
+ if ctx():
69
+ return self._value
70
+ else:
71
+ return self._value._as_py_()
72
+
73
+ def map[R](self, fn: Callable[[T], R], /) -> Maybe[R]:
74
+ """Map the contained value to a new value using the provided function.
75
+
76
+ If the value is not present, returns `Nothing`.
77
+
78
+ Args:
79
+ fn: A function that takes the contained value and returns a new value.
80
+
81
+ Returns:
82
+ A `Maybe` instance containing the result of the function if the value is present, otherwise `Nothing`.
83
+ """
84
+ if self.is_some:
85
+ return Some(fn(self.get_unsafe()))
86
+ return Nothing
87
+
88
+ def flat_map[R](self, fn: Callable[[T], Maybe[R]], /) -> Maybe[R]:
89
+ """Flat map the contained value to a new `Maybe` using the provided function.
90
+
91
+ If the value is not present, returns `Nothing`.
92
+
93
+ Args:
94
+ fn: A function that takes the contained value and returns a new `Maybe`.
95
+
96
+ Returns:
97
+ A `Maybe` instance containing the result of the function if the value is present, otherwise `Nothing`.
98
+ """
99
+ if self.is_some:
100
+ return fn(self.get_unsafe())
101
+ return Nothing
102
+
103
+ def or_default(self, default: T) -> T:
104
+ """Return a copy of the contained value if present, otherwise return a copy of the given default value.
105
+
106
+ Args:
107
+ default: The default value to return if the contained value is not present.
108
+
109
+ Returns:
110
+ A copy of the contained value if present, otherwise a copy of the default value.
111
+ """
112
+ result = _box(copy(default))
113
+ if self.is_some:
114
+ result.value = self.get_unsafe()
115
+ return result.value
116
+
117
+ @meta_fn
118
+ def or_else(self, fn: Callable[[], T], /) -> T:
119
+ """Return a copy of the contained value if present, otherwise return a copy of the result of the given function.
120
+
121
+ Args:
122
+ fn: A function that returns a value to use if the contained value is not present.
123
+
124
+ Returns:
125
+ A copy of the contained value if present, otherwise a copy of the result of calling the function.
126
+ """
127
+ from sonolus.backend.visitor import compile_and_call
128
+
129
+ if ctx():
130
+ if self.is_some._is_py_(): # type: ignore
131
+ if self.is_some._as_py_(): # type: ignore
132
+ return copy(self.get_unsafe())
133
+ else:
134
+ return copy(compile_and_call(fn))
135
+ else:
136
+ return compile_and_call(self._or_else, fn)
137
+ elif self.is_some:
138
+ return copy(self.get_unsafe())
139
+ else:
140
+ return copy(fn())
141
+
142
+ def _or_else(self, fn: Callable[[], T], /) -> T:
143
+ result = _box(zeros(self.contained_type))
144
+ if self.is_some:
145
+ result.value = self.get_unsafe()
146
+ else:
147
+ result.value = fn()
148
+ return result.value
149
+
150
+ @property
151
+ def tuple(self) -> tuple[bool, T]:
152
+ """Return whether the value is present and a copy of the contained value if present as a tuple.
153
+
154
+ If the value is not present, the tuple will contain `False` and a zero initialized value of the contained type.
155
+ """
156
+ result_value = _box(zeros(self.contained_type))
157
+ if self.is_some:
158
+ result_value.value = self.get_unsafe()
159
+ return self.is_some, result_value.value
160
+
161
+ @property
162
+ @meta_fn
163
+ def contained_type(self):
164
+ return type(self._value)
165
+
166
+ @classmethod
167
+ def _accepts_(cls, value: Any) -> bool:
168
+ return isinstance(value, cls)
169
+
170
+ @classmethod
171
+ def _accept_(cls, value: Any) -> Maybe[T]:
172
+ if not cls._accepts_(value):
173
+ raise TypeError(f"Cannot accept value of type {type(value).__name__} as {cls.__name__}.")
174
+ return value
175
+
176
+ def _is_py_(self) -> bool:
177
+ return not self._present or not isinstance(self._value, Value) or self._value._is_py_()
178
+
179
+ def _as_py_(self) -> Any:
180
+ if not self._is_py_():
181
+ raise ValueError("Not a python value")
182
+ return self
183
+
184
+ def _copy_from_(self, value: Any):
185
+ raise TypeError("Maybe does not support mutation.")
186
+
187
+ def _copy_(self) -> Maybe[T]:
188
+ raise TypeError("Maybe does not support copying.")
189
+
190
+ def _set_(self, value: Any):
191
+ if not self._accepts_(value):
192
+ raise TypeError(f"Cannot set value of type {type(value).__name__} to {self.__class__.__name__}.")
193
+ if value is not Nothing and self._value is not value._value:
194
+ raise TypeError(f"Cannot set value of type {type(value._value).__name__} to {self.__class__.__name__}.")
195
+ self._present._set_(value._present)
196
+
197
+ @classmethod
198
+ def _get_merge_target_(cls, values: list[Any]) -> Any:
199
+ if not all(isinstance(v, cls) for v in values):
200
+ return NotImplemented
201
+ distinct = []
202
+ seen_ids = set()
203
+ for v in values:
204
+ if v is Nothing:
205
+ continue
206
+ if id(v._value) not in seen_ids:
207
+ distinct.append(v)
208
+ seen_ids.add(id(v._value))
209
+ match distinct:
210
+ case []:
211
+ return Nothing
212
+ case [v]:
213
+ return Maybe(present=Num._alloc_(), value=v._value)
214
+ case _:
215
+ return NotImplemented
216
+
217
+
218
+ def Some[T](value: T) -> Maybe[T]: # noqa: N802
219
+ """Create a `Maybe` instance with a value.
220
+
221
+ Args:
222
+ value: The contained value.
223
+
224
+ Returns:
225
+ A `Maybe` instance that contains the provided value.
226
+ """
227
+ return Maybe(present=True, value=value)
228
+
229
+
230
+ Nothing: Maybe[Any] = Maybe(present=False, value=None) # type: ignore
231
+
232
+ # Note: has to come after the definition to hide the definition in the docs.
233
+ Nothing: Maybe[Any]
234
+ """The empty `Maybe` instance."""
235
+
236
+
237
+ @meta_fn
238
+ def _box(value):
239
+ from sonolus.script.containers import Box
240
+
241
+ return Box(value)
sonolus/script/num.py CHANGED
@@ -1,9 +1,11 @@
1
+ # type: ignore
1
2
  from __future__ import annotations
2
3
 
3
4
  import operator
4
5
  from collections.abc import Callable, Iterable
5
6
  from typing import TYPE_CHECKING, Any, Self, TypeGuard, final, runtime_checkable
6
7
 
8
+ from sonolus.backend.blocks import BlockData
7
9
  from sonolus.backend.ir import IRConst, IRExpr, IRGet, IRPureInstr, IRSet
8
10
  from sonolus.backend.ops import Op
9
11
  from sonolus.backend.place import BlockPlace
@@ -114,15 +116,28 @@ class _Num(Value, metaclass=_NumMeta):
114
116
 
115
117
  def _get_(self) -> Self:
116
118
  if ctx():
119
+ if isinstance(self.data, BlockPlace):
120
+ ctx().check_readable(self.data)
117
121
  place = ctx().alloc(size=1)
122
+ ctx().add_statements(IRSet(place, self.ir()))
123
+ return Num(place)
124
+ else:
125
+ return Num(self.data)
126
+
127
+ def _get_readonly_(self) -> Self:
128
+ if ctx():
118
129
  if isinstance(self.data, BlockPlace):
119
130
  ctx().check_readable(self.data)
131
+ if isinstance(self.data.block, BlockData) and not ctx().is_writable(self.data):
132
+ # This block is immutable in the current callback, so no need to copy it in case it changes.
133
+ return Num(self.data)
134
+ place = ctx().alloc(size=1)
120
135
  ctx().add_statements(IRSet(place, self.ir()))
121
136
  return Num(place)
122
137
  else:
123
138
  return Num(self.data)
124
139
 
125
- def _set_(self, value: Self):
140
+ def _set_(self, value: Any):
126
141
  value = Num._accept_(value)
127
142
  if ctx():
128
143
  match self.data:
@@ -136,7 +151,7 @@ class _Num(Value, metaclass=_NumMeta):
136
151
  else:
137
152
  self.data = value.data
138
153
 
139
- def _copy_from_(self, value: Self):
154
+ def _copy_from_(self, value: Any):
140
155
  raise ValueError("Cannot assign to a number")
141
156
 
142
157
  def _copy_(self) -> Self:
@@ -438,14 +453,14 @@ if TYPE_CHECKING:
438
453
  from typing import Protocol
439
454
 
440
455
  @runtime_checkable
441
- class Num[T](Protocol, int, bool, float):
442
- def __add__(self, other: T, /) -> Num | int | bool | float: ...
443
- def __sub__(self, other: T, /) -> Num | int | bool | float: ...
444
- def __mul__(self, other: T, /) -> Num | int | bool | float: ...
445
- def __truediv__(self, other: T, /) -> Num | int | bool | float: ...
446
- def __floordiv__(self, other: T, /) -> Num | int | bool | float: ...
447
- def __mod__(self, other: T, /) -> Num | int | bool | float: ...
448
- def __pow__(self, other: T, /) -> Num | int | bool | float: ...
456
+ class Num(Protocol, int, bool, float):
457
+ def __add__(self, other: Any, /) -> Num | int | bool | float: ...
458
+ def __sub__(self, other: Any, /) -> Num | int | bool | float: ...
459
+ def __mul__(self, other: Any, /) -> Num | int | bool | float: ...
460
+ def __truediv__(self, other: Any, /) -> Num | int | bool | float: ...
461
+ def __floordiv__(self, other: Any, /) -> Num | int | bool | float: ...
462
+ def __mod__(self, other: Any, /) -> Num | int | bool | float: ...
463
+ def __pow__(self, other: Any, /) -> Num | int | bool | float: ...
449
464
 
450
465
  def __neg__(self, /) -> Num | int | bool | float: ...
451
466
  def __pos__(self, /) -> Num | int | bool | float: ...
@@ -453,10 +468,10 @@ if TYPE_CHECKING:
453
468
 
454
469
  def __eq__(self, other: Any, /) -> bool: ...
455
470
  def __ne__(self, other: Any, /) -> bool: ...
456
- def __lt__(self, other: T, /) -> bool: ...
457
- def __le__(self, other: T, /) -> bool: ...
458
- def __gt__(self, other: T, /) -> bool: ...
459
- def __ge__(self, other: T, /) -> bool: ...
471
+ def __lt__(self, other: Any, /) -> bool: ...
472
+ def __le__(self, other: Any, /) -> bool: ...
473
+ def __gt__(self, other: Any, /) -> bool: ...
474
+ def __ge__(self, other: Any, /) -> bool: ...
460
475
 
461
476
  def __hash__(self, /) -> int: ...
462
477
 
sonolus/script/options.py CHANGED
@@ -175,7 +175,7 @@ def select_option(
175
175
  return _SelectOption(name, description, standard, advanced, scope, default, values)
176
176
 
177
177
 
178
- type Options = NewType("Options", Any)
178
+ type Options = NewType("Options", Any) # type: ignore
179
179
  type _OptionInfo = _SliderOption | _ToggleOption | _SelectOption
180
180
 
181
181
 
@@ -95,7 +95,7 @@ def particle(name: str) -> Any:
95
95
  return _ParticleInfo(name)
96
96
 
97
97
 
98
- type Particles = NewType("Particles", Any)
98
+ type Particles = NewType("Particles", Any) # type: ignore
99
99
 
100
100
 
101
101
  @dataclass_transform()
sonolus/script/project.py CHANGED
@@ -1,10 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Sequence
3
+ from collections.abc import Callable, Iterable, Sequence
4
4
  from dataclasses import dataclass
5
5
  from os import PathLike
6
6
  from pathlib import Path
7
- from typing import ClassVar, Self, TypedDict
7
+ from typing import ClassVar, TypedDict
8
8
 
9
9
  from sonolus.backend.optimize import optimize
10
10
  from sonolus.backend.optimize.passes import CompilerPass
@@ -25,14 +25,23 @@ class Project:
25
25
  def __init__(
26
26
  self,
27
27
  engine: Engine,
28
- levels: list[Level] | None = None,
28
+ levels: Iterable[Level] | Callable[[], Iterable[Level]] | None = None,
29
29
  resources: PathLike | None = None,
30
30
  ):
31
31
  self.engine = engine
32
- self.levels = levels or []
32
+ match levels:
33
+ case Callable():
34
+ self._level_source = lazy_loader(levels)
35
+ case Iterable():
36
+ self._level_source = levels
37
+ case None:
38
+ self._level_source = []
39
+ case _:
40
+ raise TypeError(f"Invalid type for levels: {type(levels)}. Expected Iterable or Callable.")
41
+ self._levels = None
33
42
  self.resources = Path(resources or "resources")
34
43
 
35
- def with_levels(self, levels: list[Level]) -> Self:
44
+ def with_levels(self, levels: Iterable[Level] | Callable[[], Iterable[Level]] | None) -> Project:
36
45
  """Create a new project with the specified levels.
37
46
 
38
47
  Args:
@@ -78,6 +87,16 @@ class Project:
78
87
 
79
88
  return get_project_schema(self)
80
89
 
90
+ @property
91
+ def levels(self) -> list[Level]:
92
+ if self._levels is None:
93
+ self._levels = list(self._level_source)
94
+ return self._levels
95
+
96
+
97
+ def lazy_loader(fn):
98
+ yield from fn()
99
+
81
100
 
82
101
  class ProjectSchema(TypedDict):
83
102
  archetypes: list[ArchetypeSchema]