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

Potentially problematic release.


This version of sonolus.py might be problematic. Click here for more details.

Files changed (63) 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 +24 -0
  18. sonolus/backend/visitor.py +260 -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 +38 -107
  45. sonolus/script/maybe.py +139 -0
  46. sonolus/script/num.py +17 -2
  47. sonolus/script/options.py +1 -1
  48. sonolus/script/particle.py +1 -1
  49. sonolus/script/project.py +24 -5
  50. sonolus/script/quad.py +15 -15
  51. sonolus/script/record.py +16 -12
  52. sonolus/script/runtime.py +22 -18
  53. sonolus/script/sprite.py +1 -1
  54. sonolus/script/stream.py +66 -82
  55. sonolus/script/transform.py +35 -34
  56. sonolus/script/values.py +10 -10
  57. sonolus/script/vec.py +21 -18
  58. {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.0.dist-info}/METADATA +1 -1
  59. sonolus_py-0.4.0.dist-info/RECORD +93 -0
  60. sonolus_py-0.3.4.dist-info/RECORD +0 -92
  61. {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.0.dist-info}/WHEEL +0 -0
  62. {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.0.dist-info}/entry_points.txt +0 -0
  63. {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,6 @@
1
1
  from abc import abstractmethod
2
2
  from collections.abc import Callable, Iterable
3
+ from types import NotImplementedType
3
4
  from typing import Any, Self
4
5
 
5
6
  from sonolus.backend.ir import IRConst, IRExpr, IRStmt
@@ -136,8 +137,19 @@ class Value:
136
137
  """
137
138
  raise NotImplementedError
138
139
 
140
+ def _get_readonly_(self) -> Self:
141
+ """Implements access to the value without copying if the underlying value is immutable.
142
+
143
+ The returned value should not be intentionally modified, but it is not guaranteed to be immutable.
144
+
145
+ For example, a Num might be backed internally by rom, which is immutable. If we aren't going to modify
146
+ (e.g. by putting it into a record where it can be modified), we can just return the original value, which
147
+ avoids unnecessary copying.
148
+ """
149
+ return self._get_()
150
+
139
151
  @abstractmethod
140
- def _set_(self, value: Self):
152
+ def _set_(self, value: Any):
141
153
  """Implements assignment (=).
142
154
 
143
155
  This is only supported by value types.
@@ -149,7 +161,7 @@ class Value:
149
161
  raise NotImplementedError
150
162
 
151
163
  @abstractmethod
152
- def _copy_from_(self, value: Self):
164
+ def _copy_from_(self, value: Any):
153
165
  """Implements copy assignment (@=).
154
166
 
155
167
  This is only supported by mutable reference types.
@@ -173,9 +185,35 @@ class Value:
173
185
  """Returns a zero-initialized value of this type."""
174
186
  raise NotImplementedError
175
187
 
188
+ @classmethod
189
+ def _get_merge_target_(cls, values: list[Any]) -> Any | NotImplementedType:
190
+ """Return the target when merging values from multiple code paths.
191
+
192
+ E.g. for code like this:
193
+ ```
194
+ if cond:
195
+ x = 1
196
+ else:
197
+ x = 2
198
+ do_something(x)
199
+ ```
200
+ This is called to create a target value for x after the if-else block,
201
+ and at the end of each block, that target value is assigned to the respective value (1 or 2).
202
+ This lets us keep the value of x as a constant within if and else branches, and only have it
203
+ become a runtime value after the if-else block.
204
+
205
+ This is an overrideable method to allow for some other special behavior, namely the Maybe type.
206
+ """
207
+ if cls._is_value_type_():
208
+ from sonolus.script.internal.context import ctx
209
+
210
+ return cls._from_place_(ctx().alloc(size=cls._size_()))
211
+ else:
212
+ return NotImplemented
213
+
176
214
  def __imatmul__(self, other):
177
215
  self._copy_from_(other)
178
216
  return self
179
217
 
180
218
 
181
- Value.__imatmul__._meta_fn_ = True
219
+ Value.__imatmul__._meta_fn_ = True # type: ignore
@@ -1,4 +1,4 @@
1
- from typing import Self
1
+ from __future__ import annotations
2
2
 
3
3
  from sonolus.backend.ops import Op
4
4
  from sonolus.script.array_like import ArrayLike
@@ -22,7 +22,7 @@ class Interval(Record):
22
22
  end: float
23
23
 
24
24
  @classmethod
25
- def zero(cls) -> Self:
25
+ def zero(cls) -> Interval:
26
26
  """Get an empty interval."""
27
27
  return cls(0, 0)
28
28
 
@@ -49,7 +49,7 @@ class Interval(Record):
49
49
  """The interval as a tuple."""
50
50
  return self.start, self.end
51
51
 
52
- def __contains__(self, item: Self | float | int) -> bool:
52
+ def __contains__(self, item: Interval | float | int) -> bool:
53
53
  """Check if an item is within the interval.
54
54
 
55
55
  Args:
@@ -66,7 +66,7 @@ class Interval(Record):
66
66
  case _:
67
67
  static_error("Invalid type for interval check")
68
68
 
69
- def __add__(self, other: float | int) -> Self:
69
+ def __add__(self, other: float | int) -> Interval:
70
70
  """Add a value to both ends of the interval.
71
71
 
72
72
  Args:
@@ -77,7 +77,7 @@ class Interval(Record):
77
77
  """
78
78
  return Interval(self.start + other, self.end + other)
79
79
 
80
- def __sub__(self, other: float | int) -> Self:
80
+ def __sub__(self, other: float | int) -> Interval:
81
81
  """Subtract a value from both ends of the interval.
82
82
 
83
83
  Args:
@@ -88,7 +88,7 @@ class Interval(Record):
88
88
  """
89
89
  return Interval(self.start - other, self.end - other)
90
90
 
91
- def __mul__(self, other: float | int) -> Self:
91
+ def __mul__(self, other: float | int) -> Interval:
92
92
  """Multiply both ends of the interval by a value.
93
93
 
94
94
  Args:
@@ -99,7 +99,7 @@ class Interval(Record):
99
99
  """
100
100
  return Interval(self.start * other, self.end * other)
101
101
 
102
- def __truediv__(self, other: float | int) -> Self:
102
+ def __truediv__(self, other: float | int) -> Interval:
103
103
  """Divide both ends of the interval by a value.
104
104
 
105
105
  Args:
@@ -110,7 +110,7 @@ class Interval(Record):
110
110
  """
111
111
  return Interval(self.start / other, self.end / other)
112
112
 
113
- def __floordiv__(self, other: float | int) -> Self:
113
+ def __floordiv__(self, other: float | int) -> Interval:
114
114
  """Divide both ends of the interval by a value and floor the result.
115
115
 
116
116
  Args:
@@ -121,7 +121,7 @@ class Interval(Record):
121
121
  """
122
122
  return Interval(self.start // other, self.end // other)
123
123
 
124
- def __and__(self, other: Self) -> Self:
124
+ def __and__(self, other: Interval) -> Interval:
125
125
  """Get the intersection of two intervals.
126
126
 
127
127
  The resulting interval will be empty and may have a negative length if the two intervals do not overlap.
@@ -134,7 +134,7 @@ class Interval(Record):
134
134
  """
135
135
  return Interval(max(self.start, other.start), min(self.end, other.end))
136
136
 
137
- def shrink(self, value: float | int) -> Self:
137
+ def shrink(self, value: float | int) -> Interval:
138
138
  """Shrink the interval by a value on both ends.
139
139
 
140
140
  Args:
@@ -145,7 +145,7 @@ class Interval(Record):
145
145
  """
146
146
  return Interval(self.start + value, self.end - value)
147
147
 
148
- def expand(self, value: float | int) -> Self:
148
+ def expand(self, value: float | int) -> Interval:
149
149
  """Expand the interval by a value on both ends.
150
150
 
151
151
  Args:
@@ -223,11 +223,11 @@ def _num_lerp_clamped(a, b, x, /):
223
223
 
224
224
 
225
225
  def _generic_lerp[T](a: T, b: T, x: float, /) -> T:
226
- return a + (b - a) * x
226
+ return a + (b - a) * x # type: ignore
227
227
 
228
228
 
229
229
  def _generic_lerp_clamped[T](a: T, b: T, x: float, /) -> T:
230
- return a + (b - a) * max(0, min(1, x))
230
+ return a + (b - a) * max(0, min(1, x)) # type: ignore
231
231
 
232
232
 
233
233
  def lerp[T](a: T, b: T, x: float, /) -> T:
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from abc import abstractmethod
4
3
  from typing import Any
5
4
 
6
5
  from sonolus.script.internal.impl import meta_fn
6
+ from sonolus.script.maybe import Maybe, Nothing, Some
7
7
  from sonolus.script.record import Record
8
8
 
9
9
 
@@ -12,57 +12,28 @@ class SonolusIterator[T]:
12
12
 
13
13
  This class is used to define custom iterators that can be used in Sonolus.py.
14
14
 
15
- Inheritors must implement the `has_next`, `get`, and `advance` methods.
16
- The `__next__` and `__iter__` methods are implemented by default.
15
+ Inheritors must implement the `next` method, which should return a `Maybe[T]`.
17
16
 
18
17
  Usage:
19
18
  ```python
20
19
  class MyIterator(Record, SonolusIterator):
21
- def has_next(self) -> bool:
22
- ...
23
-
24
- def get(self) -> Any:
25
- ...
26
-
27
- def advance(self):
20
+ def next(self) -> Maybe[T]:
28
21
  ...
29
22
  ```
30
23
  """
31
24
 
32
25
  _allow_instance_check_ = True
33
26
 
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
- """
27
+ @meta_fn
28
+ def next(self) -> Maybe[T]:
60
29
  raise NotImplementedError
61
30
 
62
31
  def __next__(self) -> T:
63
- if not self.has_next():
32
+ result = self.next()
33
+ if result.is_some:
34
+ return result.get_unsafe()
35
+ else:
64
36
  raise StopIteration
65
- return self.next()
66
37
 
67
38
  def __iter__(self) -> SonolusIterator[T]:
68
39
  return self
@@ -73,15 +44,13 @@ class _Enumerator[V: SonolusIterator](Record, SonolusIterator):
73
44
  offset: int
74
45
  iterator: V
75
46
 
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):
47
+ def next(self) -> Maybe[tuple[int, Any]]:
48
+ value = self.iterator.next()
49
+ if value.is_nothing:
50
+ return Nothing
51
+ result = (self.i + self.offset, value.get_unsafe())
83
52
  self.i += 1
84
- self.iterator.advance()
53
+ return Some(result)
85
54
 
86
55
 
87
56
  class _Zipper[T](Record, SonolusIterator):
@@ -89,11 +58,6 @@ class _Zipper[T](Record, SonolusIterator):
89
58
  iterators: T
90
59
 
91
60
  @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
61
  def _get_iterators(self) -> tuple[SonolusIterator, ...]:
98
62
  from sonolus.script.containers import Pair
99
63
 
@@ -105,81 +69,48 @@ class _Zipper[T](Record, SonolusIterator):
105
69
  iterators.append(v)
106
70
  return tuple(iterators)
107
71
 
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
72
  @meta_fn
115
- def get(self) -> tuple[Any, ...]:
73
+ def _get_next_values(self) -> tuple[Any, ...]:
116
74
  from sonolus.backend.visitor import compile_and_call
117
75
 
118
- return tuple(compile_and_call(iterator.get) for iterator in self._get_iterators())
76
+ return tuple(compile_and_call(iterator.next) for iterator in self._get_iterators())
119
77
 
120
78
  @meta_fn
121
- def advance(self):
79
+ def _values_to_tuple(self, values: tuple[Any, ...]) -> tuple[Any, ...]:
122
80
  from sonolus.backend.visitor import compile_and_call
123
81
 
124
- for iterator in self._get_iterators():
125
- compile_and_call(iterator.advance)
126
-
82
+ return tuple(compile_and_call(value.get_unsafe) for value in values)
127
83
 
128
- class _EmptyIterator(Record, SonolusIterator):
129
- def has_next(self) -> bool:
130
- return False
84
+ def next(self) -> Maybe[tuple[Any, ...]]:
85
+ values = self._get_next_values()
86
+ for value in values:
87
+ if value.is_nothing:
88
+ return Nothing
89
+ return Some(self._values_to_tuple(values))
131
90
 
132
- def get(self) -> Any:
133
- return None
134
91
 
135
- def advance(self):
136
- pass
92
+ class _EmptyIterator(Record, SonolusIterator):
93
+ def next(self) -> Maybe[Any]:
94
+ return Nothing
137
95
 
138
96
 
139
97
  class _MappingIterator[T, Fn](Record, SonolusIterator):
140
98
  fn: Fn
141
99
  iterator: T
142
100
 
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()
101
+ def next(self) -> Maybe[Any]:
102
+ return self.iterator.next().map(self.fn)
151
103
 
152
104
 
153
105
  class _FilteringIterator[T, Fn](Record, SonolusIterator):
154
106
  fn: Fn
155
107
  iterator: T
156
108
 
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
109
+ def next(self) -> Maybe[T]:
110
+ while True:
111
+ value = self.iterator.next()
112
+ if value.is_nothing:
113
+ return Nothing
114
+ inside = value.get_unsafe()
115
+ if self.fn(inside):
116
+ return Some(inside)
@@ -0,0 +1,139 @@
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
8
+ from sonolus.script.internal.transient import TransientValue
9
+ from sonolus.script.internal.value import Value
10
+ from sonolus.script.num import Num
11
+
12
+
13
+ class Maybe[T](TransientValue):
14
+ """A type that either has a value or is empty.
15
+
16
+ Maybe has special behavior when returned from a function: it may be returned from multiple places
17
+ in a function, provided that all but one return statement returns the literal `Nothing`.
18
+
19
+ This type is not intended for other uses, such as being stored in a Record, Array, or Archetype.
20
+
21
+ Usage:
22
+ ```python
23
+ def fn(a, b):
24
+ if a:
25
+ return Some(b)
26
+ else:
27
+ return Nothing
28
+
29
+ result = fn(..., ...)
30
+ if result.is_some:
31
+ value = result.get()
32
+ ...
33
+ ```
34
+ """
35
+
36
+ _present: Num
37
+ _value: T
38
+
39
+ def __init__(self, *, present: bool, value: T):
40
+ self._present = Num._accept_(present)
41
+ self._value = value
42
+
43
+ @property
44
+ @meta_fn
45
+ def is_some(self) -> bool:
46
+ """Check if the value is present."""
47
+ if ctx():
48
+ return self._present._get_readonly_()
49
+ else:
50
+ return self._present._as_py_()
51
+
52
+ @property
53
+ def is_nothing(self) -> bool:
54
+ """Check if the value is empty."""
55
+ return not self.is_some
56
+
57
+ def get(self) -> T:
58
+ """Get the value if present, otherwise raise an error."""
59
+ assert self.is_some
60
+ return self.get_unsafe()
61
+
62
+ @meta_fn
63
+ def get_unsafe(self) -> T:
64
+ return self._value
65
+
66
+ def map[R](self, fn: Callable[[T], R], /) -> Maybe[R]:
67
+ if self.is_some:
68
+ return Some(fn(self.get_unsafe()))
69
+ return Nothing
70
+
71
+ @classmethod
72
+ def _accepts_(cls, value: Any) -> bool:
73
+ return isinstance(value, cls)
74
+
75
+ @classmethod
76
+ def _accept_(cls, value: Any) -> Maybe[T]:
77
+ if not cls._accepts_(value):
78
+ raise TypeError(f"Cannot accept value of type {type(value).__name__} as {cls.__name__}.")
79
+ return value
80
+
81
+ def _is_py_(self) -> bool:
82
+ return not self._present or not isinstance(self._value, Value) or self._value._is_py_()
83
+
84
+ def _as_py_(self) -> Any:
85
+ if not self._is_py_():
86
+ raise ValueError("Not a python value")
87
+ return self
88
+
89
+ def _copy_from_(self, value: Any):
90
+ raise TypeError("Maybe does not support mutation.")
91
+
92
+ def _copy_(self) -> Maybe[T]:
93
+ raise TypeError("Maybe does not support copying.")
94
+
95
+ def _set_(self, value: Any):
96
+ if not self._accepts_(value):
97
+ raise TypeError(f"Cannot set value of type {type(value).__name__} to {self.__class__.__name__}.")
98
+ if value is not Nothing and self._value is not value._value:
99
+ raise TypeError(f"Cannot set value of type {type(value._value).__name__} to {self.__class__.__name__}.")
100
+ self._present._set_(value._present)
101
+
102
+ @classmethod
103
+ def _get_merge_target_(cls, values: list[Any]) -> Any:
104
+ if not all(isinstance(v, cls) for v in values):
105
+ return NotImplemented
106
+ distinct = []
107
+ seen_ids = set()
108
+ for v in values:
109
+ if v is Nothing:
110
+ continue
111
+ if id(v._value) not in seen_ids:
112
+ distinct.append(v)
113
+ seen_ids.add(id(v._value))
114
+ match distinct:
115
+ case []:
116
+ return Nothing
117
+ case [v]:
118
+ return Maybe(present=Num._alloc_(), value=v._value)
119
+ case _:
120
+ return NotImplemented
121
+
122
+
123
+ def Some[T](value: T) -> Maybe[T]: # noqa: N802
124
+ """Create a `Maybe` instance with a value.
125
+
126
+ Args:
127
+ value: The contained value.
128
+
129
+ Returns:
130
+ A `Maybe` instance that contains the provided value.
131
+ """
132
+ return Maybe(present=True, value=value)
133
+
134
+
135
+ Nothing: Maybe[Any] = Maybe(present=False, value=None) # type: ignore
136
+
137
+ # Note: has to come after the definition to hide the definition in the docs.
138
+ Nothing: Maybe[Any]
139
+ """The empty `Maybe` instance."""
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:
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: list[Level]) -> 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]