sonolus.py 0.10.7__py3-none-any.whl → 0.12.5__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 +38 -32
- sonolus/backend/interpret.py +3 -3
- sonolus/backend/node.py +6 -28
- sonolus/backend/optimize/liveness.py +5 -2
- sonolus/backend/visitor.py +5 -2
- sonolus/build/cli.py +18 -0
- sonolus/build/compile.py +17 -3
- sonolus/build/dev_server.py +9 -1
- sonolus/build/node.py +6 -8
- sonolus/script/archetype.py +183 -45
- sonolus/script/bucket.py +2 -2
- sonolus/script/containers.py +51 -0
- sonolus/script/debug.py +20 -3
- sonolus/script/engine.py +48 -0
- sonolus/script/internal/builtin_impls.py +91 -4
- sonolus/script/internal/context.py +51 -6
- sonolus/script/internal/generic.py +34 -7
- sonolus/script/internal/impl.py +11 -5
- sonolus/script/internal/introspection.py +10 -1
- sonolus/script/internal/tuple_impl.py +6 -0
- sonolus/script/interval.py +5 -5
- sonolus/script/project.py +2 -0
- sonolus/script/record.py +36 -21
- sonolus/script/runtime.py +1 -1
- sonolus/script/stream.py +28 -21
- sonolus/script/transform.py +9 -8
- sonolus/script/values.py +1 -6
- sonolus/script/vec.py +27 -14
- {sonolus_py-0.10.7.dist-info → sonolus_py-0.12.5.dist-info}/METADATA +2 -2
- {sonolus_py-0.10.7.dist-info → sonolus_py-0.12.5.dist-info}/RECORD +33 -33
- {sonolus_py-0.10.7.dist-info → sonolus_py-0.12.5.dist-info}/WHEEL +0 -0
- {sonolus_py-0.10.7.dist-info → sonolus_py-0.12.5.dist-info}/entry_points.txt +0 -0
- {sonolus_py-0.10.7.dist-info → sonolus_py-0.12.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from types import FunctionType
|
|
2
|
+
from typing import Any, Never, assert_never
|
|
2
3
|
|
|
3
4
|
from sonolus.backend.ops import Op
|
|
4
5
|
from sonolus.script.array import Array
|
|
@@ -22,6 +23,7 @@ from sonolus.script.iterator import (
|
|
|
22
23
|
_Zipper,
|
|
23
24
|
)
|
|
24
25
|
from sonolus.script.num import Num, _is_num
|
|
26
|
+
from sonolus.script.record import Record
|
|
25
27
|
|
|
26
28
|
_empty = object()
|
|
27
29
|
|
|
@@ -389,8 +391,90 @@ def _super(*args):
|
|
|
389
391
|
|
|
390
392
|
|
|
391
393
|
@meta_fn
|
|
392
|
-
def
|
|
393
|
-
|
|
394
|
+
def _hasattr(obj: Any, name: str) -> bool:
|
|
395
|
+
from sonolus.script.internal.constant import ConstantValue
|
|
396
|
+
from sonolus.script.internal.descriptor import SonolusDescriptor
|
|
397
|
+
|
|
398
|
+
name = validate_value(name)._as_py_()
|
|
399
|
+
if isinstance(obj, ConstantValue):
|
|
400
|
+
# Unwrap so we can access fields
|
|
401
|
+
obj = obj._as_py_()
|
|
402
|
+
descriptor = None
|
|
403
|
+
for cls in type.mro(type(obj)):
|
|
404
|
+
descriptor = cls.__dict__.get(name, None)
|
|
405
|
+
if descriptor is not None:
|
|
406
|
+
break
|
|
407
|
+
# We want to mirror what getattr supports and fail fast if a future getattr would fail.
|
|
408
|
+
match descriptor:
|
|
409
|
+
case None:
|
|
410
|
+
return hasattr(obj, name)
|
|
411
|
+
case property() | SonolusDescriptor() | FunctionType() | classmethod() | staticmethod():
|
|
412
|
+
return True
|
|
413
|
+
case non_descriptor if not hasattr(non_descriptor, "__get__"):
|
|
414
|
+
return True
|
|
415
|
+
case _:
|
|
416
|
+
raise TypeError(f"Unsupported field or descriptor {name}")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@meta_fn
|
|
420
|
+
def _getattr(obj: Any, name: str, default=_empty) -> Any:
|
|
421
|
+
from sonolus.backend.visitor import compile_and_call
|
|
422
|
+
from sonolus.script.internal.constant import ConstantValue
|
|
423
|
+
from sonolus.script.internal.descriptor import SonolusDescriptor
|
|
424
|
+
|
|
425
|
+
name = validate_value(name)._as_py_()
|
|
426
|
+
if isinstance(obj, ConstantValue):
|
|
427
|
+
# Unwrap so we can access fields
|
|
428
|
+
obj = obj._as_py_()
|
|
429
|
+
descriptor = None
|
|
430
|
+
for cls in type.mro(type(obj)):
|
|
431
|
+
descriptor = cls.__dict__.get(name, None)
|
|
432
|
+
if descriptor is not None:
|
|
433
|
+
break
|
|
434
|
+
match descriptor:
|
|
435
|
+
case property(fget=getter):
|
|
436
|
+
return compile_and_call(getter, obj)
|
|
437
|
+
case SonolusDescriptor() | FunctionType() | classmethod() | staticmethod() | None:
|
|
438
|
+
return validate_value(getattr(obj, name) if default is _empty else getattr(obj, name, default))
|
|
439
|
+
case non_descriptor if not hasattr(non_descriptor, "__get__"):
|
|
440
|
+
return validate_value(getattr(obj, name) if default is _empty else getattr(obj, name, default))
|
|
441
|
+
case _:
|
|
442
|
+
raise TypeError(f"Unsupported field or descriptor {name}")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@meta_fn
|
|
446
|
+
def _setattr(obj: Any, name: str, value: Any):
|
|
447
|
+
from sonolus.backend.visitor import compile_and_call
|
|
448
|
+
from sonolus.script.internal.descriptor import SonolusDescriptor
|
|
449
|
+
|
|
450
|
+
name = validate_value(name)._as_py_()
|
|
451
|
+
if obj._is_py_():
|
|
452
|
+
obj = obj._as_py_()
|
|
453
|
+
descriptor = getattr(type(obj), name, None)
|
|
454
|
+
match descriptor:
|
|
455
|
+
case property(fset=setter):
|
|
456
|
+
if setter is None:
|
|
457
|
+
raise AttributeError(f"Cannot set attribute {name} because property has no setter")
|
|
458
|
+
compile_and_call(setter, obj, value)
|
|
459
|
+
case SonolusDescriptor():
|
|
460
|
+
setattr(obj, name, value)
|
|
461
|
+
case _:
|
|
462
|
+
raise TypeError(f"Unsupported field or descriptor {name}")
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class _Type(Record):
|
|
466
|
+
@meta_fn
|
|
467
|
+
def __call__(self, value, /):
|
|
468
|
+
value = validate_value(value)
|
|
469
|
+
if value._is_py_():
|
|
470
|
+
value = value._as_py_()
|
|
471
|
+
return validate_value(type(value))
|
|
472
|
+
|
|
473
|
+
def __getitem__(self, item):
|
|
474
|
+
return self
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
_type = _Type()
|
|
394
478
|
|
|
395
479
|
|
|
396
480
|
@meta_fn
|
|
@@ -404,12 +488,13 @@ BUILTIN_IMPLS = {
|
|
|
404
488
|
id(abs): _abs,
|
|
405
489
|
id(all): _all,
|
|
406
490
|
id(any): _any,
|
|
407
|
-
id(sum): _sum,
|
|
408
491
|
id(bool): _bool,
|
|
409
492
|
id(callable): _callable,
|
|
410
493
|
id(enumerate): _enumerate,
|
|
411
494
|
id(filter): _filter,
|
|
412
495
|
id(float): _float,
|
|
496
|
+
id(getattr): _getattr,
|
|
497
|
+
id(hasattr): _hasattr,
|
|
413
498
|
id(int): _int,
|
|
414
499
|
id(isinstance): _isinstance,
|
|
415
500
|
id(iter): _iter,
|
|
@@ -420,6 +505,8 @@ BUILTIN_IMPLS = {
|
|
|
420
505
|
id(next): _next,
|
|
421
506
|
id(range): Range,
|
|
422
507
|
id(reversed): _reversed,
|
|
508
|
+
id(setattr): _setattr,
|
|
509
|
+
id(sum): _sum,
|
|
423
510
|
id(super): _super,
|
|
424
511
|
id(type): _type,
|
|
425
512
|
id(zip): _zip,
|
|
@@ -94,25 +94,36 @@ class ProjectContextState:
|
|
|
94
94
|
|
|
95
95
|
class ModeContextState:
|
|
96
96
|
archetypes: dict[type, int]
|
|
97
|
+
compile_time_only_archetypes: set[type]
|
|
97
98
|
archetypes_by_name: dict[str, type]
|
|
98
99
|
keys_by_archetype_id: Sequence[int]
|
|
99
100
|
is_scored_by_archetype_id: Sequence[bool]
|
|
101
|
+
archetype_mro_id_array_rom_indexes: Sequence[int] | None = None
|
|
100
102
|
environment_mappings: dict[_GlobalInfo, int]
|
|
101
103
|
environment_offsets: dict[Block, int]
|
|
102
104
|
mode: Mode
|
|
103
105
|
lock: Lock
|
|
104
106
|
|
|
105
|
-
def __init__(self, mode: Mode, archetypes:
|
|
107
|
+
def __init__(self, mode: Mode, archetypes: list[type] | None = None):
|
|
106
108
|
from sonolus.script.array import Array
|
|
107
109
|
|
|
108
|
-
|
|
110
|
+
archetypes = [*archetypes] if archetypes is not None else []
|
|
111
|
+
seen_archetypes = {*archetypes}
|
|
112
|
+
compile_time_only_archetypes = set()
|
|
113
|
+
for type_ in [*archetypes]:
|
|
114
|
+
for entry in type_.mro():
|
|
115
|
+
if getattr(entry, "_is_concrete_archetype_", False) and entry not in seen_archetypes:
|
|
116
|
+
archetypes.append(entry)
|
|
117
|
+
seen_archetypes.add(entry)
|
|
118
|
+
compile_time_only_archetypes.add(entry)
|
|
119
|
+
self.archetypes = {type_: idx for idx, type_ in enumerate(archetypes)}
|
|
120
|
+
self.compile_time_only_archetypes = compile_time_only_archetypes
|
|
109
121
|
self.archetypes_by_name = {type_.name: type_ for type_, _ in self.archetypes.items()} # type: ignore
|
|
110
|
-
ordered_archetypes = sorted(self.archetypes, key=lambda a: self.archetypes[a])
|
|
111
122
|
self.keys_by_archetype_id = (
|
|
112
|
-
Array(*((getattr(a, "_key_", -1)) for a in
|
|
123
|
+
Array(*((getattr(a, "_key_", -1)) for a in archetypes)) if archetypes else Array[int, Literal[0]]()
|
|
113
124
|
)
|
|
114
125
|
self.is_scored_by_archetype_id = (
|
|
115
|
-
Array(*((getattr(a, "_is_scored_", False)) for a in
|
|
126
|
+
Array(*((getattr(a, "_is_scored_", False)) for a in archetypes))
|
|
116
127
|
if archetypes
|
|
117
128
|
else Array[bool, Literal[0]]()
|
|
118
129
|
)
|
|
@@ -121,6 +132,27 @@ class ModeContextState:
|
|
|
121
132
|
self.mode = mode
|
|
122
133
|
self.lock = Lock()
|
|
123
134
|
|
|
135
|
+
def _init_archetype_mro_info(self, rom: ReadOnlyMemory):
|
|
136
|
+
from sonolus.script.array import Array
|
|
137
|
+
from sonolus.script.num import Num
|
|
138
|
+
|
|
139
|
+
with self.lock:
|
|
140
|
+
if self.archetype_mro_id_array_rom_indexes is not None:
|
|
141
|
+
return
|
|
142
|
+
archetype_mro_id_values = []
|
|
143
|
+
archetype_mro_id_offsets = []
|
|
144
|
+
for type_ in self.archetypes:
|
|
145
|
+
mro_ids = [self.archetypes[entry] for entry in type_.mro() if entry in self.archetypes]
|
|
146
|
+
archetype_mro_id_offsets.append(len(archetype_mro_id_values))
|
|
147
|
+
archetype_mro_id_values.append(len(mro_ids))
|
|
148
|
+
archetype_mro_id_values.extend(mro_ids)
|
|
149
|
+
archetype_mro_id_array_place = rom[tuple(archetype_mro_id_values)]
|
|
150
|
+
|
|
151
|
+
archetype_mro_id_rom_indexes = Array[int, len(archetype_mro_id_offsets)]._with_value(
|
|
152
|
+
[Num._accept_(offset + archetype_mro_id_array_place.index) for offset in archetype_mro_id_offsets]
|
|
153
|
+
)
|
|
154
|
+
self.archetype_mro_id_array_rom_indexes = archetype_mro_id_rom_indexes
|
|
155
|
+
|
|
124
156
|
|
|
125
157
|
class CallbackContextState:
|
|
126
158
|
callback: str
|
|
@@ -371,6 +403,15 @@ class Context:
|
|
|
371
403
|
self.mode_state.archetypes[type_] = len(self.mode_state.archetypes)
|
|
372
404
|
return self.mode_state.archetypes[type_]
|
|
373
405
|
|
|
406
|
+
def get_archetype_mro_id_array(self, archetype_id: int) -> Sequence[int]:
|
|
407
|
+
from sonolus.script.containers import ArrayPointer
|
|
408
|
+
from sonolus.script.num import Num
|
|
409
|
+
from sonolus.script.pointer import _deref
|
|
410
|
+
|
|
411
|
+
self.mode_state._init_archetype_mro_info(self.rom)
|
|
412
|
+
rom_index = self.mode_state.archetype_mro_id_array_rom_indexes[archetype_id]
|
|
413
|
+
return ArrayPointer[int](_deref(self.blocks.EngineRom, rom_index, Num), self.blocks.EngineRom, rom_index + 1)
|
|
414
|
+
|
|
374
415
|
|
|
375
416
|
def ctx() -> Context | Any: # Using Any to silence type checker warnings if it's None
|
|
376
417
|
return context_var.get()
|
|
@@ -551,7 +592,11 @@ def context_to_cfg(context: Context) -> BasicBlock:
|
|
|
551
592
|
edge = FlowEdge(src=blocks[current], dst=blocks[target], cond=condition)
|
|
552
593
|
blocks[current].outgoing.add(edge)
|
|
553
594
|
blocks[target].incoming.add(edge)
|
|
554
|
-
|
|
595
|
+
result = blocks[context]
|
|
596
|
+
for current in tuple(iter_contexts(context)):
|
|
597
|
+
# Break cycles so memory can be cleaned without gc
|
|
598
|
+
del current.outgoing
|
|
599
|
+
return result
|
|
555
600
|
|
|
556
601
|
|
|
557
602
|
def unique[T](iterable: Iterable[T]) -> list[T]:
|
|
@@ -37,6 +37,11 @@ def validate_type_spec(spec: Any) -> PartialGeneric | TypeVar | type[Value]:
|
|
|
37
37
|
validated_args = {validate_type_arg(arg) for arg in args}
|
|
38
38
|
if len(validated_args) == 1:
|
|
39
39
|
return validated_args.pop()
|
|
40
|
+
|
|
41
|
+
from sonolus.script.archetype import _BaseArchetype
|
|
42
|
+
|
|
43
|
+
if isinstance(spec, type) and issubclass(spec, _BaseArchetype):
|
|
44
|
+
raise TypeError(f"Expected a concrete type, got {spec}. Did you mean to write EntityRef[{spec.__name__}]?")
|
|
40
45
|
raise TypeError(f"Invalid type spec: {spec}")
|
|
41
46
|
|
|
42
47
|
|
|
@@ -44,15 +49,18 @@ def validate_concrete_type(spec: Any) -> type[Value]:
|
|
|
44
49
|
spec = validate_type_spec(spec)
|
|
45
50
|
if isinstance(spec, type) and issubclass(spec, Value) and spec._is_concrete_():
|
|
46
51
|
return spec
|
|
52
|
+
if (
|
|
53
|
+
isinstance(spec, type)
|
|
54
|
+
and issubclass(spec, Value)
|
|
55
|
+
and not spec._is_concrete_()
|
|
56
|
+
and issubclass(spec, GenericValue)
|
|
57
|
+
):
|
|
58
|
+
raise TypeError(f"Expected a concrete type, got {spec}. Are there missing type arguments?")
|
|
59
|
+
if isinstance(spec, PartialGeneric):
|
|
60
|
+
raise TypeError(f"Expected a concrete type, got {spec}. Invalid use of a type parameter as a type argument?")
|
|
47
61
|
raise TypeError(f"Expected a concrete type, got {spec}")
|
|
48
62
|
|
|
49
63
|
|
|
50
|
-
def validate_type_args(args) -> tuple[Any, ...]:
|
|
51
|
-
if not isinstance(args, tuple):
|
|
52
|
-
args = (args,)
|
|
53
|
-
return tuple(validate_type_arg(arg) for arg in args)
|
|
54
|
-
|
|
55
|
-
|
|
56
64
|
def contains_incomplete_type(args) -> bool:
|
|
57
65
|
if not isinstance(args, tuple):
|
|
58
66
|
args = (args,)
|
|
@@ -102,7 +110,26 @@ class GenericValue(Value):
|
|
|
102
110
|
def __class_getitem__(cls, args: Any) -> type[Self]:
|
|
103
111
|
if cls._type_args_ is not None:
|
|
104
112
|
raise TypeError(f"Type {cls.__name__} is already parameterized")
|
|
105
|
-
|
|
113
|
+
if not isinstance(args, tuple):
|
|
114
|
+
args = (args,)
|
|
115
|
+
validated_args = []
|
|
116
|
+
for i, arg in enumerate(args):
|
|
117
|
+
if i < len(cls.__type_params__):
|
|
118
|
+
type_param = cls.__type_params__[i]
|
|
119
|
+
else:
|
|
120
|
+
type_param = None
|
|
121
|
+
try:
|
|
122
|
+
validated_args.append(validate_type_arg(arg))
|
|
123
|
+
except TypeError as e:
|
|
124
|
+
if type_param is not None:
|
|
125
|
+
type_param_body = ", ".join(str(tp) for tp in cls.__type_params__)
|
|
126
|
+
raise TypeError(
|
|
127
|
+
f"Invalid type argument for type parameter {type_param} of "
|
|
128
|
+
f"class {cls.__name__}[{type_param_body}]: {e}"
|
|
129
|
+
) from e
|
|
130
|
+
else:
|
|
131
|
+
raise TypeError(f"Invalid type argument at position {i} of class {cls.__name__}: {e}") from e
|
|
132
|
+
args = tuple(validated_args)
|
|
106
133
|
args = cls._validate_type_args_(args)
|
|
107
134
|
if contains_incomplete_type(args):
|
|
108
135
|
return PartialGeneric(cls, args)
|
sonolus/script/internal/impl.py
CHANGED
|
@@ -62,6 +62,12 @@ def try_validate_value(value: Any) -> Value | None:
|
|
|
62
62
|
except ImportError:
|
|
63
63
|
pass
|
|
64
64
|
|
|
65
|
+
if hasattr(value, "_init_") and callable(value._init_):
|
|
66
|
+
try:
|
|
67
|
+
value._init_()
|
|
68
|
+
except Exception as e:
|
|
69
|
+
raise RuntimeError(f"Error initializing value {value}: {e}") from e
|
|
70
|
+
|
|
65
71
|
match value:
|
|
66
72
|
case Value():
|
|
67
73
|
return value
|
|
@@ -77,6 +83,10 @@ def try_validate_value(value: Any) -> Value | None:
|
|
|
77
83
|
return TupleImpl._accept_(value)
|
|
78
84
|
case dict():
|
|
79
85
|
return DictImpl._accept_(value)
|
|
86
|
+
case set() | frozenset():
|
|
87
|
+
from sonolus.script.containers import FrozenNumSet
|
|
88
|
+
|
|
89
|
+
return FrozenNumSet.of(*value)
|
|
80
90
|
case (
|
|
81
91
|
PartialGeneric()
|
|
82
92
|
| TypeVar()
|
|
@@ -90,11 +100,7 @@ def try_validate_value(value: Any) -> Value | None:
|
|
|
90
100
|
| super()
|
|
91
101
|
):
|
|
92
102
|
return BasicConstantValue.of(value)
|
|
93
|
-
case special_form if value
|
|
94
|
-
Literal,
|
|
95
|
-
Annotated,
|
|
96
|
-
Union,
|
|
97
|
-
}:
|
|
103
|
+
case special_form if value == Literal or value == Annotated or value == Union: # noqa: PLR1714, SIM109
|
|
98
104
|
return TypingSpecialFormConstant.of(special_form)
|
|
99
105
|
case other_type if get_origin(value) in {Literal, Annotated, UnionType, tuple, type}:
|
|
100
106
|
return BasicConstantValue.of(other_type)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import inspect
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from collections.abc import Sequence
|
|
2
4
|
from typing import Annotated
|
|
3
5
|
|
|
4
6
|
_missing = object()
|
|
@@ -11,9 +13,15 @@ def get_field_specifiers(
|
|
|
11
13
|
globals=None, # noqa: A002
|
|
12
14
|
locals=None, # noqa: A002
|
|
13
15
|
eval_str=True,
|
|
16
|
+
included_classes: Sequence[type] | None = None,
|
|
14
17
|
):
|
|
15
18
|
"""Like inspect.get_annotations, but also turns class attributes into Annotated."""
|
|
16
|
-
|
|
19
|
+
if included_classes is not None:
|
|
20
|
+
results = {}
|
|
21
|
+
for entry in reversed(included_classes):
|
|
22
|
+
results.update(inspect.get_annotations(entry, eval_str=eval_str))
|
|
23
|
+
else:
|
|
24
|
+
results = inspect.get_annotations(cls, globals=globals, locals=locals, eval_str=eval_str)
|
|
17
25
|
for key, value in results.items():
|
|
18
26
|
class_value = getattr(cls, key, _missing)
|
|
19
27
|
if class_value is not _missing and key not in skip:
|
|
@@ -26,6 +34,7 @@ def get_field_specifiers(
|
|
|
26
34
|
and not callable(value)
|
|
27
35
|
and not hasattr(value, "__func__")
|
|
28
36
|
and not isinstance(value, property)
|
|
37
|
+
and not (issubclass(cls, ABC) and (hasattr(ABC, key)))
|
|
29
38
|
):
|
|
30
39
|
raise ValueError(f"Missing annotation for {cls.__name__}.{key}")
|
|
31
40
|
for skipped_key in skip:
|
|
@@ -91,6 +91,12 @@ class TupleImpl(TransientValue):
|
|
|
91
91
|
other = TupleImpl._accept_(other)
|
|
92
92
|
return TupleImpl._accept_(self.value + other.value)
|
|
93
93
|
|
|
94
|
+
def __contains__(self, item):
|
|
95
|
+
for element in self.value: # noqa: SIM110
|
|
96
|
+
if element == item:
|
|
97
|
+
return True
|
|
98
|
+
return False
|
|
99
|
+
|
|
94
100
|
@staticmethod
|
|
95
101
|
@meta_fn
|
|
96
102
|
def _is_tuple_impl(value: Any) -> bool:
|
sonolus/script/interval.py
CHANGED
|
@@ -78,7 +78,7 @@ class Interval(Record):
|
|
|
78
78
|
Returns:
|
|
79
79
|
A new interval with the value added to both ends.
|
|
80
80
|
"""
|
|
81
|
-
return Interval.
|
|
81
|
+
return Interval._unchecked(start=self.start + other, end=self.end + other)
|
|
82
82
|
|
|
83
83
|
@perf_meta_fn
|
|
84
84
|
def __sub__(self, other: float | int) -> Interval:
|
|
@@ -90,7 +90,7 @@ class Interval(Record):
|
|
|
90
90
|
Returns:
|
|
91
91
|
A new interval with the value subtracted from both ends.
|
|
92
92
|
"""
|
|
93
|
-
return Interval.
|
|
93
|
+
return Interval._unchecked(start=self.start - other, end=self.end - other)
|
|
94
94
|
|
|
95
95
|
@perf_meta_fn
|
|
96
96
|
def __mul__(self, other: float | int) -> Interval:
|
|
@@ -102,7 +102,7 @@ class Interval(Record):
|
|
|
102
102
|
Returns:
|
|
103
103
|
A new interval with both ends multiplied by the value.
|
|
104
104
|
"""
|
|
105
|
-
return Interval.
|
|
105
|
+
return Interval._unchecked(start=self.start * other, end=self.end * other)
|
|
106
106
|
|
|
107
107
|
@perf_meta_fn
|
|
108
108
|
def __truediv__(self, other: float | int) -> Interval:
|
|
@@ -114,7 +114,7 @@ class Interval(Record):
|
|
|
114
114
|
Returns:
|
|
115
115
|
A new interval with both ends divided by the value.
|
|
116
116
|
"""
|
|
117
|
-
return Interval.
|
|
117
|
+
return Interval._unchecked(start=self.start / other, end=self.end / other)
|
|
118
118
|
|
|
119
119
|
@perf_meta_fn
|
|
120
120
|
def __floordiv__(self, other: float | int) -> Interval:
|
|
@@ -126,7 +126,7 @@ class Interval(Record):
|
|
|
126
126
|
Returns:
|
|
127
127
|
A new interval with both ends divided by the value and floored.
|
|
128
128
|
"""
|
|
129
|
-
return Interval.
|
|
129
|
+
return Interval._unchecked(start=self.start // other, end=self.end // other)
|
|
130
130
|
|
|
131
131
|
def __and__(self, other: Interval) -> Interval:
|
|
132
132
|
"""Get the intersection of two intervals.
|
sonolus/script/project.py
CHANGED
sonolus/script/record.py
CHANGED
|
@@ -4,7 +4,7 @@ import inspect
|
|
|
4
4
|
from abc import ABCMeta
|
|
5
5
|
from collections.abc import Iterable
|
|
6
6
|
from inspect import getmro
|
|
7
|
-
from typing import Any, ClassVar, Self, TypeVar, dataclass_transform, get_origin
|
|
7
|
+
from typing import Any, ClassVar, Protocol, Self, TypeVar, dataclass_transform, get_origin
|
|
8
8
|
|
|
9
9
|
from sonolus.backend.place import BlockPlace
|
|
10
10
|
from sonolus.script.internal.context import ctx
|
|
@@ -21,7 +21,8 @@ from sonolus.script.internal.value import BackingSource, DataValue, Value
|
|
|
21
21
|
from sonolus.script.num import Num
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
# Protocol metaclass is a subclass of ABCMeta, but let's make ABCMeta explicit for clarity
|
|
25
|
+
class RecordMeta(type(Protocol), ABCMeta):
|
|
25
26
|
@meta_fn
|
|
26
27
|
def __pos__[T](cls: type[T]) -> T:
|
|
27
28
|
"""Create a zero-initialized record instance."""
|
|
@@ -103,25 +104,28 @@ class Record(GenericValue, metaclass=RecordMeta):
|
|
|
103
104
|
index = 0
|
|
104
105
|
offset = 0
|
|
105
106
|
for name, hint in hints.items():
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
107
|
+
try:
|
|
108
|
+
if name not in cls.__annotations__:
|
|
109
|
+
continue
|
|
110
|
+
if hint is ClassVar or get_origin(hint) is ClassVar:
|
|
111
|
+
continue
|
|
112
|
+
if hasattr(cls, name):
|
|
113
|
+
raise TypeError("Default values are not supported for Record fields")
|
|
114
|
+
type_ = validate_type_spec(hint)
|
|
115
|
+
fields.append(_RecordField(name, type_, index, offset))
|
|
116
|
+
if isinstance(type_, type) and issubclass(type_, Value) and type_._is_concrete_():
|
|
117
|
+
offset += type_._size_()
|
|
118
|
+
setattr(cls, name, fields[-1])
|
|
119
|
+
index += 1
|
|
120
|
+
params.append(
|
|
121
|
+
inspect.Parameter(
|
|
122
|
+
name,
|
|
123
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
124
|
+
annotation=type_,
|
|
125
|
+
)
|
|
123
126
|
)
|
|
124
|
-
|
|
127
|
+
except Exception as e:
|
|
128
|
+
raise TypeError(f"Error processing field '{name}' of Record {cls.__name__}: {e}") from e
|
|
125
129
|
|
|
126
130
|
cls._parameterized_ = {}
|
|
127
131
|
cls._fields_ = fields
|
|
@@ -172,7 +176,8 @@ class Record(GenericValue, metaclass=RecordMeta):
|
|
|
172
176
|
return result
|
|
173
177
|
|
|
174
178
|
@classmethod
|
|
175
|
-
def
|
|
179
|
+
def _unchecked(cls, **kwargs) -> Self:
|
|
180
|
+
# Skips most validation, generally for internal use in frequently-called methods for performance reasons
|
|
176
181
|
result = object.__new__(cls)
|
|
177
182
|
for k, v in kwargs.items():
|
|
178
183
|
if isinstance(v, int | float):
|
|
@@ -319,6 +324,16 @@ class Record(GenericValue, metaclass=RecordMeta):
|
|
|
319
324
|
"""
|
|
320
325
|
return super().type_var_value(var)
|
|
321
326
|
|
|
327
|
+
@classmethod
|
|
328
|
+
def _validate_parameterized_(cls):
|
|
329
|
+
pass
|
|
330
|
+
|
|
331
|
+
@classmethod
|
|
332
|
+
def _get_parameterized(cls, args: tuple[Any, ...]) -> type[Self]:
|
|
333
|
+
result = super()._get_parameterized(args)
|
|
334
|
+
result._validate_parameterized_()
|
|
335
|
+
return result
|
|
336
|
+
|
|
322
337
|
|
|
323
338
|
class _RecordField(SonolusDescriptor):
|
|
324
339
|
def __init__(self, name: str, type_: type[Value] | Any, index: int, offset: int):
|
sonolus/script/runtime.py
CHANGED
|
@@ -1157,7 +1157,7 @@ def canvas() -> _PreviewRuntimeCanvas:
|
|
|
1157
1157
|
@perf_meta_fn
|
|
1158
1158
|
def screen() -> Rect:
|
|
1159
1159
|
"""Get the screen boundaries as a rectangle."""
|
|
1160
|
-
return Rect.
|
|
1160
|
+
return Rect._unchecked(t=1, r=aspect_ratio(), b=-1, l=-aspect_ratio())
|
|
1161
1161
|
|
|
1162
1162
|
|
|
1163
1163
|
def level_score() -> _LevelScore:
|
sonolus/script/stream.py
CHANGED
|
@@ -94,27 +94,34 @@ def streams[T](cls: type[T]) -> T:
|
|
|
94
94
|
"""
|
|
95
95
|
if len(cls.__bases__) != 1:
|
|
96
96
|
raise ValueError("Options class must not inherit from any class (except object)")
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def _init_(cls):
|
|
100
|
+
if getattr(cls, "_init_done_", False):
|
|
101
|
+
return
|
|
102
|
+
entries = []
|
|
103
|
+
# Offset 0 is unused so we can tell when a stream object is uninitialized since it'll have offset 0.
|
|
104
|
+
offset = 1
|
|
105
|
+
for name, annotation in get_field_specifiers(cls, skip={"_init_done_"}).items():
|
|
106
|
+
if issubclass(annotation, Stream | StreamGroup):
|
|
107
|
+
annotation = cast(type[Stream | StreamGroup], annotation)
|
|
108
|
+
if annotation is Stream or annotation is StreamGroup:
|
|
109
|
+
raise TypeError(f"Invalid annotation for streams: {annotation}. Must have type arguments.")
|
|
110
|
+
setattr(cls, name, _StreamField(offset, annotation))
|
|
111
|
+
# Streams store their data across several backing streams
|
|
112
|
+
entries.append((name, offset, annotation))
|
|
113
|
+
offset += annotation.backing_size()
|
|
114
|
+
elif issubclass(annotation, Value) and annotation._is_concrete_():
|
|
115
|
+
setattr(cls, name, _StreamDataField(offset, annotation))
|
|
116
|
+
# Data fields store their data in a single backing stream at different offsets in the same stream
|
|
117
|
+
entries.append((name, offset, annotation))
|
|
118
|
+
offset += 1
|
|
119
|
+
cls._streams_ = entries
|
|
120
|
+
cls._is_comptime_value_ = True
|
|
121
|
+
cls._init_done_ = True
|
|
122
|
+
|
|
123
|
+
cls._init_ = _init_
|
|
124
|
+
return cls()
|
|
118
125
|
|
|
119
126
|
|
|
120
127
|
@meta_fn
|
sonolus/script/transform.py
CHANGED
|
@@ -326,15 +326,16 @@ class Transform2d(Record):
|
|
|
326
326
|
A new normalized transform.
|
|
327
327
|
"""
|
|
328
328
|
assert self.a22 != 0, "Cannot normalize transform with a22 == 0"
|
|
329
|
+
a22 = self.a22 + (self.a22 == 0)
|
|
329
330
|
return Transform2d(
|
|
330
|
-
self.a00 /
|
|
331
|
-
self.a01 /
|
|
332
|
-
self.a02 /
|
|
333
|
-
self.a10 /
|
|
334
|
-
self.a11 /
|
|
335
|
-
self.a12 /
|
|
336
|
-
self.a20 /
|
|
337
|
-
self.a21 /
|
|
331
|
+
self.a00 / a22,
|
|
332
|
+
self.a01 / a22,
|
|
333
|
+
self.a02 / a22,
|
|
334
|
+
self.a10 / a22,
|
|
335
|
+
self.a11 / a22,
|
|
336
|
+
self.a12 / a22,
|
|
337
|
+
self.a20 / a22,
|
|
338
|
+
self.a21 / a22,
|
|
338
339
|
1,
|
|
339
340
|
)
|
|
340
341
|
|
sonolus/script/values.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from sonolus.script.internal.context import ctx
|
|
2
2
|
from sonolus.script.internal.generic import validate_concrete_type
|
|
3
3
|
from sonolus.script.internal.impl import meta_fn, validate_value
|
|
4
|
-
from sonolus.script.num import Num
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
@meta_fn
|
|
@@ -49,8 +48,4 @@ def swap[T](a: T, b: T):
|
|
|
49
48
|
@meta_fn
|
|
50
49
|
def sizeof(type_: type, /) -> int:
|
|
51
50
|
"""Return the size of the given type."""
|
|
52
|
-
|
|
53
|
-
if ctx():
|
|
54
|
-
return Num(type_._size_())
|
|
55
|
-
else:
|
|
56
|
-
return type_._size_()
|
|
51
|
+
return validate_concrete_type(type_)._size_()
|