sonolus.py 0.1.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/__init__.py +0 -0
- sonolus/backend/__init__.py +0 -0
- sonolus/backend/allocate.py +51 -0
- sonolus/backend/blocks.py +756 -0
- sonolus/backend/excepthook.py +37 -0
- sonolus/backend/finalize.py +69 -0
- sonolus/backend/flow.py +92 -0
- sonolus/backend/interpret.py +333 -0
- sonolus/backend/ir.py +89 -0
- sonolus/backend/mode.py +24 -0
- sonolus/backend/node.py +40 -0
- sonolus/backend/ops.py +197 -0
- sonolus/backend/optimize.py +9 -0
- sonolus/backend/passes.py +6 -0
- sonolus/backend/place.py +90 -0
- sonolus/backend/simplify.py +30 -0
- sonolus/backend/utils.py +48 -0
- sonolus/backend/visitor.py +880 -0
- sonolus/build/__init__.py +0 -0
- sonolus/build/cli.py +170 -0
- sonolus/build/collection.py +293 -0
- sonolus/build/compile.py +90 -0
- sonolus/build/defaults.py +32 -0
- sonolus/build/engine.py +149 -0
- sonolus/build/level.py +23 -0
- sonolus/build/node.py +43 -0
- sonolus/build/project.py +94 -0
- sonolus/py.typed +0 -0
- sonolus/script/__init__.py +0 -0
- sonolus/script/archetype.py +651 -0
- sonolus/script/array.py +241 -0
- sonolus/script/bucket.py +192 -0
- sonolus/script/callbacks.py +105 -0
- sonolus/script/comptime.py +146 -0
- sonolus/script/containers.py +247 -0
- sonolus/script/debug.py +70 -0
- sonolus/script/effect.py +132 -0
- sonolus/script/engine.py +101 -0
- sonolus/script/globals.py +234 -0
- sonolus/script/graphics.py +141 -0
- sonolus/script/icon.py +73 -0
- sonolus/script/internal/__init__.py +5 -0
- sonolus/script/internal/builtin_impls.py +144 -0
- sonolus/script/internal/context.py +365 -0
- sonolus/script/internal/descriptor.py +17 -0
- sonolus/script/internal/error.py +15 -0
- sonolus/script/internal/generic.py +197 -0
- sonolus/script/internal/impl.py +69 -0
- sonolus/script/internal/introspection.py +14 -0
- sonolus/script/internal/native.py +38 -0
- sonolus/script/internal/value.py +144 -0
- sonolus/script/interval.py +98 -0
- sonolus/script/iterator.py +211 -0
- sonolus/script/level.py +52 -0
- sonolus/script/math.py +92 -0
- sonolus/script/num.py +382 -0
- sonolus/script/options.py +194 -0
- sonolus/script/particle.py +158 -0
- sonolus/script/pointer.py +30 -0
- sonolus/script/project.py +17 -0
- sonolus/script/range.py +58 -0
- sonolus/script/record.py +293 -0
- sonolus/script/runtime.py +526 -0
- sonolus/script/sprite.py +332 -0
- sonolus/script/text.py +404 -0
- sonolus/script/timing.py +42 -0
- sonolus/script/transform.py +118 -0
- sonolus/script/ui.py +160 -0
- sonolus/script/values.py +43 -0
- sonolus/script/vec.py +48 -0
- sonolus_py-0.1.0.dist-info/METADATA +10 -0
- sonolus_py-0.1.0.dist-info/RECORD +75 -0
- sonolus_py-0.1.0.dist-info/WHEEL +4 -0
- sonolus_py-0.1.0.dist-info/entry_points.txt +2 -0
- sonolus_py-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Annotated, Any, Protocol, dataclass_transform, get_origin
|
|
5
|
+
|
|
6
|
+
from sonolus.backend.ops import Op
|
|
7
|
+
from sonolus.script.graphics import QuadLike, flatten_quad
|
|
8
|
+
from sonolus.script.internal.introspection import get_field_specifiers
|
|
9
|
+
from sonolus.script.internal.native import native_function
|
|
10
|
+
from sonolus.script.record import Record
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Particle(Record):
|
|
14
|
+
id: int
|
|
15
|
+
|
|
16
|
+
def is_available(self) -> bool:
|
|
17
|
+
return _has_particle_effect(self.id)
|
|
18
|
+
|
|
19
|
+
def spawn(self, quad: QuadLike, duration: float, loop: bool = False) -> ParticleHandle:
|
|
20
|
+
return ParticleHandle(_spawn_particle_effect(self.id, *flatten_quad(quad), duration, loop))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ParticleHandle(Record):
|
|
24
|
+
id: int
|
|
25
|
+
|
|
26
|
+
def move(self, quad: QuadLike) -> None:
|
|
27
|
+
_move_particle_effect(self.id, *flatten_quad(quad))
|
|
28
|
+
|
|
29
|
+
def destroy(self) -> None:
|
|
30
|
+
_destroy_particle_effect(self.id)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@native_function(Op.HasParticleEffect)
|
|
34
|
+
def _has_particle_effect(particle_id: int) -> bool:
|
|
35
|
+
raise NotImplementedError
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@native_function(Op.SpawnParticleEffect)
|
|
39
|
+
def _spawn_particle_effect(
|
|
40
|
+
particle_id: int,
|
|
41
|
+
x1: float,
|
|
42
|
+
y1: float,
|
|
43
|
+
x2: float,
|
|
44
|
+
y2: float,
|
|
45
|
+
x3: float,
|
|
46
|
+
y3: float,
|
|
47
|
+
x4: float,
|
|
48
|
+
y4: float,
|
|
49
|
+
duration: float,
|
|
50
|
+
loop: bool,
|
|
51
|
+
) -> int:
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@native_function(Op.MoveParticleEffect)
|
|
56
|
+
def _move_particle_effect(
|
|
57
|
+
handle: int, x1: float, y1: float, x2: float, y2: float, x3: float, y3: float, x4: float, y4: float
|
|
58
|
+
) -> None:
|
|
59
|
+
raise NotImplementedError
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@native_function(Op.DestroyParticleEffect)
|
|
63
|
+
def _destroy_particle_effect(handle: int) -> None:
|
|
64
|
+
raise NotImplementedError
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class ParticleInfo:
|
|
69
|
+
name: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def particle(name: str) -> Any:
|
|
73
|
+
return ParticleInfo(name)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Particles(Protocol):
|
|
77
|
+
_particles_: list[str]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass_transform()
|
|
81
|
+
def particles[T](cls: type[T]) -> T | Particles:
|
|
82
|
+
if len(cls.__bases__) != 1:
|
|
83
|
+
raise ValueError("Particles class must not inherit from any class (except object)")
|
|
84
|
+
instance = cls()
|
|
85
|
+
names = []
|
|
86
|
+
for i, (name, annotation) in enumerate(get_field_specifiers(cls).items()):
|
|
87
|
+
if get_origin(annotation) is not Annotated:
|
|
88
|
+
raise TypeError(f"Invalid annotation for particles: {annotation}")
|
|
89
|
+
annotation_type = annotation.__args__[0]
|
|
90
|
+
annotation_values = annotation.__metadata__
|
|
91
|
+
if annotation_type is not Particle:
|
|
92
|
+
raise TypeError(f"Invalid annotation for particles: {annotation}, expected annotation of type Particle")
|
|
93
|
+
if len(annotation_values) != 1 or not isinstance(annotation_values[0], ParticleInfo):
|
|
94
|
+
raise TypeError(
|
|
95
|
+
f"Invalid annotation for particles: {annotation}, expected a single string annotation value"
|
|
96
|
+
)
|
|
97
|
+
particle_name = annotation_values[0].name
|
|
98
|
+
names.append(particle_name)
|
|
99
|
+
setattr(instance, name, Particle(i))
|
|
100
|
+
instance._particles_ = names
|
|
101
|
+
instance._is_comptime_value_ = True
|
|
102
|
+
return instance
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class StandardParticle:
|
|
106
|
+
NoteCircularTapNeutral = Annotated[Particle, particle("#NOTE_CIRCULAR_TAP_NEUTRAL")]
|
|
107
|
+
NoteCircularTapRed = Annotated[Particle, particle("#NOTE_CIRCULAR_TAP_RED")]
|
|
108
|
+
NoteCircularTapGreen = Annotated[Particle, particle("#NOTE_CIRCULAR_TAP_GREEN")]
|
|
109
|
+
NoteCircularTapBlue = Annotated[Particle, particle("#NOTE_CIRCULAR_TAP_BLUE")]
|
|
110
|
+
NoteCircularTapYellow = Annotated[Particle, particle("#NOTE_CIRCULAR_TAP_YELLOW")]
|
|
111
|
+
NoteCircularTapPurple = Annotated[Particle, particle("#NOTE_CIRCULAR_TAP_PURPLE")]
|
|
112
|
+
NoteCircularTapCyan = Annotated[Particle, particle("#NOTE_CIRCULAR_TAP_CYAN")]
|
|
113
|
+
NoteCircularAlternativeNeutral = Annotated[Particle, particle("#NOTE_CIRCULAR_ALTERNATIVE_NEUTRAL")]
|
|
114
|
+
NoteCircularAlternativeRed = Annotated[Particle, particle("#NOTE_CIRCULAR_ALTERNATIVE_RED")]
|
|
115
|
+
NoteCircularAlternativeGreen = Annotated[Particle, particle("#NOTE_CIRCULAR_ALTERNATIVE_GREEN")]
|
|
116
|
+
NoteCircularAlternativeBlue = Annotated[Particle, particle("#NOTE_CIRCULAR_ALTERNATIVE_BLUE")]
|
|
117
|
+
NoteCircularAlternativeYellow = Annotated[Particle, particle("#NOTE_CIRCULAR_ALTERNATIVE_YELLOW")]
|
|
118
|
+
NoteCircularAlternativePurple = Annotated[Particle, particle("#NOTE_CIRCULAR_ALTERNATIVE_PURPLE")]
|
|
119
|
+
NoteCircularAlternativeCyan = Annotated[Particle, particle("#NOTE_CIRCULAR_ALTERNATIVE_CYAN")]
|
|
120
|
+
NoteCircularHoldNeutral = Annotated[Particle, particle("#NOTE_CIRCULAR_HOLD_NEUTRAL")]
|
|
121
|
+
NoteCircularHoldRed = Annotated[Particle, particle("#NOTE_CIRCULAR_HOLD_RED")]
|
|
122
|
+
NoteCircularHoldGreen = Annotated[Particle, particle("#NOTE_CIRCULAR_HOLD_GREEN")]
|
|
123
|
+
NoteCircularHoldBlue = Annotated[Particle, particle("#NOTE_CIRCULAR_HOLD_BLUE")]
|
|
124
|
+
NoteCircularHoldYellow = Annotated[Particle, particle("#NOTE_CIRCULAR_HOLD_YELLOW")]
|
|
125
|
+
NoteCircularHoldPurple = Annotated[Particle, particle("#NOTE_CIRCULAR_HOLD_PURPLE")]
|
|
126
|
+
NoteCircularHoldCyan = Annotated[Particle, particle("#NOTE_CIRCULAR_HOLD_CYAN")]
|
|
127
|
+
NoteLinearTapNeutral = Annotated[Particle, particle("#NOTE_LINEAR_TAP_NEUTRAL")]
|
|
128
|
+
NoteLinearTapRed = Annotated[Particle, particle("#NOTE_LINEAR_TAP_RED")]
|
|
129
|
+
NoteLinearTapGreen = Annotated[Particle, particle("#NOTE_LINEAR_TAP_GREEN")]
|
|
130
|
+
NoteLinearTapBlue = Annotated[Particle, particle("#NOTE_LINEAR_TAP_BLUE")]
|
|
131
|
+
NoteLinearTapYellow = Annotated[Particle, particle("#NOTE_LINEAR_TAP_YELLOW")]
|
|
132
|
+
NoteLinearTapPurple = Annotated[Particle, particle("#NOTE_LINEAR_TAP_PURPLE")]
|
|
133
|
+
NoteLinearTapCyan = Annotated[Particle, particle("#NOTE_LINEAR_TAP_CYAN")]
|
|
134
|
+
NoteLinearAlternativeNeutral = Annotated[Particle, particle("#NOTE_LINEAR_ALTERNATIVE_NEUTRAL")]
|
|
135
|
+
NoteLinearAlternativeRed = Annotated[Particle, particle("#NOTE_LINEAR_ALTERNATIVE_RED")]
|
|
136
|
+
NoteLinearAlternativeGreen = Annotated[Particle, particle("#NOTE_LINEAR_ALTERNATIVE_GREEN")]
|
|
137
|
+
NoteLinearAlternativeBlue = Annotated[Particle, particle("#NOTE_LINEAR_ALTERNATIVE_BLUE")]
|
|
138
|
+
NoteLinearAlternativeYellow = Annotated[Particle, particle("#NOTE_LINEAR_ALTERNATIVE_YELLOW")]
|
|
139
|
+
NoteLinearAlternativePurple = Annotated[Particle, particle("#NOTE_LINEAR_ALTERNATIVE_PURPLE")]
|
|
140
|
+
NoteLinearAlternativeCyan = Annotated[Particle, particle("#NOTE_LINEAR_ALTERNATIVE_CYAN")]
|
|
141
|
+
NoteLinearHoldNeutral = Annotated[Particle, particle("#NOTE_LINEAR_HOLD_NEUTRAL")]
|
|
142
|
+
NoteLinearHoldRed = Annotated[Particle, particle("#NOTE_LINEAR_HOLD_RED")]
|
|
143
|
+
NoteLinearHoldGreen = Annotated[Particle, particle("#NOTE_LINEAR_HOLD_GREEN")]
|
|
144
|
+
NoteLinearHoldBlue = Annotated[Particle, particle("#NOTE_LINEAR_HOLD_BLUE")]
|
|
145
|
+
NoteLinearHoldYellow = Annotated[Particle, particle("#NOTE_LINEAR_HOLD_YELLOW")]
|
|
146
|
+
NoteLinearHoldPurple = Annotated[Particle, particle("#NOTE_LINEAR_HOLD_PURPLE")]
|
|
147
|
+
NoteLinearHoldCyan = Annotated[Particle, particle("#NOTE_LINEAR_HOLD_CYAN")]
|
|
148
|
+
LaneCircular = Annotated[Particle, particle("#LANE_CIRCULAR")]
|
|
149
|
+
LaneLinear = Annotated[Particle, particle("#LANE_LINEAR")]
|
|
150
|
+
SlotCircular = Annotated[Particle, particle("#SLOT_CIRCULAR")]
|
|
151
|
+
SlotLinear = Annotated[Particle, particle("#SLOT_LINEAR")]
|
|
152
|
+
JudgeLineCircular = Annotated[Particle, particle("#JUDGE_LINE_CIRCULAR")]
|
|
153
|
+
JudgeLineLinear = Annotated[Particle, particle("#JUDGE_LINE_LINEAR")]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@particles
|
|
157
|
+
class EmptyParticles:
|
|
158
|
+
pass
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from sonolus.backend.place import BlockPlace
|
|
2
|
+
from sonolus.script.internal.impl import meta_fn, validate_value
|
|
3
|
+
from sonolus.script.internal.value import Value
|
|
4
|
+
from sonolus.script.num import Num
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@meta_fn
|
|
8
|
+
def deref[T: Value](block: Num, offset: Num, type_: type[T]) -> T:
|
|
9
|
+
block = Num._accept_(block)
|
|
10
|
+
offset = Num._accept_(offset)
|
|
11
|
+
type_ = validate_value(type_)._as_py_()
|
|
12
|
+
if block._is_py_():
|
|
13
|
+
block = block._as_py_()
|
|
14
|
+
if not isinstance(block, int):
|
|
15
|
+
raise TypeError("block must be an integer")
|
|
16
|
+
else:
|
|
17
|
+
if not isinstance(block, Num):
|
|
18
|
+
raise TypeError("block must be a Num")
|
|
19
|
+
block = block.index()
|
|
20
|
+
if offset._is_py_():
|
|
21
|
+
offset = offset._as_py_()
|
|
22
|
+
if not isinstance(offset, int):
|
|
23
|
+
raise TypeError("offset must be an integer")
|
|
24
|
+
else:
|
|
25
|
+
if not isinstance(offset, Num):
|
|
26
|
+
raise TypeError("offset must be a Num")
|
|
27
|
+
offset = offset.index()
|
|
28
|
+
if not (isinstance(type_, type) and issubclass(type_, Value)):
|
|
29
|
+
raise TypeError("type_ must be a Value")
|
|
30
|
+
return type_._from_place_(BlockPlace(block, offset))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from os import PathLike
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from sonolus.script.engine import Engine
|
|
5
|
+
from sonolus.script.level import Level
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Project:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
engine: Engine,
|
|
12
|
+
levels: list[Level] | None = None,
|
|
13
|
+
resources: PathLike | None = None,
|
|
14
|
+
):
|
|
15
|
+
self.engine = engine
|
|
16
|
+
self.levels = levels or []
|
|
17
|
+
self.resources = Path(resources or "resources")
|
sonolus/script/range.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from sonolus.script.iterator import ArrayLike, SonolusIterator
|
|
2
|
+
from sonolus.script.num import Num
|
|
3
|
+
from sonolus.script.record import Record
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Range(Record, ArrayLike[Num]):
|
|
7
|
+
start: int
|
|
8
|
+
end: int
|
|
9
|
+
step: int
|
|
10
|
+
|
|
11
|
+
def __new__(cls, start: Num, end: Num | None = None, step: Num = 1):
|
|
12
|
+
if end is None:
|
|
13
|
+
start, end = 0, start
|
|
14
|
+
return super().__new__(cls, start, end, step)
|
|
15
|
+
|
|
16
|
+
def __iter__(self) -> SonolusIterator:
|
|
17
|
+
return RangeIterator(self.start, self.end, self.step)
|
|
18
|
+
|
|
19
|
+
def __contains__(self, item):
|
|
20
|
+
if self.step > 0:
|
|
21
|
+
return self.start <= item < self.end and (item - self.start) % self.step == 0
|
|
22
|
+
else:
|
|
23
|
+
return self.end < item <= self.start and (self.start - item) % -self.step == 0
|
|
24
|
+
|
|
25
|
+
def size(self) -> int:
|
|
26
|
+
if self.step > 0:
|
|
27
|
+
diff = self.end - self.start
|
|
28
|
+
if diff <= 0:
|
|
29
|
+
return 0
|
|
30
|
+
return (diff + self.step - 1) // self.step
|
|
31
|
+
else:
|
|
32
|
+
diff = self.start - self.end
|
|
33
|
+
if diff <= 0:
|
|
34
|
+
return 0
|
|
35
|
+
return (diff - self.step - 1) // -self.step
|
|
36
|
+
|
|
37
|
+
def __getitem__(self, index: Num) -> Num:
|
|
38
|
+
return self.start + index * self.step
|
|
39
|
+
|
|
40
|
+
def __setitem__(self, index: Num, value: Num):
|
|
41
|
+
raise TypeError("Range does not support item assignment")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class RangeIterator(Record, SonolusIterator):
|
|
45
|
+
value: int
|
|
46
|
+
end: int
|
|
47
|
+
step: int
|
|
48
|
+
|
|
49
|
+
def has_next(self) -> bool:
|
|
50
|
+
if self.step > 0:
|
|
51
|
+
return self.value < self.end
|
|
52
|
+
else:
|
|
53
|
+
return self.value > self.end
|
|
54
|
+
|
|
55
|
+
def next(self) -> Num:
|
|
56
|
+
value = self.value
|
|
57
|
+
self.value += self.step
|
|
58
|
+
return value
|
sonolus/script/record.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from inspect import getmro
|
|
6
|
+
from typing import Any, ClassVar, Self, dataclass_transform, get_origin
|
|
7
|
+
|
|
8
|
+
from sonolus.backend.place import BlockPlace
|
|
9
|
+
from sonolus.script.internal.context import ctx
|
|
10
|
+
from sonolus.script.internal.descriptor import SonolusDescriptor
|
|
11
|
+
from sonolus.script.internal.generic import (
|
|
12
|
+
GenericValue,
|
|
13
|
+
accept_and_infer_types,
|
|
14
|
+
validate_and_resolve_type,
|
|
15
|
+
validate_concrete_type,
|
|
16
|
+
validate_type_spec,
|
|
17
|
+
)
|
|
18
|
+
from sonolus.script.internal.impl import meta_fn
|
|
19
|
+
from sonolus.script.internal.value import Value
|
|
20
|
+
from sonolus.script.num import Num
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass_transform(eq_default=True)
|
|
24
|
+
class Record(GenericValue):
|
|
25
|
+
_value: dict[str, Value]
|
|
26
|
+
_fields: ClassVar[list[RecordField] | None] = None
|
|
27
|
+
_constructor_signature: ClassVar[inspect.Signature]
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def _validate__type_args_(cls, args: tuple[Any, ...]) -> tuple[Any, ...]:
|
|
31
|
+
if cls._fields is None:
|
|
32
|
+
raise TypeError("Base Record class cannot have type arguments")
|
|
33
|
+
return super()._validate__type_args_(args)
|
|
34
|
+
|
|
35
|
+
def __init_subclass__(cls, **kwargs):
|
|
36
|
+
super().__init_subclass__()
|
|
37
|
+
is_parameterizing = cls._type_args_ is not None and all(
|
|
38
|
+
getattr(parent, "_type_args_", None) is None for parent in getmro(cls)[1:]
|
|
39
|
+
)
|
|
40
|
+
if is_parameterizing:
|
|
41
|
+
fields = []
|
|
42
|
+
offset = 0
|
|
43
|
+
for generic_field in cls._fields:
|
|
44
|
+
resolved_type = validate_and_resolve_type(generic_field.type, cls._type_vars_to_args_)
|
|
45
|
+
resolved_type = validate_concrete_type(resolved_type)
|
|
46
|
+
field = RecordField(generic_field.name, resolved_type, generic_field.index, offset)
|
|
47
|
+
fields.append(field)
|
|
48
|
+
setattr(cls, field.name, field)
|
|
49
|
+
offset += resolved_type._size_()
|
|
50
|
+
cls._fields = fields
|
|
51
|
+
return
|
|
52
|
+
is_inheriting_from_existing_record_class = cls._fields is not None
|
|
53
|
+
if is_inheriting_from_existing_record_class and not is_parameterizing:
|
|
54
|
+
# The main reason this is disallowed is that subclasses wouldn't be substitutable for their parent classes
|
|
55
|
+
# Assignment of a subclass instance to a variable of the parent class would either be disallowed or would
|
|
56
|
+
# require object slicing. Either way, it could lead to confusion.
|
|
57
|
+
# Dealing with generic supertypes is also tricky, so it isn't really worth the effort to support this.
|
|
58
|
+
raise TypeError("Subclassing of a Record is not supported")
|
|
59
|
+
|
|
60
|
+
hints = inspect.get_annotations(cls, eval_str=True)
|
|
61
|
+
fields = []
|
|
62
|
+
params = []
|
|
63
|
+
index = 0
|
|
64
|
+
offset = 0
|
|
65
|
+
for name, hint in hints.items():
|
|
66
|
+
if name not in cls.__annotations__:
|
|
67
|
+
continue
|
|
68
|
+
if hint is ClassVar or get_origin(hint) is ClassVar:
|
|
69
|
+
continue
|
|
70
|
+
if hasattr(cls, name):
|
|
71
|
+
raise TypeError("Default values are not supported for Record fields")
|
|
72
|
+
type_ = validate_type_spec(hint)
|
|
73
|
+
fields.append(RecordField(name, type_, index, offset))
|
|
74
|
+
if isinstance(type_, type) and issubclass(type_, Value) and type_._is_concrete_():
|
|
75
|
+
offset += type_._size_()
|
|
76
|
+
setattr(cls, name, fields[-1])
|
|
77
|
+
index += 1
|
|
78
|
+
params.append(
|
|
79
|
+
inspect.Parameter(
|
|
80
|
+
name,
|
|
81
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
82
|
+
annotation=type_,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
cls._parameterized_ = {}
|
|
87
|
+
cls._fields = fields
|
|
88
|
+
cls._constructor_signature = inspect.Signature(params)
|
|
89
|
+
|
|
90
|
+
_add_inplace_ops(cls)
|
|
91
|
+
|
|
92
|
+
cls.__match_args__ = tuple(field.name for field in fields)
|
|
93
|
+
|
|
94
|
+
if len(getattr(cls, "__type_params__", ())) == 0:
|
|
95
|
+
# Make the class behave as the parameterized version
|
|
96
|
+
cls._type_args_ = ()
|
|
97
|
+
cls._type_vars_to_args_ = {}
|
|
98
|
+
cls._parameterized_[()] = cls
|
|
99
|
+
|
|
100
|
+
def __new__(cls, *args, **kwargs):
|
|
101
|
+
# We override __new__ to allow changing to the parameterized version
|
|
102
|
+
if cls._constructor_signature is None:
|
|
103
|
+
raise TypeError(f"Cannot instantiate {cls.__name__}")
|
|
104
|
+
bound = cls._constructor_signature.bind(*args, **kwargs)
|
|
105
|
+
bound.apply_defaults()
|
|
106
|
+
values = {}
|
|
107
|
+
type_vars = {}
|
|
108
|
+
for field in cls._fields:
|
|
109
|
+
value = bound.arguments[field.name]
|
|
110
|
+
value = accept_and_infer_types(field.type, value, type_vars)
|
|
111
|
+
values[field.name] = value._get_()
|
|
112
|
+
for type_param in cls.__type_params__:
|
|
113
|
+
if type_param not in type_vars:
|
|
114
|
+
raise TypeError(f"Type parameter {type_param} is not used")
|
|
115
|
+
type_args = tuple(type_vars[type_param] for type_param in cls.__type_params__)
|
|
116
|
+
if cls._type_args_ is not None:
|
|
117
|
+
parameterized = cls
|
|
118
|
+
else:
|
|
119
|
+
parameterized = cls[type_args]
|
|
120
|
+
result: cls = object.__new__(parameterized) # type: ignore
|
|
121
|
+
result._value = values
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
def __init__(self, *args, **kwargs):
|
|
125
|
+
# Initialization is done in __new__ and other methods
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def _raw(cls, **kwargs) -> Self:
|
|
130
|
+
result = object.__new__(cls)
|
|
131
|
+
result._value = kwargs
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def _size_(cls) -> int:
|
|
136
|
+
return sum(field.type._size_() for field in cls._fields)
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def _is_value_type_(cls) -> bool:
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def _from_place_(cls, place: BlockPlace) -> Self:
|
|
144
|
+
result = object.__new__(cls)
|
|
145
|
+
result._value = {field.name: field.type._from_place_(place.add_offset(field.offset)) for field in cls._fields}
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def _accepts_(cls, value: Any) -> bool:
|
|
150
|
+
return issubclass(type(value), cls)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def _accept_(cls, value: Any) -> Self:
|
|
154
|
+
if not cls._accepts_(value):
|
|
155
|
+
raise TypeError(f"Cannot accept value {value} as {cls.__name__}")
|
|
156
|
+
return value
|
|
157
|
+
|
|
158
|
+
def _is_py_(self) -> bool:
|
|
159
|
+
return all(value._is_py_() for value in self._value.values())
|
|
160
|
+
|
|
161
|
+
def _as_py_(self) -> Self:
|
|
162
|
+
if not self._is_py_():
|
|
163
|
+
raise ValueError("Not a python value")
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def _from_list_(cls, values: Iterable[float | BlockPlace]) -> Self:
|
|
168
|
+
iterator = iter(values)
|
|
169
|
+
return cls(**{field.name: field.type._from_list_(iterator) for field in cls._fields})
|
|
170
|
+
|
|
171
|
+
def _to_list_(self) -> list[float | BlockPlace]:
|
|
172
|
+
result = []
|
|
173
|
+
for field in self._fields:
|
|
174
|
+
result.extend(self._value[field.name]._to_list_())
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def _flat_keys_(cls, prefix: str) -> list[str]:
|
|
179
|
+
result = []
|
|
180
|
+
for field in cls._fields:
|
|
181
|
+
result.extend(field.type._flat_keys_(f"{prefix}.{field.name}"))
|
|
182
|
+
return result
|
|
183
|
+
|
|
184
|
+
def _get_(self) -> Self:
|
|
185
|
+
return self
|
|
186
|
+
|
|
187
|
+
def _set_(self, value: Self):
|
|
188
|
+
raise TypeError("Record does not support set_")
|
|
189
|
+
|
|
190
|
+
def _copy_from_(self, value: Self):
|
|
191
|
+
if not isinstance(value, type(self)):
|
|
192
|
+
raise TypeError("Cannot copy from different type")
|
|
193
|
+
for field in self._fields:
|
|
194
|
+
field.__set__(self, field.__get__(value))
|
|
195
|
+
|
|
196
|
+
def _copy_(self) -> Self:
|
|
197
|
+
return type(self)(**{field.name: self._value[field.name]._copy_() for field in self._fields})
|
|
198
|
+
|
|
199
|
+
@classmethod
|
|
200
|
+
def _alloc_(cls) -> Self:
|
|
201
|
+
# Compared to using the constructor, this avoids unnecessary _get_ calls
|
|
202
|
+
result = object.__new__(cls)
|
|
203
|
+
result._value = {field.name: field.type._alloc_() for field in cls._fields}
|
|
204
|
+
return result
|
|
205
|
+
|
|
206
|
+
def __str__(self):
|
|
207
|
+
return (
|
|
208
|
+
f"{self.__class__.__name__}({", ".join(f"{field.name}={field.__get__(self)}" for field in self._fields)})"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def __repr__(self):
|
|
212
|
+
return (
|
|
213
|
+
f"{self.__class__.__name__}({", ".join(f"{field.name}={field.__get__(self)!r}" for field in self._fields)})"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
@meta_fn
|
|
217
|
+
def __eq__(self, other):
|
|
218
|
+
if not isinstance(other, type(self)):
|
|
219
|
+
return False
|
|
220
|
+
result: Num = Num._accept_(True)
|
|
221
|
+
for field in self._fields:
|
|
222
|
+
result = result.and_(field.__get__(self) == field.__get__(other))
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
@meta_fn
|
|
226
|
+
def __ne__(self, other):
|
|
227
|
+
if not isinstance(other, type(self)):
|
|
228
|
+
return True
|
|
229
|
+
result: Num = Num._accept_(False)
|
|
230
|
+
for field in self._fields:
|
|
231
|
+
result = result.or_(field.__get__(self) != field.__get__(other))
|
|
232
|
+
return result
|
|
233
|
+
|
|
234
|
+
def __hash__(self):
|
|
235
|
+
raise TypeError("Record is not hashable")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class RecordField(SonolusDescriptor):
|
|
239
|
+
def __init__(self, name: str, type_: type[Value], index: int, offset: int):
|
|
240
|
+
self.name = name
|
|
241
|
+
self.type = type_
|
|
242
|
+
self.index = index
|
|
243
|
+
self.offset = offset
|
|
244
|
+
|
|
245
|
+
def __get__(self, instance: Record | None, owner=None):
|
|
246
|
+
if instance is None:
|
|
247
|
+
return self
|
|
248
|
+
result = instance._value[self.name]._get_()
|
|
249
|
+
if ctx():
|
|
250
|
+
return result
|
|
251
|
+
else:
|
|
252
|
+
return result._as_py_()
|
|
253
|
+
|
|
254
|
+
def __set__(self, instance: Record, value):
|
|
255
|
+
value = self.type._accept_(value)
|
|
256
|
+
if self.type._is_value_type_():
|
|
257
|
+
instance._value[self.name]._set_(value)
|
|
258
|
+
else:
|
|
259
|
+
instance._value[self.name]._copy_from_(value)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
ops_to_inplace_ops = {
|
|
263
|
+
"__add__": "__iadd__",
|
|
264
|
+
"__sub__": "__isub__",
|
|
265
|
+
"__mul__": "__imul__",
|
|
266
|
+
"__truediv__": "__itruediv__",
|
|
267
|
+
"__floordiv__": "__ifloordiv__",
|
|
268
|
+
"__mod__": "__imod__",
|
|
269
|
+
"__pow__": "__ipow__",
|
|
270
|
+
"__lshift__": "__ilshift__",
|
|
271
|
+
"__rshift__": "__irshift__",
|
|
272
|
+
"__or__": "__ior__",
|
|
273
|
+
"__xor__": "__ixor__",
|
|
274
|
+
"__and__": "__iand__",
|
|
275
|
+
"__matmul__": "__imatmul__",
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _add_inplace_ops(cls):
|
|
280
|
+
for op, inplace_op in ops_to_inplace_ops.items():
|
|
281
|
+
if hasattr(cls, op) and not hasattr(cls, inplace_op):
|
|
282
|
+
setattr(cls, inplace_op, _make_inplace_op(op))
|
|
283
|
+
return cls
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _make_inplace_op(op: str):
|
|
287
|
+
@meta_fn
|
|
288
|
+
def inplace_op(self, other):
|
|
289
|
+
_compiler_internal_ = True # noqa: F841
|
|
290
|
+
self._copy_from_(getattr(self, op)(other))
|
|
291
|
+
return self
|
|
292
|
+
|
|
293
|
+
return inplace_op
|