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.

@@ -1,4 +1,5 @@
1
- from typing import Never, assert_never
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 _type(value):
393
- return type(value)
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: dict[type, int] | None = None):
107
+ def __init__(self, mode: Mode, archetypes: list[type] | None = None):
106
108
  from sonolus.script.array import Array
107
109
 
108
- self.archetypes = archetypes or {}
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 ordered_archetypes)) if archetypes else Array[int, Literal[0]]()
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 ordered_archetypes))
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
- return blocks[context]
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
- args = validate_type_args(args)
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)
@@ -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 in {
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
- results = inspect.get_annotations(cls, globals=globals, locals=locals, eval_str=eval_str)
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:
@@ -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._quick_construct(start=self.start + other, end=self.end + other)
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._quick_construct(start=self.start - other, end=self.end - other)
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._quick_construct(start=self.start * other, end=self.end * other)
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._quick_construct(start=self.start / other, end=self.end / other)
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._quick_construct(start=self.start // other, end=self.end // other)
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
@@ -149,3 +149,5 @@ class BuildConfig:
149
149
 
150
150
  runtime_checks: RuntimeChecks = RuntimeChecks.NONE
151
151
  """Runtime error checking mode."""
152
+
153
+ verbose: bool = False
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
- class RecordMeta(ABCMeta):
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
- if name not in cls.__annotations__:
107
- continue
108
- if hint is ClassVar or get_origin(hint) is ClassVar:
109
- continue
110
- if hasattr(cls, name):
111
- raise TypeError("Default values are not supported for Record fields")
112
- type_ = validate_type_spec(hint)
113
- fields.append(_RecordField(name, type_, index, offset))
114
- if isinstance(type_, type) and issubclass(type_, Value) and type_._is_concrete_():
115
- offset += type_._size_()
116
- setattr(cls, name, fields[-1])
117
- index += 1
118
- params.append(
119
- inspect.Parameter(
120
- name,
121
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
122
- annotation=type_,
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 _quick_construct(cls, **kwargs) -> Self:
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._quick_construct(t=1, r=aspect_ratio(), b=-1, l=-aspect_ratio())
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
- instance = cls()
98
- entries = []
99
- # Offset 0 is unused so we can tell when a stream object is uninitialized since it'll have offset 0.
100
- offset = 1
101
- for name, annotation in get_field_specifiers(cls).items():
102
- if issubclass(annotation, Stream | StreamGroup):
103
- annotation = cast(type[Stream | StreamGroup], annotation)
104
- if annotation is Stream or annotation is StreamGroup:
105
- raise TypeError(f"Invalid annotation for streams: {annotation}. Must have type arguments.")
106
- setattr(cls, name, _StreamField(offset, annotation))
107
- # Streams store their data across several backing streams
108
- entries.append((name, offset, annotation))
109
- offset += annotation.backing_size()
110
- elif issubclass(annotation, Value) and annotation._is_concrete_():
111
- setattr(cls, name, _StreamDataField(offset, annotation))
112
- # Data fields store their data in a single backing stream at different offsets in the same stream
113
- entries.append((name, offset, annotation))
114
- offset += 1
115
- instance._streams_ = entries
116
- instance._is_comptime_value_ = True
117
- return instance
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
@@ -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 / self.a22,
331
- self.a01 / self.a22,
332
- self.a02 / self.a22,
333
- self.a10 / self.a22,
334
- self.a11 / self.a22,
335
- self.a12 / self.a22,
336
- self.a20 / self.a22,
337
- self.a21 / self.a22,
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
- type_ = validate_concrete_type(type_)
53
- if ctx():
54
- return Num(type_._size_())
55
- else:
56
- return type_._size_()
51
+ return validate_concrete_type(type_)._size_()