sonolus.py 0.3.1__py3-none-any.whl → 0.3.3__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/finalize.py +16 -4
- sonolus/backend/node.py +13 -5
- sonolus/backend/optimize/allocate.py +41 -4
- sonolus/backend/optimize/flow.py +24 -7
- sonolus/backend/optimize/optimize.py +2 -9
- sonolus/backend/utils.py +6 -1
- sonolus/backend/visitor.py +72 -23
- sonolus/build/cli.py +6 -1
- sonolus/build/engine.py +1 -1
- sonolus/script/archetype.py +52 -24
- sonolus/script/array.py +20 -8
- sonolus/script/array_like.py +30 -3
- sonolus/script/containers.py +27 -7
- sonolus/script/debug.py +66 -8
- sonolus/script/globals.py +17 -0
- sonolus/script/internal/builtin_impls.py +12 -8
- sonolus/script/internal/context.py +55 -1
- sonolus/script/internal/range.py +25 -2
- sonolus/script/internal/simulation_context.py +131 -0
- sonolus/script/internal/tuple_impl.py +18 -11
- sonolus/script/interval.py +60 -2
- sonolus/script/iterator.py +3 -2
- sonolus/script/num.py +11 -2
- sonolus/script/options.py +24 -1
- sonolus/script/quad.py +41 -3
- sonolus/script/record.py +24 -3
- sonolus/script/runtime.py +411 -0
- sonolus/script/stream.py +133 -16
- sonolus/script/transform.py +291 -2
- sonolus/script/values.py +9 -3
- sonolus/script/vec.py +14 -2
- {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/METADATA +1 -1
- {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/RECORD +36 -35
- {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/WHEEL +0 -0
- {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/entry_points.txt +0 -0
- {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -19,6 +19,25 @@ _compiler_internal_ = True
|
|
|
19
19
|
context_var = ContextVar("context_var", default=None)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class DebugConfig:
|
|
24
|
+
unchecked_reads: bool
|
|
25
|
+
unchecked_writes: bool
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_full_debug_config = DebugConfig(
|
|
29
|
+
unchecked_reads=True,
|
|
30
|
+
unchecked_writes=True,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
_disabled_debug_config = DebugConfig(
|
|
34
|
+
unchecked_reads=False,
|
|
35
|
+
unchecked_writes=False,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
debug_var = ContextVar("debug_var", default=_disabled_debug_config)
|
|
39
|
+
|
|
40
|
+
|
|
22
41
|
class GlobalContextState:
|
|
23
42
|
archetypes: dict[type, int]
|
|
24
43
|
rom: ReadOnlyMemory
|
|
@@ -92,12 +111,16 @@ class Context:
|
|
|
92
111
|
return self.callback_state.used_names
|
|
93
112
|
|
|
94
113
|
def check_readable(self, place: BlockPlace):
|
|
114
|
+
if debug_config().unchecked_reads:
|
|
115
|
+
return
|
|
95
116
|
if not self.callback:
|
|
96
117
|
return
|
|
97
118
|
if isinstance(place.block, BlockData) and self.callback not in self.blocks(place.block).readable:
|
|
98
119
|
raise RuntimeError(f"Block {place.block} is not readable in {self.callback}")
|
|
99
120
|
|
|
100
121
|
def check_writable(self, place: BlockPlace):
|
|
122
|
+
if debug_config().unchecked_writes:
|
|
123
|
+
return
|
|
101
124
|
if not self.callback:
|
|
102
125
|
return
|
|
103
126
|
if isinstance(place.block, BlockData) and self.callback not in self.blocks(place.block).writable:
|
|
@@ -238,6 +261,12 @@ class Context:
|
|
|
238
261
|
context.outgoing[None] = target
|
|
239
262
|
return target
|
|
240
263
|
|
|
264
|
+
def register_archetype(self, type_: type) -> int:
|
|
265
|
+
with self.global_state.lock:
|
|
266
|
+
if type_ not in self.global_state.archetypes:
|
|
267
|
+
self.global_state.archetypes[type_] = len(self.global_state.archetypes)
|
|
268
|
+
return self.global_state.archetypes[type_]
|
|
269
|
+
|
|
241
270
|
|
|
242
271
|
def ctx() -> Context | None:
|
|
243
272
|
return context_var.get()
|
|
@@ -262,7 +291,11 @@ class ReadOnlyMemory:
|
|
|
262
291
|
_lock: Lock
|
|
263
292
|
|
|
264
293
|
def __init__(self):
|
|
265
|
-
self.values = [
|
|
294
|
+
self.values = [
|
|
295
|
+
float("nan"),
|
|
296
|
+
float("inf"),
|
|
297
|
+
float("-inf"),
|
|
298
|
+
]
|
|
266
299
|
self.indexes = {}
|
|
267
300
|
self._lock = Lock()
|
|
268
301
|
|
|
@@ -283,6 +316,27 @@ class ReadOnlyMemory:
|
|
|
283
316
|
else:
|
|
284
317
|
return PlayBlock.EngineRom
|
|
285
318
|
|
|
319
|
+
def get_value(self, index: int) -> float:
|
|
320
|
+
with self._lock:
|
|
321
|
+
if index < 0 or index >= len(self.values):
|
|
322
|
+
raise IndexError(f"Index {index} out of bounds for ReadOnlyMemory")
|
|
323
|
+
return self.values[index]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@contextmanager
|
|
327
|
+
def enable_debug(config: DebugConfig | None = None):
|
|
328
|
+
if config is None:
|
|
329
|
+
config = _full_debug_config
|
|
330
|
+
token = debug_var.set(config)
|
|
331
|
+
try:
|
|
332
|
+
yield
|
|
333
|
+
finally:
|
|
334
|
+
debug_var.reset(token)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def debug_config() -> DebugConfig:
|
|
338
|
+
return debug_var.get()
|
|
339
|
+
|
|
286
340
|
|
|
287
341
|
@dataclass
|
|
288
342
|
class ValueBinding:
|
sonolus/script/internal/range.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
from sonolus.script.array_like import ArrayLike
|
|
1
|
+
from sonolus.script.array_like import ArrayLike, get_positive_index
|
|
2
|
+
from sonolus.script.internal.context import ctx
|
|
3
|
+
from sonolus.script.internal.impl import meta_fn, validate_value
|
|
2
4
|
from sonolus.script.iterator import SonolusIterator
|
|
3
5
|
from sonolus.script.num import Num
|
|
4
6
|
from sonolus.script.record import Record
|
|
@@ -36,7 +38,7 @@ class Range(Record, ArrayLike[Num]):
|
|
|
36
38
|
return (diff - self.step - 1) // -self.step
|
|
37
39
|
|
|
38
40
|
def __getitem__(self, index: Num) -> Num:
|
|
39
|
-
return self.start + index * self.step
|
|
41
|
+
return self.start + get_positive_index(index, len(self)) * self.step
|
|
40
42
|
|
|
41
43
|
def __setitem__(self, index: Num, value: Num):
|
|
42
44
|
raise TypeError("Range does not support item assignment")
|
|
@@ -79,3 +81,24 @@ class RangeIterator(Record, SonolusIterator):
|
|
|
79
81
|
|
|
80
82
|
def advance(self):
|
|
81
83
|
self.value += self.step
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@meta_fn
|
|
87
|
+
def range_or_tuple(start: Num, stop: Num | None = None, step: Num = 1) -> Range | tuple[Num, ...]:
|
|
88
|
+
if stop is None:
|
|
89
|
+
start, stop = 0, start
|
|
90
|
+
if not ctx():
|
|
91
|
+
return range(start, stop, step) # type: ignore
|
|
92
|
+
start = Num._accept_(start)
|
|
93
|
+
stop = Num._accept_(stop) if stop is not None else None
|
|
94
|
+
step = Num._accept_(step)
|
|
95
|
+
if start._is_py_() and stop._is_py_() and step._is_py_():
|
|
96
|
+
start_int = start._as_py_()
|
|
97
|
+
stop_int = stop._as_py_() if stop is not None else None
|
|
98
|
+
if stop_int is None:
|
|
99
|
+
start_int, stop_int = 0, start_int
|
|
100
|
+
step_int = step._as_py_()
|
|
101
|
+
if start_int % 1 != 0 or stop_int % 1 != 0 or step_int % 1 != 0:
|
|
102
|
+
raise TypeError("Range arguments must be integers")
|
|
103
|
+
return validate_value(tuple(range(int(start_int), int(stop_int), int(step_int)))) # type: ignore
|
|
104
|
+
return Range(start, stop, step)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
import sys
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from types import MethodType, ModuleType
|
|
5
|
+
from typing import Any, ClassVar, Self
|
|
6
|
+
|
|
7
|
+
from sonolus.script.internal.impl import validate_value
|
|
8
|
+
|
|
9
|
+
_MISSING = object()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SimulationContext:
|
|
13
|
+
"""Context manager to simulate additional Sonolus runtime features like level memory."""
|
|
14
|
+
|
|
15
|
+
_active_context: ClassVar[Self | None] = None
|
|
16
|
+
additional_replacements: dict[Any, Any]
|
|
17
|
+
values: dict[Any, Any]
|
|
18
|
+
_original_values: dict[str, dict[str, Any]] # module_name: {var_name: original_value}
|
|
19
|
+
_original_import: Callable[[str, Any, Any, Any, Any], Any] | None
|
|
20
|
+
|
|
21
|
+
def __init__(self, *, additional_replacements: dict[Any, Any] | None = None):
|
|
22
|
+
if SimulationContext._active_context is not None:
|
|
23
|
+
raise RuntimeError("SimulationContext is already active")
|
|
24
|
+
SimulationContext._active_context = self
|
|
25
|
+
self.additional_replacements = additional_replacements or {}
|
|
26
|
+
self.values = {}
|
|
27
|
+
self._original_values = {}
|
|
28
|
+
self._original_import = None
|
|
29
|
+
|
|
30
|
+
def get_or_put_value(self, key: Any, factory: Callable[[], Any]) -> Any:
|
|
31
|
+
if key not in self.values:
|
|
32
|
+
self.values[key] = validate_value(factory())
|
|
33
|
+
return self.values[key]._get_()._as_py_()
|
|
34
|
+
|
|
35
|
+
def set_or_put_value(self, key: Any, factory: Callable[[], Any], value: Any):
|
|
36
|
+
if key not in self.values:
|
|
37
|
+
self.values[key] = validate_value(factory())
|
|
38
|
+
existing_value = self.values[key]
|
|
39
|
+
existing_type = type(existing_value)
|
|
40
|
+
value = existing_type._accept_(validate_value(value))
|
|
41
|
+
if existing_type._is_value_type_():
|
|
42
|
+
existing_value._set_(value)
|
|
43
|
+
else:
|
|
44
|
+
existing_value._copy_from_(value)
|
|
45
|
+
|
|
46
|
+
def _get_replacement(self, value: Any) -> Any:
|
|
47
|
+
try:
|
|
48
|
+
if value in self.additional_replacements:
|
|
49
|
+
return self.additional_replacements[value]
|
|
50
|
+
except TypeError:
|
|
51
|
+
pass
|
|
52
|
+
if hasattr(value, "_get_sim_replacement_") and isinstance(value._get_sim_replacement_, MethodType):
|
|
53
|
+
return value._get_sim_replacement_()
|
|
54
|
+
return _MISSING
|
|
55
|
+
|
|
56
|
+
def _substitute_module_variables(self, module) -> None:
|
|
57
|
+
if not isinstance(module, ModuleType):
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
module_name = module.__name__
|
|
61
|
+
|
|
62
|
+
if module_name in self._original_values:
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
original_values = {}
|
|
66
|
+
|
|
67
|
+
for var_name, var_value in list(module.__dict__.items()):
|
|
68
|
+
replacement = self._get_replacement(var_value)
|
|
69
|
+
if replacement is not _MISSING:
|
|
70
|
+
original_values[var_name] = var_value
|
|
71
|
+
setattr(module, var_name, replacement)
|
|
72
|
+
|
|
73
|
+
if original_values:
|
|
74
|
+
self._original_values[module_name] = original_values
|
|
75
|
+
|
|
76
|
+
def _update_loaded_modules(self) -> None:
|
|
77
|
+
loaded_modules = list(sys.modules.values())
|
|
78
|
+
|
|
79
|
+
for module in loaded_modules:
|
|
80
|
+
if module is not None:
|
|
81
|
+
self._substitute_module_variables(module)
|
|
82
|
+
|
|
83
|
+
def _create_import_hook(self):
|
|
84
|
+
original_import = builtins.__import__
|
|
85
|
+
|
|
86
|
+
def hooked_import(name, globals=None, locals=None, fromlist=(), level=0): # noqa: A002
|
|
87
|
+
module = original_import(name, globals, locals, fromlist, level)
|
|
88
|
+
if isinstance(module, ModuleType):
|
|
89
|
+
self._substitute_module_variables(module)
|
|
90
|
+
|
|
91
|
+
if fromlist:
|
|
92
|
+
for item in fromlist:
|
|
93
|
+
if hasattr(module, item):
|
|
94
|
+
attr = getattr(module, item)
|
|
95
|
+
if isinstance(attr, ModuleType):
|
|
96
|
+
self._substitute_module_variables(attr)
|
|
97
|
+
|
|
98
|
+
return module
|
|
99
|
+
|
|
100
|
+
return hooked_import
|
|
101
|
+
|
|
102
|
+
def __enter__(self) -> Self:
|
|
103
|
+
self._update_loaded_modules()
|
|
104
|
+
|
|
105
|
+
self._original_import = builtins.__import__
|
|
106
|
+
builtins.__import__ = self._create_import_hook()
|
|
107
|
+
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any):
|
|
111
|
+
try:
|
|
112
|
+
if self._original_import is not None:
|
|
113
|
+
builtins.__import__ = self._original_import
|
|
114
|
+
self._original_import = None
|
|
115
|
+
|
|
116
|
+
for module_name, original_values in self._original_values.items():
|
|
117
|
+
if module_name in sys.modules:
|
|
118
|
+
module = sys.modules[module_name]
|
|
119
|
+
if isinstance(module, ModuleType):
|
|
120
|
+
for var_name, original_value in original_values.items():
|
|
121
|
+
setattr(module, var_name, original_value)
|
|
122
|
+
|
|
123
|
+
self._original_values.clear()
|
|
124
|
+
|
|
125
|
+
finally:
|
|
126
|
+
SimulationContext._active_context = None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def sim_ctx() -> SimulationContext | None:
|
|
130
|
+
"""Get the current simulation context, or None if not active."""
|
|
131
|
+
return SimulationContext._active_context
|
|
@@ -21,8 +21,10 @@ class TupleImpl(TransientValue):
|
|
|
21
21
|
raise TypeError(f"Cannot index tuple with {item}")
|
|
22
22
|
if int(item) != item:
|
|
23
23
|
raise TypeError(f"Cannot index tuple with non-integer {item}")
|
|
24
|
-
if not (
|
|
24
|
+
if not (-len(self.value) <= item < len(self.value)):
|
|
25
25
|
raise IndexError(f"Tuple index out of range: {item}")
|
|
26
|
+
if item < 0:
|
|
27
|
+
item += len(self.value)
|
|
26
28
|
return self.value[int(item)]
|
|
27
29
|
|
|
28
30
|
@meta_fn
|
|
@@ -30,7 +32,7 @@ class TupleImpl(TransientValue):
|
|
|
30
32
|
return len(self.value)
|
|
31
33
|
|
|
32
34
|
def __eq__(self, other):
|
|
33
|
-
if not
|
|
35
|
+
if not self._is_tuple_impl(other):
|
|
34
36
|
return False
|
|
35
37
|
if len(self) != len(other):
|
|
36
38
|
return False
|
|
@@ -40,7 +42,7 @@ class TupleImpl(TransientValue):
|
|
|
40
42
|
return True
|
|
41
43
|
|
|
42
44
|
def __ne__(self, other):
|
|
43
|
-
if not
|
|
45
|
+
if not self._is_tuple_impl(other):
|
|
44
46
|
return True
|
|
45
47
|
if len(self) != len(other):
|
|
46
48
|
return True
|
|
@@ -50,33 +52,33 @@ class TupleImpl(TransientValue):
|
|
|
50
52
|
return False
|
|
51
53
|
|
|
52
54
|
def __lt__(self, other):
|
|
53
|
-
if not
|
|
55
|
+
if not self._is_tuple_impl(other):
|
|
54
56
|
return NotImplemented
|
|
55
|
-
for a, b in zip(self
|
|
57
|
+
for a, b in zip(self, other):
|
|
56
58
|
if a != b:
|
|
57
59
|
return a < b
|
|
58
60
|
return len(self.value) < len(other.value)
|
|
59
61
|
|
|
60
62
|
def __le__(self, other):
|
|
61
|
-
if not
|
|
63
|
+
if not self._is_tuple_impl(other):
|
|
62
64
|
return NotImplemented
|
|
63
|
-
for a, b in zip(self
|
|
65
|
+
for a, b in zip(self, other):
|
|
64
66
|
if a != b:
|
|
65
67
|
return a < b
|
|
66
68
|
return len(self.value) <= len(other.value)
|
|
67
69
|
|
|
68
70
|
def __gt__(self, other):
|
|
69
|
-
if not
|
|
71
|
+
if not self._is_tuple_impl(other):
|
|
70
72
|
return NotImplemented
|
|
71
|
-
for a, b in zip(self
|
|
73
|
+
for a, b in zip(self, other):
|
|
72
74
|
if a != b:
|
|
73
75
|
return a > b
|
|
74
76
|
return len(self.value) > len(other.value)
|
|
75
77
|
|
|
76
78
|
def __ge__(self, other):
|
|
77
|
-
if not
|
|
79
|
+
if not self._is_tuple_impl(other):
|
|
78
80
|
return NotImplemented
|
|
79
|
-
for a, b in zip(self
|
|
81
|
+
for a, b in zip(self, other):
|
|
80
82
|
if a != b:
|
|
81
83
|
return a > b
|
|
82
84
|
return len(self.value) >= len(other.value)
|
|
@@ -89,6 +91,11 @@ class TupleImpl(TransientValue):
|
|
|
89
91
|
other = TupleImpl._accept_(other)
|
|
90
92
|
return TupleImpl._accept_(self.value + other.value)
|
|
91
93
|
|
|
94
|
+
@staticmethod
|
|
95
|
+
@meta_fn
|
|
96
|
+
def _is_tuple_impl(value: Any) -> bool:
|
|
97
|
+
return isinstance(value, TupleImpl)
|
|
98
|
+
|
|
92
99
|
@classmethod
|
|
93
100
|
def _accepts_(cls, value: Any) -> bool:
|
|
94
101
|
return isinstance(value, cls | tuple)
|
sonolus/script/interval.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from typing import Self
|
|
2
2
|
|
|
3
3
|
from sonolus.backend.ops import Op
|
|
4
|
+
from sonolus.script.array_like import ArrayLike
|
|
4
5
|
from sonolus.script.debug import static_error
|
|
5
6
|
from sonolus.script.internal.native import native_function
|
|
7
|
+
from sonolus.script.internal.range import range_or_tuple
|
|
6
8
|
from sonolus.script.num import Num
|
|
7
9
|
from sonolus.script.record import Record
|
|
8
10
|
|
|
@@ -296,7 +298,7 @@ def unlerp_clamped(a: float, b: float, x: float, /) -> float:
|
|
|
296
298
|
|
|
297
299
|
@native_function(Op.Remap)
|
|
298
300
|
def remap(a: float, b: float, c: float, d: float, x: float, /) -> float:
|
|
299
|
-
"""
|
|
301
|
+
"""Linearly remap a value from one interval to another.
|
|
300
302
|
|
|
301
303
|
Args:
|
|
302
304
|
a: The start of the input interval.
|
|
@@ -313,7 +315,7 @@ def remap(a: float, b: float, c: float, d: float, x: float, /) -> float:
|
|
|
313
315
|
|
|
314
316
|
@native_function(Op.RemapClamped)
|
|
315
317
|
def remap_clamped(a: float, b: float, c: float, d: float, x: float, /) -> float:
|
|
316
|
-
"""
|
|
318
|
+
"""Linearly remap a value from one interval to another, clamped to the output interval.
|
|
317
319
|
|
|
318
320
|
Args:
|
|
319
321
|
a: The start of the input interval.
|
|
@@ -341,3 +343,59 @@ def clamp(x: float, a: float, b: float, /) -> float:
|
|
|
341
343
|
The clamped value.
|
|
342
344
|
"""
|
|
343
345
|
return max(a, min(b, x))
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def interp(
|
|
349
|
+
xp: ArrayLike[float] | tuple[float, ...],
|
|
350
|
+
fp: ArrayLike[float] | tuple[float, ...],
|
|
351
|
+
x: float,
|
|
352
|
+
) -> float:
|
|
353
|
+
"""Linearly interpolate a value within a sequence of points.
|
|
354
|
+
|
|
355
|
+
The sequence must have at least 2 elements and be sorted in increasing order of x-coordinates.
|
|
356
|
+
For values of x outside the range of xp, the slope of the first or last segment is used to extrapolate.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
xp: The x-coordinates of the points in increasing order.
|
|
360
|
+
fp: The y-coordinates of the points.
|
|
361
|
+
x: The x-coordinate to interpolate.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
The interpolated value.
|
|
365
|
+
"""
|
|
366
|
+
assert len(xp) == len(fp)
|
|
367
|
+
assert len(xp) >= 2
|
|
368
|
+
for i in range_or_tuple(1, len(xp) - 1):
|
|
369
|
+
# At i == 1, x may be less than x[0], but since we're extrapolating, we use the first segment regardless.
|
|
370
|
+
if x <= xp[i]:
|
|
371
|
+
return remap(xp[i - 1], xp[i], fp[i - 1], fp[i], x)
|
|
372
|
+
# x > xp[-2] so we can just use the last segment regardless of whether x is in it or to the right of it.
|
|
373
|
+
return remap(xp[-2], xp[-1], fp[-2], fp[-1], x)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def interp_clamped(
|
|
377
|
+
xp: ArrayLike[float] | tuple[float, ...],
|
|
378
|
+
fp: ArrayLike[float] | tuple[float, ...],
|
|
379
|
+
x: float,
|
|
380
|
+
):
|
|
381
|
+
"""Linearly interpolate a value within a sequence of points.
|
|
382
|
+
|
|
383
|
+
The sequence must have at least 2 elements and be sorted in increasing order of x-coordinates.
|
|
384
|
+
For x-coordinates outside the range of the sequence, the respective endpoint of fp is returned.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
xp: The x-coordinates of the points in increasing order.
|
|
388
|
+
fp: The y-coordinates of the points.
|
|
389
|
+
x: The x-coordinate to interpolate.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
The interpolated value.
|
|
393
|
+
"""
|
|
394
|
+
assert len(xp) == len(fp)
|
|
395
|
+
assert len(xp) >= 2
|
|
396
|
+
if x <= xp[0]:
|
|
397
|
+
return fp[0]
|
|
398
|
+
for i in range_or_tuple(1, len(xp)):
|
|
399
|
+
if x <= xp[i]:
|
|
400
|
+
return remap(xp[i - 1], xp[i], fp[i - 1], fp[i], x)
|
|
401
|
+
return fp[-1]
|
sonolus/script/iterator.py
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from abc import abstractmethod
|
|
4
|
-
from collections.abc import Iterator
|
|
5
4
|
from typing import Any
|
|
6
5
|
|
|
7
6
|
from sonolus.script.internal.impl import meta_fn
|
|
8
7
|
from sonolus.script.record import Record
|
|
9
8
|
|
|
10
9
|
|
|
11
|
-
class SonolusIterator[T]
|
|
10
|
+
class SonolusIterator[T]:
|
|
12
11
|
"""Base class for Sonolus iterators.
|
|
13
12
|
|
|
14
13
|
This class is used to define custom iterators that can be used in Sonolus.py.
|
|
@@ -30,6 +29,8 @@ class SonolusIterator[T](Iterator[T]):
|
|
|
30
29
|
```
|
|
31
30
|
"""
|
|
32
31
|
|
|
32
|
+
_allow_instance_check_ = True
|
|
33
|
+
|
|
33
34
|
def next(self) -> T:
|
|
34
35
|
result = self.get()
|
|
35
36
|
self.advance()
|
sonolus/script/num.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# ruff: noqa: N801
|
|
2
1
|
from __future__ import annotations
|
|
3
2
|
|
|
4
3
|
import operator
|
|
@@ -81,12 +80,22 @@ class _Num(Value, metaclass=_NumMeta):
|
|
|
81
80
|
return value
|
|
82
81
|
return cls(value)
|
|
83
82
|
|
|
83
|
+
def _is_rom_constant(self) -> bool:
|
|
84
|
+
return (
|
|
85
|
+
ctx()
|
|
86
|
+
and isinstance(self.data, BlockPlace)
|
|
87
|
+
and self.data.block == ctx().blocks.EngineRom
|
|
88
|
+
and isinstance(self.data.index, int)
|
|
89
|
+
)
|
|
90
|
+
|
|
84
91
|
def _is_py_(self) -> bool:
|
|
85
|
-
return isinstance(self.data, float | int | bool)
|
|
92
|
+
return isinstance(self.data, float | int | bool) or self._is_rom_constant()
|
|
86
93
|
|
|
87
94
|
def _as_py_(self) -> Any:
|
|
88
95
|
if not self._is_py_():
|
|
89
96
|
raise ValueError("Not a compile time constant Num")
|
|
97
|
+
if self._is_rom_constant():
|
|
98
|
+
return ctx().rom.get_value(self.data.index + self.data.offset)
|
|
90
99
|
if self.data.is_integer():
|
|
91
100
|
return int(self.data)
|
|
92
101
|
return self.data
|
sonolus/script/options.py
CHANGED
|
@@ -5,11 +5,13 @@ from typing import Annotated, Any, NewType, dataclass_transform, get_origin
|
|
|
5
5
|
from sonolus.backend.mode import Mode
|
|
6
6
|
from sonolus.backend.place import BlockPlace
|
|
7
7
|
from sonolus.script.debug import assert_unreachable
|
|
8
|
-
from sonolus.script.internal.context import ctx
|
|
8
|
+
from sonolus.script.internal.context import ctx, debug_config
|
|
9
9
|
from sonolus.script.internal.descriptor import SonolusDescriptor
|
|
10
10
|
from sonolus.script.internal.generic import validate_concrete_type
|
|
11
11
|
from sonolus.script.internal.introspection import get_field_specifiers
|
|
12
|
+
from sonolus.script.internal.simulation_context import sim_ctx
|
|
12
13
|
from sonolus.script.num import Num
|
|
14
|
+
from sonolus.script.values import copy
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
@dataclass
|
|
@@ -186,6 +188,8 @@ class _OptionField(SonolusDescriptor):
|
|
|
186
188
|
self.index = index
|
|
187
189
|
|
|
188
190
|
def __get__(self, instance, owner):
|
|
191
|
+
if sim_ctx():
|
|
192
|
+
return sim_ctx().get_or_put_value((instance, self), lambda: copy(self.info.default))
|
|
189
193
|
if ctx():
|
|
190
194
|
match ctx().global_state.mode:
|
|
191
195
|
case Mode.PLAY:
|
|
@@ -202,8 +206,27 @@ class _OptionField(SonolusDescriptor):
|
|
|
202
206
|
return Num._from_place_(BlockPlace(block, self.index))
|
|
203
207
|
else:
|
|
204
208
|
return Num._accept_(self.info.default)
|
|
209
|
+
raise RuntimeError("Options can only be accessed in a context")
|
|
205
210
|
|
|
206
211
|
def __set__(self, instance, value):
|
|
212
|
+
if sim_ctx():
|
|
213
|
+
return sim_ctx().set_or_put_value((instance, self), lambda: copy(self.info.default), value)
|
|
214
|
+
if ctx() and debug_config().unchecked_writes:
|
|
215
|
+
match ctx().global_state.mode:
|
|
216
|
+
case Mode.PLAY:
|
|
217
|
+
block = ctx().blocks.LevelOption
|
|
218
|
+
case Mode.WATCH:
|
|
219
|
+
block = ctx().blocks.LevelOption
|
|
220
|
+
case Mode.PREVIEW:
|
|
221
|
+
block = ctx().blocks.PreviewOption
|
|
222
|
+
case Mode.TUTORIAL:
|
|
223
|
+
block = None
|
|
224
|
+
case _:
|
|
225
|
+
assert_unreachable()
|
|
226
|
+
if block is not None:
|
|
227
|
+
Num._from_place_(BlockPlace(block, self.index))._set_(Num._accept_(value))
|
|
228
|
+
else:
|
|
229
|
+
raise RuntimeError("Options in the current mode cannot be set and use the default value")
|
|
207
230
|
raise AttributeError("Options are read-only")
|
|
208
231
|
|
|
209
232
|
|
sonolus/script/quad.py
CHANGED
|
@@ -28,6 +28,20 @@ class Quad(Record):
|
|
|
28
28
|
br: Vec2
|
|
29
29
|
"""The bottom-right corner of the quad."""
|
|
30
30
|
|
|
31
|
+
@classmethod
|
|
32
|
+
def zero(cls) -> Quad:
|
|
33
|
+
"""Return a quad with all corners set to (0, 0).
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
A new quad with all corners at the origin.
|
|
37
|
+
"""
|
|
38
|
+
return cls(
|
|
39
|
+
bl=Vec2.zero(),
|
|
40
|
+
tl=Vec2.zero(),
|
|
41
|
+
tr=Vec2.zero(),
|
|
42
|
+
br=Vec2.zero(),
|
|
43
|
+
)
|
|
44
|
+
|
|
31
45
|
@classmethod
|
|
32
46
|
def from_quad(cls, value: QuadLike, /) -> Quad:
|
|
33
47
|
"""Create a quad from a quad-like value."""
|
|
@@ -80,7 +94,14 @@ class Quad(Record):
|
|
|
80
94
|
).translate(self.center * (Vec2(1, 1) - factor))
|
|
81
95
|
|
|
82
96
|
def rotate(self, angle: float, /) -> Self:
|
|
83
|
-
"""Rotate the quad by the given angle about the origin and return a new quad.
|
|
97
|
+
"""Rotate the quad by the given angle about the origin and return a new quad.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
angle: The angle of rotation in radians. Positive angles rotate counterclockwise.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
A new quad rotated by the given angle.
|
|
104
|
+
"""
|
|
84
105
|
return Quad(
|
|
85
106
|
bl=self.bl.rotate(angle),
|
|
86
107
|
tl=self.tl.rotate(angle),
|
|
@@ -94,7 +115,15 @@ class Quad(Record):
|
|
|
94
115
|
/,
|
|
95
116
|
pivot: Vec2,
|
|
96
117
|
) -> Self:
|
|
97
|
-
"""Rotate the quad by the given angle about the given pivot and return a new quad.
|
|
118
|
+
"""Rotate the quad by the given angle about the given pivot and return a new quad.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
angle: The angle of rotation in radians. Positive angles rotate counterclockwise.
|
|
122
|
+
pivot: The pivot point for rotation.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
A new quad rotated about the pivot by the given angle.
|
|
126
|
+
"""
|
|
98
127
|
return Quad(
|
|
99
128
|
bl=self.bl.rotate_about(angle, pivot),
|
|
100
129
|
tl=self.tl.rotate_about(angle, pivot),
|
|
@@ -103,7 +132,14 @@ class Quad(Record):
|
|
|
103
132
|
)
|
|
104
133
|
|
|
105
134
|
def rotate_centered(self, angle: float, /) -> Self:
|
|
106
|
-
"""Rotate the quad by the given angle about its center and return a new quad.
|
|
135
|
+
"""Rotate the quad by the given angle about its center and return a new quad.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
angle: The angle of rotation in radians. Positive angles rotate counterclockwise.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
A new quad rotated about its center by the given angle.
|
|
142
|
+
"""
|
|
107
143
|
return self.rotate_about(angle, self.center)
|
|
108
144
|
|
|
109
145
|
def permute(self, count: int = 1, /) -> Self:
|
|
@@ -147,6 +183,8 @@ class Quad(Record):
|
|
|
147
183
|
def contains_point(self, point: Vec2, /) -> bool:
|
|
148
184
|
"""Check if the quad contains the given point.
|
|
149
185
|
|
|
186
|
+
It is not guaranteed whether points on the edges of the quad are considered inside or outside.
|
|
187
|
+
|
|
150
188
|
Args:
|
|
151
189
|
point: The point to check.
|
|
152
190
|
|
sonolus/script/record.py
CHANGED
|
@@ -20,8 +20,15 @@ from sonolus.script.internal.value import BackingSource, DataValue, Value
|
|
|
20
20
|
from sonolus.script.num import Num
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
class RecordMeta(type):
|
|
24
|
+
@meta_fn
|
|
25
|
+
def __pos__[T](cls: type[T]) -> T:
|
|
26
|
+
"""Create a zero-initialized record instance."""
|
|
27
|
+
return cls._zero_()
|
|
28
|
+
|
|
29
|
+
|
|
23
30
|
@dataclass_transform(eq_default=True)
|
|
24
|
-
class Record(GenericValue):
|
|
31
|
+
class Record(GenericValue, metaclass=RecordMeta):
|
|
25
32
|
"""Base class for user-defined data structures.
|
|
26
33
|
|
|
27
34
|
Usage:
|
|
@@ -38,6 +45,15 @@ class Record(GenericValue):
|
|
|
38
45
|
field1: T
|
|
39
46
|
field2: U
|
|
40
47
|
```
|
|
48
|
+
|
|
49
|
+
Creating an instance:
|
|
50
|
+
```python
|
|
51
|
+
record = MyRecord(field1=42, field2=True)
|
|
52
|
+
record_2 = MyGenericRecord[int, int](field1=42, field2=100)
|
|
53
|
+
record_3 = MyGenericRecord(field1=42, field2=100) # Type arguments can be inferred
|
|
54
|
+
record_4 = +MyRecord # Create a zero-initialized record
|
|
55
|
+
record_5 = +MyGenericRecord[int, int]
|
|
56
|
+
```
|
|
41
57
|
"""
|
|
42
58
|
|
|
43
59
|
_value: dict[str, Value]
|
|
@@ -239,12 +255,12 @@ class Record(GenericValue):
|
|
|
239
255
|
|
|
240
256
|
def __str__(self):
|
|
241
257
|
return (
|
|
242
|
-
f"{self.__class__.__name__}({
|
|
258
|
+
f"{self.__class__.__name__}({', '.join(f'{field.name}={field.__get__(self)}' for field in self._fields)})"
|
|
243
259
|
)
|
|
244
260
|
|
|
245
261
|
def __repr__(self):
|
|
246
262
|
return (
|
|
247
|
-
f"{self.__class__.__name__}({
|
|
263
|
+
f"{self.__class__.__name__}({', '.join(f'{field.name}={field.__get__(self)!r}' for field in self._fields)})"
|
|
248
264
|
)
|
|
249
265
|
|
|
250
266
|
@meta_fn
|
|
@@ -268,6 +284,11 @@ class Record(GenericValue):
|
|
|
268
284
|
def __hash__(self):
|
|
269
285
|
return hash(tuple(field.__get__(self) for field in self._fields))
|
|
270
286
|
|
|
287
|
+
@meta_fn
|
|
288
|
+
def __pos__(self) -> Self:
|
|
289
|
+
"""Return a copy of the record."""
|
|
290
|
+
return self._copy_()
|
|
291
|
+
|
|
271
292
|
@classmethod
|
|
272
293
|
@meta_fn
|
|
273
294
|
def type_var_value(cls, var: TypeVar, /) -> Any:
|