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
sonolus/script/engine.py CHANGED
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  from collections.abc import Callable
4
- from typing import Any
5
+ from os import PathLike
6
+ from pathlib import Path
7
+ from typing import Any, Literal
5
8
 
6
- from sonolus.build.collection import Asset
9
+ from sonolus.build.collection import Asset, load_asset
7
10
  from sonolus.script.archetype import PlayArchetype, PreviewArchetype, WatchArchetype, _BaseArchetype
8
11
  from sonolus.script.bucket import Buckets, EmptyBuckets
9
12
  from sonolus.script.effect import Effects, EmptyEffects
@@ -13,12 +16,52 @@ from sonolus.script.instruction import (
13
16
  TutorialInstructionIcons,
14
17
  TutorialInstructions,
15
18
  )
19
+ from sonolus.script.metadata import AnyText, Tag, as_localization_text
16
20
  from sonolus.script.options import EmptyOptions, Options
17
21
  from sonolus.script.particle import EmptyParticles, Particles
18
22
  from sonolus.script.sprite import EmptySkin, Skin
19
23
  from sonolus.script.ui import UiConfig
20
24
 
21
25
 
26
+ class ExportedEngine:
27
+ """An exported Sonolus.py engine."""
28
+
29
+ def __init__(
30
+ self,
31
+ *,
32
+ item: dict,
33
+ thumbnail: bytes,
34
+ play_data: bytes,
35
+ watch_data: bytes,
36
+ preview_data: bytes,
37
+ tutorial_data: bytes,
38
+ rom: bytes | None = None,
39
+ configuration: bytes,
40
+ ) -> None:
41
+ self.item = item
42
+ self.thumbnail = thumbnail
43
+ self.play_data = play_data
44
+ self.watch_data = watch_data
45
+ self.preview_data = preview_data
46
+ self.tutorial_data = tutorial_data
47
+ self.rom = rom
48
+ self.configuration = configuration
49
+
50
+ def write_to_dir(self, path: PathLike):
51
+ """Write the exported engine to a directory."""
52
+ path = Path(path)
53
+ path.mkdir(parents=True, exist_ok=True)
54
+ (path / "item.json").write_text(json.dumps(self.item, ensure_ascii=False), encoding="utf-8")
55
+ (path / "thumbnail").write_bytes(self.thumbnail)
56
+ (path / "playData").write_bytes(self.play_data)
57
+ (path / "watchData").write_bytes(self.watch_data)
58
+ (path / "previewData").write_bytes(self.preview_data)
59
+ (path / "tutorialData").write_bytes(self.tutorial_data)
60
+ if self.rom is not None:
61
+ (path / "rom").write_bytes(self.rom)
62
+ (path / "configuration").write_bytes(self.configuration)
63
+
64
+
22
65
  class Engine:
23
66
  """A Sonolus.py engine.
24
67
 
@@ -33,34 +76,79 @@ class Engine:
33
76
  particle: The default particle for the engine.
34
77
  thumbnail: The thumbnail for the engine.
35
78
  data: The engine's modes and configurations.
79
+ tags: The tags of the engine.
80
+ description: The description of the engine.
81
+ meta: Additional metadata of the engine.
36
82
  """
37
83
 
38
- version = 12
84
+ version: Literal[13] = 13
39
85
 
40
86
  def __init__(
41
87
  self,
42
88
  *,
43
89
  name: str,
44
- title: str | None = None,
45
- subtitle: str = "Sonolus.py Engine",
46
- author: str = "Unknown",
90
+ title: AnyText | None = None,
91
+ subtitle: AnyText = "Sonolus.py Engine",
92
+ author: AnyText = "Unknown",
47
93
  skin: str | None = None,
48
94
  background: str | None = None,
49
95
  effect: str | None = None,
50
96
  particle: str | None = None,
51
97
  thumbnail: Asset | None = None,
52
98
  data: EngineData,
99
+ tags: list[Tag] | None = None,
100
+ description: AnyText | None = None,
101
+ meta: Any = None,
53
102
  ) -> None:
54
103
  self.name = name
55
- self.title = title or name
56
- self.subtitle = subtitle
57
- self.author = author
104
+ self.title = as_localization_text(title or name)
105
+ self.subtitle = as_localization_text(subtitle)
106
+ self.author = as_localization_text(author)
58
107
  self.skin = skin
59
108
  self.background = background
60
109
  self.effect = effect
61
110
  self.particle = particle
62
111
  self.thumbnail = thumbnail
63
112
  self.data = data
113
+ self.tags = tags or []
114
+ self.description = as_localization_text(description) if description is not None else None
115
+ self.meta = meta
116
+
117
+ def export(self) -> ExportedEngine:
118
+ """Export the engine in a sonolus-pack compatible format.
119
+
120
+ Returns:
121
+ An exported engine.
122
+ """
123
+ from sonolus.build.engine import package_engine
124
+ from sonolus.build.project import BLANK_PNG
125
+
126
+ item = {
127
+ "version": self.version,
128
+ "title": self.title,
129
+ "subtitle": self.subtitle,
130
+ "author": self.author,
131
+ "tags": [tag.as_dict() for tag in self.tags],
132
+ "skin": self.skin,
133
+ "background": self.background,
134
+ "effect": self.effect,
135
+ "particle": self.particle,
136
+ }
137
+ packaged = package_engine(self.data)
138
+ if self.description is not None:
139
+ item["description"] = self.description
140
+ if self.meta is not None:
141
+ item["meta"] = self.meta
142
+ return ExportedEngine(
143
+ item=item,
144
+ thumbnail=load_asset(self.thumbnail) if self.thumbnail is not None else BLANK_PNG,
145
+ play_data=packaged.play_data,
146
+ watch_data=packaged.watch_data,
147
+ preview_data=packaged.preview_data,
148
+ tutorial_data=packaged.tutorial_data,
149
+ rom=packaged.rom,
150
+ configuration=packaged.configuration,
151
+ )
64
152
 
65
153
 
66
154
  def default_callback() -> Any:
@@ -190,6 +278,30 @@ class TutorialMode:
190
278
  self.update = update
191
279
 
192
280
 
281
+ def empty_play_mode() -> PlayMode:
282
+ """Create an empty play mode."""
283
+ return PlayMode()
284
+
285
+
286
+ def empty_watch_mode() -> WatchMode:
287
+ """Create an empty watch mode."""
288
+ return WatchMode(update_spawn=default_callback)
289
+
290
+
291
+ def empty_preview_mode() -> PreviewMode:
292
+ """Create an empty preview mode."""
293
+ return PreviewMode()
294
+
295
+
296
+ def empty_tutorial_mode() -> TutorialMode:
297
+ """Create an empty tutorial mode."""
298
+ return TutorialMode(
299
+ preprocess=default_callback,
300
+ navigate=default_callback,
301
+ update=default_callback,
302
+ )
303
+
304
+
193
305
  class EngineData:
194
306
  """A Sonolus.py engine's modes and configurations.
195
307
 
@@ -214,11 +326,7 @@ class EngineData:
214
326
  ) -> None:
215
327
  self.ui = ui or UiConfig()
216
328
  self.options = options
217
- self.play = play or PlayMode()
218
- self.watch = watch or WatchMode(update_spawn=default_callback)
219
- self.preview = preview or PreviewMode()
220
- self.tutorial = tutorial or TutorialMode(
221
- preprocess=default_callback,
222
- navigate=default_callback,
223
- update=default_callback,
224
- )
329
+ self.play = play or empty_play_mode()
330
+ self.watch = watch or empty_watch_mode()
331
+ self.preview = preview or empty_preview_mode()
332
+ self.tutorial = tutorial or empty_tutorial_mode()
@@ -52,9 +52,11 @@ def _enumerate(iterable, start=0):
52
52
  from sonolus.backend.visitor import compile_and_call
53
53
 
54
54
  iterable = validate_value(iterable)
55
- if not hasattr(iterable, "__iter__"):
55
+ if isinstance(iterable, TupleImpl):
56
+ return TupleImpl._accept_(tuple((start + i, value) for i, value in enumerate(iterable._as_py_(), start=start)))
57
+ elif not hasattr(iterable, "__iter__"):
56
58
  raise TypeError(f"'{type(iterable).__name__}' object is not iterable")
57
- if isinstance(iterable, ArrayLike):
59
+ elif isinstance(iterable, ArrayLike):
58
60
  return compile_and_call(iterable._enumerate_, start)
59
61
  else:
60
62
  iterator = compile_and_call(iterable.__iter__)
@@ -232,10 +234,27 @@ _int._type_mapping_ = Num
232
234
  _float._type_mapping_ = Num
233
235
  _bool._type_mapping_ = Num
234
236
 
237
+
238
+ def _any(iterable):
239
+ for value in iterable: # noqa: SIM110
240
+ if value:
241
+ return True
242
+ return False
243
+
244
+
245
+ def _all(iterable):
246
+ for value in iterable: # noqa: SIM110
247
+ if not value:
248
+ return False
249
+ return True
250
+
251
+
235
252
  # classmethod, property, staticmethod are supported as decorators, but not within functions
236
253
 
237
254
  BUILTIN_IMPLS = {
238
255
  id(abs): _abs,
256
+ id(all): _all,
257
+ id(any): _any,
239
258
  id(bool): _bool,
240
259
  id(callable): _callable,
241
260
  id(enumerate): _enumerate,
@@ -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:
@@ -94,10 +94,10 @@ class ConstantValue(Value):
94
94
  return self.value()
95
95
 
96
96
  @classmethod
97
- def _from_list_(cls, values: Iterable[float | BlockPlace]) -> Self:
97
+ def _from_list_(cls, values: Iterable[DataValue]) -> Self:
98
98
  return cls()
99
99
 
100
- def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[float | str | BlockPlace]:
100
+ def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
101
101
  return []
102
102
 
103
103
  @classmethod
@@ -107,7 +107,7 @@ class ConstantValue(Value):
107
107
  def _get_(self) -> Self:
108
108
  return self
109
109
 
110
- def _set_(self, value: Any) -> Self:
110
+ def _set_(self, value: Any):
111
111
  if value is not self:
112
112
  raise ValueError(f"{type(self).__name__} is immutable")
113
113
 
@@ -122,6 +122,10 @@ class ConstantValue(Value):
122
122
  def _alloc_(cls) -> Self:
123
123
  return cls()
124
124
 
125
+ @classmethod
126
+ def _zero_(cls) -> Self:
127
+ return cls()
128
+
125
129
  @meta_fn
126
130
  def __eq__(self, other):
127
131
  return self is other
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from contextlib import contextmanager
4
4
  from contextvars import ContextVar
5
5
  from dataclasses import dataclass
6
+ from threading import Lock
6
7
  from typing import Any, Self
7
8
 
8
9
  from sonolus.backend.blocks import BlockData, PlayBlock
@@ -25,6 +26,7 @@ class GlobalContextState:
25
26
  environment_mappings: dict[_GlobalInfo, int]
26
27
  environment_offsets: dict[Block, int]
27
28
  mode: Mode
29
+ lock: Lock
28
30
 
29
31
  def __init__(self, mode: Mode, archetypes: dict[type, int] | None = None, rom: ReadOnlyMemory | None = None):
30
32
  self.archetypes = archetypes or {}
@@ -33,6 +35,7 @@ class GlobalContextState:
33
35
  self.environment_mappings = {}
34
36
  self.environment_offsets = {}
35
37
  self.mode = mode
38
+ self.lock = Lock()
36
39
 
37
40
 
38
41
  class CallbackContextState:
@@ -88,10 +91,6 @@ class Context:
88
91
  def used_names(self) -> dict[str, int]:
89
92
  return self.callback_state.used_names
90
93
 
91
- @property
92
- def const_mappings(self) -> dict[Any, int]:
93
- return self.global_state.const_mappings
94
-
95
94
  def check_readable(self, place: BlockPlace):
96
95
  if not self.callback:
97
96
  return
@@ -204,22 +203,25 @@ class Context:
204
203
  )
205
204
 
206
205
  def map_constant(self, value: Any) -> int:
207
- if value not in self.const_mappings:
208
- self.const_mappings[value] = len(self.const_mappings)
209
- return self.const_mappings[value]
206
+ with self.global_state.lock:
207
+ const_mappings = self.global_state.const_mappings
208
+ if value not in const_mappings:
209
+ const_mappings[value] = len(const_mappings)
210
+ return const_mappings[value]
210
211
 
211
212
  def get_global_base(self, value: _GlobalInfo | _GlobalPlaceholder) -> BlockPlace:
212
- block = value.blocks.get(self.global_state.mode)
213
- if block is None:
214
- raise RuntimeError(f"Global {value.name} is not available in '{self.global_state.mode.name}' mode")
215
- if value not in self.global_state.environment_mappings:
216
- if value.offset is None:
217
- offset = self.global_state.environment_offsets.get(block, 0)
218
- self.global_state.environment_mappings[value] = offset
219
- self.global_state.environment_offsets[block] = offset + value.size
220
- else:
221
- self.global_state.environment_mappings[value] = value.offset
222
- return BlockPlace(block, self.global_state.environment_mappings[value])
213
+ with self.global_state.lock:
214
+ block = value.blocks.get(self.global_state.mode)
215
+ if block is None:
216
+ raise RuntimeError(f"Global {value.name} is not available in '{self.global_state.mode.name}' mode")
217
+ if value not in self.global_state.environment_mappings:
218
+ if value.offset is None:
219
+ offset = self.global_state.environment_offsets.get(block, 0)
220
+ self.global_state.environment_mappings[value] = offset
221
+ self.global_state.environment_offsets[block] = offset + value.size
222
+ else:
223
+ self.global_state.environment_mappings[value] = value.offset
224
+ return BlockPlace(block, self.global_state.environment_mappings[value])
223
225
 
224
226
  @classmethod
225
227
  def meet(cls, contexts: list[Context]) -> Context:
@@ -257,19 +259,22 @@ def using_ctx(value: Context | None):
257
259
  class ReadOnlyMemory:
258
260
  values: list[float]
259
261
  indexes: dict[tuple[float, ...], int]
262
+ _lock: Lock
260
263
 
261
264
  def __init__(self):
262
265
  self.values = []
263
266
  self.indexes = {}
267
+ self._lock = Lock()
264
268
 
265
269
  def __getitem__(self, item: tuple[float, ...]) -> BlockPlace:
266
- if item not in self.indexes:
267
- index = len(self.values)
268
- self.indexes[item] = index
269
- self.values.extend(item)
270
- else:
271
- index = self.indexes[item]
272
- return BlockPlace(self.block, index)
270
+ with self._lock:
271
+ if item not in self.indexes:
272
+ index = len(self.values)
273
+ self.indexes[item] = index
274
+ self.values.extend(item)
275
+ else:
276
+ index = self.indexes[item]
277
+ return BlockPlace(self.block, index)
273
278
 
274
279
  @property
275
280
  def block(self) -> Block:
@@ -93,7 +93,8 @@ def _ln(x: float) -> float:
93
93
  def _log(x: float, base: float | None = None) -> float:
94
94
  if base is None:
95
95
  return _ln(x)
96
- return _ln(x) / _ln(base)
96
+ else:
97
+ return _ln(x) / _ln(base)
97
98
 
98
99
 
99
100
  @native_function(Op.Rem)
@@ -2,7 +2,7 @@ from collections.abc import Iterable
2
2
  from typing import Any, Self
3
3
 
4
4
  from sonolus.backend.place import BlockPlace
5
- from sonolus.script.internal.value import Value
5
+ from sonolus.script.internal.value import DataValue, Value
6
6
 
7
7
 
8
8
  class TransientValue(Value):
@@ -23,10 +23,10 @@ class TransientValue(Value):
23
23
  raise TypeError(f"{cls.__name__} cannot be dereferenced")
24
24
 
25
25
  @classmethod
26
- def _from_list_(cls, values: Iterable[float | BlockPlace]) -> Self:
26
+ def _from_list_(cls, values: Iterable[DataValue]) -> Self:
27
27
  raise TypeError(f"{cls.__name__} cannot be constructed from list")
28
28
 
29
- def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[float | str | BlockPlace]:
29
+ def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
30
30
  raise TypeError(f"{type(self).__name__} cannot be deconstructed to list")
31
31
 
32
32
  @classmethod
@@ -49,3 +49,7 @@ class TransientValue(Value):
49
49
  @classmethod
50
50
  def _alloc_(cls) -> Self:
51
51
  raise TypeError(f"{cls.__name__} is not allocatable")
52
+
53
+ @classmethod
54
+ def _zero_(cls) -> Self:
55
+ raise TypeError(f"{cls.__name__} does not have a zero value")
@@ -1,10 +1,23 @@
1
1
  from abc import abstractmethod
2
- from collections.abc import Iterable
2
+ from collections.abc import Callable, Iterable
3
3
  from typing import Any, Self
4
4
 
5
+ from sonolus.backend.ir import IRConst, IRExpr, IRStmt
5
6
  from sonolus.backend.place import BlockPlace
6
7
 
7
8
 
9
+ class BackingValue:
10
+ def read(self) -> IRExpr:
11
+ raise NotImplementedError()
12
+
13
+ def write(self, value: IRExpr) -> IRStmt:
14
+ raise NotImplementedError()
15
+
16
+
17
+ type DataValue = BlockPlace | BackingValue | float | int | bool
18
+ type BackingSource = Callable[[IRExpr], BackingValue]
19
+
20
+
8
21
  class Value:
9
22
  """Base class for values."""
10
23
 
@@ -59,15 +72,20 @@ class Value:
59
72
  """
60
73
  raise NotImplementedError
61
74
 
75
+ @classmethod
76
+ def _from_backing_source_(cls, source: BackingSource) -> Self:
77
+ """Creates a value from a backing source."""
78
+ return cls._from_list_(source(IRConst(i)) for i in range(cls._size_()))
79
+
62
80
  @classmethod
63
81
  @abstractmethod
64
- def _from_list_(cls, values: Iterable[float | BlockPlace]) -> Self:
65
- """Creates a value from a list of floats."""
82
+ def _from_list_(cls, values: Iterable[DataValue]) -> Self:
83
+ """Creates a value from a list of data values."""
66
84
  raise NotImplementedError
67
85
 
68
86
  @abstractmethod
69
- def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[float | str | BlockPlace]:
70
- """Converts this value to a list of floats."""
87
+ def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
88
+ """Converts this value to a list of data values."""
71
89
  raise NotImplementedError
72
90
 
73
91
  @classmethod
@@ -76,9 +94,7 @@ class Value:
76
94
  """Returns the keys to a flat representation of this value."""
77
95
  raise NotImplementedError
78
96
 
79
- def _to_flat_dict_(
80
- self, prefix: str, level_refs: dict[Any, str] | None = None
81
- ) -> dict[str, float | str | BlockPlace]:
97
+ def _to_flat_dict_(self, prefix: str, level_refs: dict[Any, str] | None = None) -> dict[str, DataValue | str]:
82
98
  """Converts this value to a flat dictionary."""
83
99
  return dict(zip(self._flat_keys_(prefix), self._to_list_(level_refs), strict=False))
84
100
 
@@ -96,12 +112,12 @@ class Value:
96
112
  v: Num
97
113
 
98
114
  a = 1
99
- b = X(a) # (1) _get() is called on a
100
- c = b.v # (2) _get() is called on the value for v
115
+ b = X(a) # (1) _get_() is called on a
116
+ c = b.v # (2) _get_() is called on the value for v
101
117
 
102
118
  # (1) prevents this from changing the value of a
103
119
  # (2) prevents this from changing the value of c
104
- # Thus, both calls to _get() are necessary to ensure values behave immutably.
120
+ # Thus, both calls to _get_() are necessary to ensure values behave immutably.
105
121
  b.v = 2
106
122
  ```
107
123
  """
@@ -138,6 +154,12 @@ class Value:
138
154
  """Allocates a new value which may be uninitialized."""
139
155
  raise NotImplementedError
140
156
 
157
+ @classmethod
158
+ @abstractmethod
159
+ def _zero_(cls) -> Self:
160
+ """Returns a zero-initialized value of this type."""
161
+ raise NotImplementedError
162
+
141
163
  def __imatmul__(self, other):
142
164
  self._copy_from_(other)
143
165
  return self
@@ -1,7 +1,7 @@
1
1
  from typing import Self
2
2
 
3
3
  from sonolus.backend.ops import Op
4
- from sonolus.script.debug import error
4
+ from sonolus.script.debug import static_error
5
5
  from sonolus.script.internal.native import native_function
6
6
  from sonolus.script.num import Num
7
7
  from sonolus.script.record import Record
@@ -19,6 +19,11 @@ class Interval(Record):
19
19
  start: float
20
20
  end: float
21
21
 
22
+ @classmethod
23
+ def zero(cls) -> Self:
24
+ """Get an empty interval."""
25
+ return cls(0, 0)
26
+
22
27
  @property
23
28
  def length(self) -> float:
24
29
  """The length of the interval.
@@ -29,7 +34,7 @@ class Interval(Record):
29
34
 
30
35
  @property
31
36
  def is_empty(self) -> bool:
32
- """Whether the interval has length of zero or less."""
37
+ """Whether the has a start greater than its end."""
33
38
  return self.start > self.end
34
39
 
35
40
  @property
@@ -57,7 +62,7 @@ class Interval(Record):
57
62
  case Num(value):
58
63
  return self.start <= value <= self.end
59
64
  case _:
60
- error("Invalid type for interval check")
65
+ static_error("Invalid type for interval check")
61
66
 
62
67
  def __add__(self, other: float | int) -> Self:
63
68
  """Add a value to both ends of the interval.
@@ -165,3 +165,20 @@ class _FilteringIterator[T, Fn](Record, SonolusIterator):
165
165
 
166
166
  def advance(self):
167
167
  self.iterator.advance()
168
+
169
+
170
+ class _ChainingIterator[T](Record, SonolusIterator):
171
+ iterator: T
172
+
173
+ def has_next(self) -> bool:
174
+ return self.iterator.has_next()
175
+
176
+ def get(self) -> Any:
177
+ return self.iterator.get().get()
178
+
179
+ def advance(self):
180
+ self.iterator.get().advance()
181
+ while not self.iterator.get().has_next():
182
+ self.iterator.advance()
183
+ if not self.iterator.has_next():
184
+ break