sonolus.py 0.3.3__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 (66) 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 +27 -24
  25. sonolus/script/array.py +25 -19
  26. sonolus/script/array_like.py +46 -49
  27. sonolus/script/bucket.py +1 -1
  28. sonolus/script/containers.py +22 -26
  29. sonolus/script/debug.py +24 -47
  30. sonolus/script/effect.py +1 -1
  31. sonolus/script/engine.py +2 -2
  32. sonolus/script/globals.py +3 -3
  33. sonolus/script/instruction.py +3 -3
  34. sonolus/script/internal/builtin_impls.py +155 -28
  35. sonolus/script/internal/constant.py +13 -3
  36. sonolus/script/internal/context.py +46 -15
  37. sonolus/script/internal/impl.py +9 -3
  38. sonolus/script/internal/introspection.py +8 -1
  39. sonolus/script/internal/math_impls.py +17 -0
  40. sonolus/script/internal/native.py +5 -5
  41. sonolus/script/internal/range.py +14 -17
  42. sonolus/script/internal/simulation_context.py +1 -1
  43. sonolus/script/internal/transient.py +2 -2
  44. sonolus/script/internal/value.py +42 -4
  45. sonolus/script/interval.py +15 -15
  46. sonolus/script/iterator.py +38 -107
  47. sonolus/script/maybe.py +139 -0
  48. sonolus/script/num.py +30 -15
  49. sonolus/script/options.py +1 -1
  50. sonolus/script/particle.py +1 -1
  51. sonolus/script/pointer.py +1 -1
  52. sonolus/script/project.py +24 -5
  53. sonolus/script/quad.py +15 -15
  54. sonolus/script/record.py +21 -12
  55. sonolus/script/runtime.py +22 -18
  56. sonolus/script/sprite.py +1 -1
  57. sonolus/script/stream.py +69 -85
  58. sonolus/script/transform.py +35 -34
  59. sonolus/script/values.py +10 -10
  60. sonolus/script/vec.py +23 -20
  61. {sonolus_py-0.3.3.dist-info → sonolus_py-0.4.0.dist-info}/METADATA +1 -1
  62. sonolus_py-0.4.0.dist-info/RECORD +93 -0
  63. sonolus_py-0.3.3.dist-info/RECORD +0 -92
  64. {sonolus_py-0.3.3.dist-info → sonolus_py-0.4.0.dist-info}/WHEEL +0 -0
  65. {sonolus_py-0.3.3.dist-info → sonolus_py-0.4.0.dist-info}/entry_points.txt +0 -0
  66. {sonolus_py-0.3.3.dist-info → sonolus_py-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Iterable
3
4
  from contextlib import contextmanager
4
5
  from contextvars import ContextVar
5
6
  from dataclasses import dataclass
@@ -7,7 +8,7 @@ from threading import Lock
7
8
  from typing import Any, Self
8
9
 
9
10
  from sonolus.backend.blocks import BlockData, PlayBlock
10
- from sonolus.backend.ir import IRConst, IRStmt
11
+ from sonolus.backend.ir import IRConst, IRExpr, IRStmt
11
12
  from sonolus.backend.mode import Mode
12
13
  from sonolus.backend.optimize.flow import BasicBlock, FlowEdge
13
14
  from sonolus.backend.place import Block, BlockPlace, TempBlock
@@ -16,7 +17,7 @@ from sonolus.script.internal.value import Value
16
17
 
17
18
  _compiler_internal_ = True
18
19
 
19
- context_var = ContextVar("context_var", default=None)
20
+ context_var: ContextVar[Context | None] = ContextVar("context_var", default=None) # type: ignore
20
21
 
21
22
 
22
23
  @dataclass(frozen=True)
@@ -70,7 +71,7 @@ class Context:
70
71
  global_state: GlobalContextState
71
72
  callback_state: CallbackContextState
72
73
  statements: list[IRStmt]
73
- test: IRStmt
74
+ test: IRExpr
74
75
  outgoing: dict[float | None, Context]
75
76
  scope: Scope
76
77
  loop_variables: dict[str, Value]
@@ -115,7 +116,7 @@ class Context:
115
116
  return
116
117
  if not self.callback:
117
118
  return
118
- if isinstance(place.block, BlockData) and self.callback not in self.blocks(place.block).readable:
119
+ if isinstance(place.block, BlockData) and not self.is_readable(place):
119
120
  raise RuntimeError(f"Block {place.block} is not readable in {self.callback}")
120
121
 
121
122
  def check_writable(self, place: BlockPlace):
@@ -123,9 +124,19 @@ class Context:
123
124
  return
124
125
  if not self.callback:
125
126
  return
126
- if isinstance(place.block, BlockData) and self.callback not in self.blocks(place.block).writable:
127
+ if isinstance(place.block, BlockData) and not self.is_writable(place):
127
128
  raise RuntimeError(f"Block {place.block} is not writable in {self.callback}")
128
129
 
130
+ def is_readable(self, place: BlockPlace) -> bool:
131
+ if debug_config().unchecked_reads:
132
+ return True
133
+ return self.callback and self.callback in self.blocks(place.block).readable
134
+
135
+ def is_writable(self, place: BlockPlace) -> bool:
136
+ if debug_config().unchecked_writes:
137
+ return True
138
+ return self.callback and self.callback in self.blocks(place.block).writable
139
+
129
140
  def add_statement(self, statement: IRStmt):
130
141
  if not self.live:
131
142
  return
@@ -169,20 +180,26 @@ class Context:
169
180
  self.outgoing[condition] = result
170
181
  return result
171
182
 
183
+ def new_disconnected(self):
184
+ return self.copy_with_scope(self.scope.copy())
185
+
186
+ def new_empty_disconnected(self):
187
+ return self.copy_with_scope(Scope())
188
+
172
189
  def into_dead(self):
173
190
  """Create a new context for code that is unreachable, like after a return statement."""
174
191
  result = self.copy_with_scope(self.scope.copy())
175
192
  result.live = False
176
193
  return result
177
194
 
178
- def prepare_loop_header(self, to_merge: set[str]) -> Self:
195
+ def prepare_loop_header(self, to_merge: set[str]) -> Context:
179
196
  # to_merge is the set of bindings set anywhere in the loop
180
197
  # we need to invalidate them in the header if they're reference types
181
198
  # or merge them if they're value types
182
199
  # structure is self -> intermediate -> header -> body (continue -> header) | exit
183
200
  assert len(self.outgoing) == 0
184
201
  header = self.branch(None)
185
- for name in to_merge:
202
+ for name in sorted(to_merge):
186
203
  binding = self.scope.get_binding(name)
187
204
  if not isinstance(binding, ValueBinding):
188
205
  continue
@@ -205,12 +222,15 @@ class Context:
205
222
  assert len(self.outgoing) == 0
206
223
  self.outgoing[None] = header
207
224
  values = {}
208
- # First do a pass through and copy every value
225
+ # First do a pass through and get every value
209
226
  for name, target_value in header.loop_variables.items():
210
227
  with using_ctx(self):
211
228
  if type(target_value)._is_value_type_():
212
229
  value = self.scope.get_value(name)
213
- values[name] = value._get_() # _get_() will make a copy on value types
230
+ # We make this call to _get_readonly_() to ensure that we're reading the value at this
231
+ # point in time specifically, since _get_readonly_ will make a copy if the value is
232
+ # e.g. a Num backed by a TempBlock which could be mutated.
233
+ values[name] = value._get_readonly_()
214
234
  # Then actually set them
215
235
  for name, target_value in header.loop_variables.items():
216
236
  with using_ctx(self):
@@ -236,7 +256,7 @@ class Context:
236
256
  with self.global_state.lock:
237
257
  block = value.blocks.get(self.global_state.mode)
238
258
  if block is None:
239
- raise RuntimeError(f"Global {value.name} is not available in '{self.global_state.mode.name}' mode")
259
+ raise RuntimeError(f"Global {value} is not available in '{self.global_state.mode.name}' mode")
240
260
  if value not in self.global_state.environment_mappings:
241
261
  if value.offset is None:
242
262
  offset = self.global_state.environment_offsets.get(block, 0)
@@ -268,7 +288,7 @@ class Context:
268
288
  return self.global_state.archetypes[type_]
269
289
 
270
290
 
271
- def ctx() -> Context | None:
291
+ def ctx() -> Context | Any: # Using Any to silence type checker warnings if it's None
272
292
  return context_var.get()
273
293
 
274
294
 
@@ -368,7 +388,7 @@ class Scope:
368
388
  def set_binding(self, name: str, binding: Binding):
369
389
  self.bindings[name] = binding
370
390
 
371
- def get_value(self, name: str) -> Value:
391
+ def get_value(self, name: str) -> Value | Any:
372
392
  binding = self.get_binding(name)
373
393
  match binding:
374
394
  case ValueBinding(value):
@@ -398,7 +418,7 @@ class Scope:
398
418
  return
399
419
  assert all(len(inc.outgoing) == 0 for inc in incoming)
400
420
  sources = [context.scope for context in incoming]
401
- keys = {key for source in sources for key in source.bindings}
421
+ keys = unique(key for source in sources for key in source.bindings)
402
422
  for key in keys:
403
423
  bindings = [source.get_binding(key) for source in sources]
404
424
  if not all(isinstance(binding, ValueBinding) for binding in bindings):
@@ -413,8 +433,9 @@ class Scope:
413
433
  target.scope.set_binding(key, ConflictBinding())
414
434
  continue
415
435
  common_type: type[Value] = types.pop()
416
- if common_type._is_value_type_():
417
- target_value = common_type._from_place_(target.alloc(size=common_type._size_()))
436
+ with using_ctx(target):
437
+ target_value = common_type._get_merge_target_(values)
438
+ if target_value is not NotImplemented:
418
439
  for inc in incoming:
419
440
  with using_ctx(inc):
420
441
  target_value._set_(inc.scope.get_value(key))
@@ -447,3 +468,13 @@ def context_to_cfg(context: Context) -> BasicBlock:
447
468
  blocks[current].outgoing.add(edge)
448
469
  blocks[target].incoming.add(edge)
449
470
  return blocks[context]
471
+
472
+
473
+ def unique[T](iterable: Iterable[T]) -> list[T]:
474
+ result = []
475
+ seen = set()
476
+ for item in iterable:
477
+ if item not in seen:
478
+ seen.add(item)
479
+ result.append(item)
480
+ return result
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from collections.abc import Callable
4
4
  from enum import Enum
5
5
  from types import EllipsisType, FunctionType, MethodType, ModuleType, NoneType, NotImplementedType, UnionType
6
- from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeVar, get_origin, overload
6
+ from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeVar, Union, get_origin, overload
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from sonolus.script.internal.value import Value
@@ -33,7 +33,7 @@ def meta_fn(fn=None):
33
33
  return decorator(fn)
34
34
 
35
35
 
36
- def validate_value(value: Any) -> Value:
36
+ def validate_value[T](value: T) -> Value | T:
37
37
  result = try_validate_value(value)
38
38
  if result is None:
39
39
  raise TypeError(f"Unsupported value: {value!r}")
@@ -42,7 +42,7 @@ def validate_value(value: Any) -> Value:
42
42
 
43
43
  def try_validate_value(value: Any) -> Value | None:
44
44
  from sonolus.script.globals import _GlobalPlaceholder
45
- from sonolus.script.internal.constant import BasicConstantValue
45
+ from sonolus.script.internal.constant import BasicConstantValue, TypingSpecialFormConstant
46
46
  from sonolus.script.internal.dict_impl import DictImpl
47
47
  from sonolus.script.internal.generic import PartialGeneric
48
48
  from sonolus.script.internal.tuple_impl import TupleImpl
@@ -85,6 +85,12 @@ def try_validate_value(value: Any) -> Value | None:
85
85
  | EllipsisType()
86
86
  ):
87
87
  return BasicConstantValue.of(value)
88
+ case special_form if value in {
89
+ Literal,
90
+ Annotated,
91
+ Union,
92
+ }:
93
+ return TypingSpecialFormConstant.of(special_form)
88
94
  case other_type if get_origin(value) in {Literal, Annotated, UnionType, tuple}:
89
95
  return BasicConstantValue.of(other_type)
90
96
  case _GlobalPlaceholder():
@@ -4,7 +4,14 @@ from typing import Annotated
4
4
  _missing = object()
5
5
 
6
6
 
7
- def get_field_specifiers(cls, *, skip: set[str] = frozenset(), globals=None, locals=None, eval_str=True): # noqa: A002
7
+ def get_field_specifiers(
8
+ cls,
9
+ *,
10
+ skip: frozenset[str] | set[str] = frozenset(),
11
+ globals=None, # noqa: A002
12
+ locals=None, # noqa: A002
13
+ eval_str=True,
14
+ ):
8
15
  """Like inspect.get_annotations, but also turns class attributes into Annotated."""
9
16
  results = inspect.get_annotations(cls, globals=globals, locals=locals, eval_str=eval_str)
10
17
  for key, value in results.items():
@@ -97,6 +97,22 @@ def _log(x: float, base: float | None = None) -> float:
97
97
  return _ln(x) / _ln(base)
98
98
 
99
99
 
100
+ def _sqrt(x: float) -> float:
101
+ return x**0.5
102
+
103
+
104
+ @native_function(Op.Degree)
105
+ def _degrees(x: float) -> float:
106
+ """Convert radians to degrees."""
107
+ return math.degrees(x)
108
+
109
+
110
+ @native_function(Op.Radian)
111
+ def _radians(x: float) -> float:
112
+ """Convert degrees to radians."""
113
+ return math.radians(x)
114
+
115
+
100
116
  @native_function(Op.Rem)
101
117
  def _remainder(x: float, y: float) -> float:
102
118
  # This is different from math.remainder in Python's math package, which could be confusing
@@ -119,4 +135,5 @@ MATH_BUILTIN_IMPLS = {
119
135
  id(math.trunc): _trunc,
120
136
  id(round): _round,
121
137
  id(math.log): _log,
138
+ id(math.sqrt): _sqrt,
122
139
  }
@@ -9,7 +9,7 @@ from sonolus.script.internal.impl import meta_fn, validate_value
9
9
  from sonolus.script.num import Num, _is_num
10
10
 
11
11
 
12
- def native_call(op: Op, *args: Num) -> Num:
12
+ def native_call(op: Op, *args: int | float | bool) -> Num:
13
13
  if not ctx():
14
14
  raise RuntimeError("Unexpected native call")
15
15
  args = tuple(validate_value(arg) for arg in args)
@@ -21,20 +21,20 @@ def native_call(op: Op, *args: Num) -> Num:
21
21
 
22
22
 
23
23
  def native_function[**P, R](op: Op) -> Callable[[Callable[P, R]], Callable[P, R]]:
24
- def decorator(fn: Callable[P, Num]) -> Callable[P, Num]:
24
+ def decorator(fn: Callable[P, int | float | bool]) -> Callable[P, Num]:
25
25
  signature = inspect.signature(fn)
26
26
 
27
27
  @functools.wraps(fn)
28
28
  @meta_fn
29
- def wrapper(*args: Num) -> Num:
29
+ def wrapper(*args: int | float | bool) -> Num:
30
30
  if len(args) < sum(1 for p in signature.parameters.values() if p.default == inspect.Parameter.empty):
31
31
  raise TypeError(f"Expected {len(signature.parameters)} arguments, got {len(args)}")
32
32
  if ctx():
33
33
  bound_args = signature.bind(*args)
34
34
  bound_args.apply_defaults()
35
35
  return native_call(op, *(Num._accept_(arg) for arg in bound_args.args))
36
- return fn(*args)
36
+ return fn(*args) # type: ignore
37
37
 
38
38
  return wrapper
39
39
 
40
- return decorator
40
+ return decorator # type: ignore
@@ -2,16 +2,17 @@ from sonolus.script.array_like import ArrayLike, get_positive_index
2
2
  from sonolus.script.internal.context import ctx
3
3
  from sonolus.script.internal.impl import meta_fn, validate_value
4
4
  from sonolus.script.iterator import SonolusIterator
5
+ from sonolus.script.maybe import Maybe, Nothing, Some
5
6
  from sonolus.script.num import Num
6
7
  from sonolus.script.record import Record
7
8
 
8
9
 
9
- class Range(Record, ArrayLike[Num]):
10
+ class Range(Record, ArrayLike[int]):
10
11
  start: int
11
12
  stop: int
12
13
  step: int
13
14
 
14
- def __new__(cls, start: Num, stop: Num | None = None, step: Num = 1):
15
+ def __new__(cls, start: int, stop: int | None = None, step: int = 1):
15
16
  if stop is None:
16
17
  start, stop = 0, start
17
18
  return super().__new__(cls, start, stop, step)
@@ -37,14 +38,14 @@ class Range(Record, ArrayLike[Num]):
37
38
  return 0
38
39
  return (diff - self.step - 1) // -self.step
39
40
 
40
- def __getitem__(self, index: Num) -> Num:
41
+ def __getitem__(self, index: int) -> int:
41
42
  return self.start + get_positive_index(index, len(self)) * self.step
42
43
 
43
- def __setitem__(self, index: Num, value: Num):
44
+ def __setitem__(self, index: int, value: int):
44
45
  raise TypeError("Range does not support item assignment")
45
46
 
46
47
  @property
47
- def last(self) -> Num:
48
+ def last(self) -> int:
48
49
  return self[len(self) - 1]
49
50
 
50
51
  def __eq__(self, other):
@@ -70,21 +71,17 @@ class RangeIterator(Record, SonolusIterator):
70
71
  stop: int
71
72
  step: int
72
73
 
73
- def has_next(self) -> bool:
74
- if self.step > 0:
75
- return self.value < self.stop
76
- else:
77
- return self.value > self.stop
78
-
79
- def get(self) -> int:
80
- return self.value
81
-
82
- def advance(self):
83
- self.value += self.step
74
+ def next(self) -> Maybe[int]:
75
+ has_next = self.value < self.stop if self.step > 0 else self.value > self.stop
76
+ if has_next:
77
+ current = self.value
78
+ self.value += self.step
79
+ return Some(current)
80
+ return Nothing
84
81
 
85
82
 
86
83
  @meta_fn
87
- def range_or_tuple(start: Num, stop: Num | None = None, step: Num = 1) -> Range | tuple[Num, ...]:
84
+ def range_or_tuple(start: int, stop: int | None = None, step: int = 1) -> Range | tuple[int, ...]:
88
85
  if stop is None:
89
86
  start, stop = 0, start
90
87
  if not ctx():
@@ -126,6 +126,6 @@ class SimulationContext:
126
126
  SimulationContext._active_context = None
127
127
 
128
128
 
129
- def sim_ctx() -> SimulationContext | None:
129
+ def sim_ctx() -> SimulationContext | Any:
130
130
  """Get the current simulation context, or None if not active."""
131
131
  return SimulationContext._active_context
@@ -36,11 +36,11 @@ class TransientValue(Value):
36
36
  def _get_(self) -> Self:
37
37
  return self
38
38
 
39
- def _set_(self, value: Self) -> None:
39
+ def _set_(self, value: Any) -> None:
40
40
  if value is not self:
41
41
  raise TypeError(f"{type(self).__name__} is immutable")
42
42
 
43
- def _copy_from_(self, value: Self):
43
+ def _copy_from_(self, value: Any):
44
44
  raise TypeError(f"{type(self).__name__} is immutable")
45
45
 
46
46
  def _copy_(self) -> Self:
@@ -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
@@ -122,7 +123,7 @@ class Value:
122
123
  For instance:
123
124
  ```
124
125
  class X(Record):
125
- v: Num
126
+ v: int
126
127
 
127
128
  a = 1
128
129
  b = X(a) # (1) _get_() is called on a
@@ -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
 
@@ -36,7 +36,7 @@ class Interval(Record):
36
36
 
37
37
  @property
38
38
  def is_empty(self) -> bool:
39
- """Whether the has a start greater than its end."""
39
+ """Whether the interval has a start greater than its end."""
40
40
  return self.start > self.end
41
41
 
42
42
  @property
@@ -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:
@@ -377,7 +377,7 @@ def interp_clamped(
377
377
  xp: ArrayLike[float] | tuple[float, ...],
378
378
  fp: ArrayLike[float] | tuple[float, ...],
379
379
  x: float,
380
- ):
380
+ ) -> float:
381
381
  """Linearly interpolate a value within a sequence of points.
382
382
 
383
383
  The sequence must have at least 2 elements and be sorted in increasing order of x-coordinates.