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.

Files changed (75) hide show
  1. sonolus/__init__.py +0 -0
  2. sonolus/backend/__init__.py +0 -0
  3. sonolus/backend/allocate.py +51 -0
  4. sonolus/backend/blocks.py +756 -0
  5. sonolus/backend/excepthook.py +37 -0
  6. sonolus/backend/finalize.py +69 -0
  7. sonolus/backend/flow.py +92 -0
  8. sonolus/backend/interpret.py +333 -0
  9. sonolus/backend/ir.py +89 -0
  10. sonolus/backend/mode.py +24 -0
  11. sonolus/backend/node.py +40 -0
  12. sonolus/backend/ops.py +197 -0
  13. sonolus/backend/optimize.py +9 -0
  14. sonolus/backend/passes.py +6 -0
  15. sonolus/backend/place.py +90 -0
  16. sonolus/backend/simplify.py +30 -0
  17. sonolus/backend/utils.py +48 -0
  18. sonolus/backend/visitor.py +880 -0
  19. sonolus/build/__init__.py +0 -0
  20. sonolus/build/cli.py +170 -0
  21. sonolus/build/collection.py +293 -0
  22. sonolus/build/compile.py +90 -0
  23. sonolus/build/defaults.py +32 -0
  24. sonolus/build/engine.py +149 -0
  25. sonolus/build/level.py +23 -0
  26. sonolus/build/node.py +43 -0
  27. sonolus/build/project.py +94 -0
  28. sonolus/py.typed +0 -0
  29. sonolus/script/__init__.py +0 -0
  30. sonolus/script/archetype.py +651 -0
  31. sonolus/script/array.py +241 -0
  32. sonolus/script/bucket.py +192 -0
  33. sonolus/script/callbacks.py +105 -0
  34. sonolus/script/comptime.py +146 -0
  35. sonolus/script/containers.py +247 -0
  36. sonolus/script/debug.py +70 -0
  37. sonolus/script/effect.py +132 -0
  38. sonolus/script/engine.py +101 -0
  39. sonolus/script/globals.py +234 -0
  40. sonolus/script/graphics.py +141 -0
  41. sonolus/script/icon.py +73 -0
  42. sonolus/script/internal/__init__.py +5 -0
  43. sonolus/script/internal/builtin_impls.py +144 -0
  44. sonolus/script/internal/context.py +365 -0
  45. sonolus/script/internal/descriptor.py +17 -0
  46. sonolus/script/internal/error.py +15 -0
  47. sonolus/script/internal/generic.py +197 -0
  48. sonolus/script/internal/impl.py +69 -0
  49. sonolus/script/internal/introspection.py +14 -0
  50. sonolus/script/internal/native.py +38 -0
  51. sonolus/script/internal/value.py +144 -0
  52. sonolus/script/interval.py +98 -0
  53. sonolus/script/iterator.py +211 -0
  54. sonolus/script/level.py +52 -0
  55. sonolus/script/math.py +92 -0
  56. sonolus/script/num.py +382 -0
  57. sonolus/script/options.py +194 -0
  58. sonolus/script/particle.py +158 -0
  59. sonolus/script/pointer.py +30 -0
  60. sonolus/script/project.py +17 -0
  61. sonolus/script/range.py +58 -0
  62. sonolus/script/record.py +293 -0
  63. sonolus/script/runtime.py +526 -0
  64. sonolus/script/sprite.py +332 -0
  65. sonolus/script/text.py +404 -0
  66. sonolus/script/timing.py +42 -0
  67. sonolus/script/transform.py +118 -0
  68. sonolus/script/ui.py +160 -0
  69. sonolus/script/values.py +43 -0
  70. sonolus/script/vec.py +48 -0
  71. sonolus_py-0.1.0.dist-info/METADATA +10 -0
  72. sonolus_py-0.1.0.dist-info/RECORD +75 -0
  73. sonolus_py-0.1.0.dist-info/WHEEL +4 -0
  74. sonolus_py-0.1.0.dist-info/entry_points.txt +2 -0
  75. 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")
@@ -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
@@ -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