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.
- sonolus/backend/excepthook.py +30 -0
- sonolus/backend/finalize.py +15 -1
- sonolus/backend/ops.py +4 -0
- sonolus/backend/optimize/allocate.py +5 -5
- sonolus/backend/optimize/constant_evaluation.py +124 -19
- sonolus/backend/optimize/copy_coalesce.py +15 -12
- sonolus/backend/optimize/dead_code.py +7 -6
- sonolus/backend/optimize/dominance.py +2 -2
- sonolus/backend/optimize/flow.py +54 -8
- sonolus/backend/optimize/inlining.py +137 -30
- sonolus/backend/optimize/liveness.py +2 -2
- sonolus/backend/optimize/optimize.py +15 -1
- sonolus/backend/optimize/passes.py +11 -3
- sonolus/backend/optimize/simplify.py +137 -8
- sonolus/backend/optimize/ssa.py +47 -13
- sonolus/backend/place.py +5 -4
- sonolus/backend/utils.py +24 -0
- sonolus/backend/visitor.py +260 -17
- sonolus/build/cli.py +47 -19
- sonolus/build/compile.py +12 -5
- sonolus/build/engine.py +70 -1
- sonolus/build/level.py +3 -3
- sonolus/build/project.py +2 -2
- sonolus/script/archetype.py +27 -24
- sonolus/script/array.py +25 -19
- sonolus/script/array_like.py +46 -49
- sonolus/script/bucket.py +1 -1
- sonolus/script/containers.py +22 -26
- sonolus/script/debug.py +24 -47
- sonolus/script/effect.py +1 -1
- sonolus/script/engine.py +2 -2
- sonolus/script/globals.py +3 -3
- sonolus/script/instruction.py +3 -3
- sonolus/script/internal/builtin_impls.py +155 -28
- sonolus/script/internal/constant.py +13 -3
- sonolus/script/internal/context.py +46 -15
- sonolus/script/internal/impl.py +9 -3
- sonolus/script/internal/introspection.py +8 -1
- sonolus/script/internal/math_impls.py +17 -0
- sonolus/script/internal/native.py +5 -5
- sonolus/script/internal/range.py +14 -17
- sonolus/script/internal/simulation_context.py +1 -1
- sonolus/script/internal/transient.py +2 -2
- sonolus/script/internal/value.py +42 -4
- sonolus/script/interval.py +15 -15
- sonolus/script/iterator.py +38 -107
- sonolus/script/maybe.py +139 -0
- sonolus/script/num.py +30 -15
- sonolus/script/options.py +1 -1
- sonolus/script/particle.py +1 -1
- sonolus/script/pointer.py +1 -1
- sonolus/script/project.py +24 -5
- sonolus/script/quad.py +15 -15
- sonolus/script/record.py +21 -12
- sonolus/script/runtime.py +22 -18
- sonolus/script/sprite.py +1 -1
- sonolus/script/stream.py +69 -85
- sonolus/script/transform.py +35 -34
- sonolus/script/values.py +10 -10
- sonolus/script/vec.py +23 -20
- {sonolus_py-0.3.3.dist-info → sonolus_py-0.4.0.dist-info}/METADATA +1 -1
- sonolus_py-0.4.0.dist-info/RECORD +93 -0
- sonolus_py-0.3.3.dist-info/RECORD +0 -92
- {sonolus_py-0.3.3.dist-info → sonolus_py-0.4.0.dist-info}/WHEEL +0 -0
- {sonolus_py-0.3.3.dist-info → sonolus_py-0.4.0.dist-info}/entry_points.txt +0 -0
- {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:
|
|
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
|
|
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
|
|
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]) ->
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
417
|
-
target_value = common_type.
|
|
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
|
sonolus/script/internal/impl.py
CHANGED
|
@@ -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:
|
|
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(
|
|
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:
|
|
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,
|
|
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:
|
|
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
|
sonolus/script/internal/range.py
CHANGED
|
@@ -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[
|
|
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:
|
|
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:
|
|
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:
|
|
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) ->
|
|
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
|
|
74
|
-
if self.step > 0
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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:
|
|
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 |
|
|
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:
|
|
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:
|
|
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:
|
sonolus/script/internal/value.py
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
|
sonolus/script/interval.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from
|
|
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) ->
|
|
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:
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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:
|
|
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) ->
|
|
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) ->
|
|
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.
|