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,365 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+ from contextvars import ContextVar
5
+ from dataclasses import dataclass
6
+ from typing import Any, Self
7
+
8
+ from sonolus.backend.blocks import BlockData, PlayBlock
9
+ from sonolus.backend.flow import BasicBlock, FlowEdge
10
+ from sonolus.backend.ir import IRConst, IRStmt
11
+ from sonolus.backend.mode import Mode
12
+ from sonolus.backend.place import Block, BlockPlace, TempBlock
13
+ from sonolus.script.globals import GlobalInfo, GlobalPlaceholder
14
+ from sonolus.script.internal.value import Value
15
+
16
+ _compiler_internal_ = True
17
+
18
+ context_var = ContextVar("context_var", default=None)
19
+
20
+
21
+ class GlobalContextState:
22
+ archetypes: dict[type, int]
23
+ rom: ReadOnlyMemory
24
+ const_mappings: dict[Any, int]
25
+ environment_mappings: dict[GlobalInfo, int]
26
+ environment_offsets: dict[Block, int]
27
+ mode: Mode
28
+
29
+ def __init__(self, mode: Mode, archetypes: dict[type, int] | None = None, rom: ReadOnlyMemory | None = None):
30
+ self.archetypes = archetypes or {}
31
+ self.rom = ReadOnlyMemory() if rom is None else rom
32
+ self.const_mappings = {}
33
+ self.environment_mappings = {}
34
+ self.environment_offsets = {}
35
+ self.mode = mode
36
+
37
+
38
+ class CallbackContextState:
39
+ callback: str
40
+ used_names: dict[str, int]
41
+
42
+ def __init__(self, callback: str):
43
+ self.callback = callback
44
+ self.used_names = {}
45
+
46
+
47
+ class Context:
48
+ global_state: GlobalContextState
49
+ callback_state: CallbackContextState
50
+ statements: list[IRStmt]
51
+ test: IRStmt
52
+ outgoing: dict[float | None, Context]
53
+ scope: Scope
54
+ loop_variables: dict[str, Value]
55
+ live: bool
56
+
57
+ def __init__(
58
+ self,
59
+ global_state: GlobalContextState,
60
+ callback_state: CallbackContextState,
61
+ scope: Scope | None = None,
62
+ live: bool = True,
63
+ ):
64
+ self.global_state = global_state
65
+ self.callback_state = callback_state
66
+ self.statements = []
67
+ self.test = IRConst(0)
68
+ self.outgoing = {}
69
+ self.scope = scope if scope is not None else Scope()
70
+ self.loop_variables = {}
71
+ self.live = live
72
+
73
+ @property
74
+ def rom(self) -> ReadOnlyMemory:
75
+ return self.global_state.rom
76
+
77
+ @property
78
+ def blocks(self) -> type[Block]:
79
+ return self.global_state.mode.blocks
80
+
81
+ @property
82
+ def callback(self) -> str:
83
+ return self.callback_state.callback
84
+
85
+ @property
86
+ def used_names(self) -> dict[str, int]:
87
+ return self.callback_state.used_names
88
+
89
+ @property
90
+ def const_mappings(self) -> dict[Any, int]:
91
+ return self.global_state.const_mappings
92
+
93
+ def check_readable(self, place: BlockPlace):
94
+ if not self.callback:
95
+ return
96
+ if isinstance(place.block, BlockData) and self.callback not in self.blocks(place.block).readable:
97
+ raise RuntimeError(f"Block {place.block} is not readable in {self.callback}")
98
+
99
+ def check_writable(self, place: BlockPlace):
100
+ if not self.callback:
101
+ return
102
+ if isinstance(place.block, BlockData) and self.callback not in self.blocks(place.block).writable:
103
+ raise RuntimeError(f"Block {place.block} is not writable in {self.callback}")
104
+
105
+ def add_statements(self, *statements: IRStmt):
106
+ self.statements.extend(statements)
107
+
108
+ def alloc(self, name: str | None = None, size: int = 1) -> BlockPlace:
109
+ if size == 0:
110
+ return BlockPlace(TempBlock(name or "e", 0), 0)
111
+ name = name or ("v" if size == 1 else "a")
112
+ num = self._get_alloc_number(name)
113
+ return BlockPlace(TempBlock(f"{name}{num}", size), 0)
114
+
115
+ def _get_alloc_number(self, name: str) -> int:
116
+ if name not in self.used_names:
117
+ self.used_names[name] = 0
118
+ self.used_names[name] += 1
119
+ return self.used_names[name]
120
+
121
+ def copy_with_scope(self, scope: Scope) -> Context:
122
+ return Context(
123
+ global_state=self.global_state,
124
+ callback_state=self.callback_state,
125
+ scope=scope,
126
+ live=self.live,
127
+ )
128
+
129
+ def branch(self, condition: float | None):
130
+ assert condition not in self.outgoing
131
+ result = self.copy_with_scope(self.scope.copy())
132
+ self.outgoing[condition] = result
133
+ return result
134
+
135
+ def branch_with_scope(self, condition: float | None, scope: Scope):
136
+ assert condition not in self.outgoing
137
+ result = self.copy_with_scope(scope)
138
+ self.outgoing[condition] = result
139
+ return result
140
+
141
+ def into_dead(self):
142
+ """Create a new context for code that is unreachable, like after a return statement."""
143
+ result = self.copy_with_scope(self.scope.copy())
144
+ result.live = False
145
+ return result
146
+
147
+ def prepare_loop_header(self, to_merge: set[str]) -> Self:
148
+ # to_merge is the set of bindings set anywhere in the loop
149
+ # we need to invalidate them in the header if they're reference types
150
+ # or merge them if they're value types
151
+ # structure is self -> intermediate -> header -> body (continue -> header) | exit
152
+ assert len(self.outgoing) == 0
153
+ header = self.branch(None)
154
+ for name in to_merge:
155
+ binding = self.scope.get_binding(name)
156
+ if not isinstance(binding, ValueBinding):
157
+ continue
158
+ value = binding.value
159
+ type_ = type(value)
160
+ if type_._is_value_type_():
161
+ target_value = type_._from_place_(header.alloc(size=type_._size_()))
162
+ with using_ctx(self):
163
+ target_value._set_(value)
164
+ header.scope.set_value(name, target_value)
165
+ header.loop_variables[name] = target_value
166
+ else:
167
+ header.scope.set_binding(name, ConflictBinding())
168
+ return header
169
+
170
+ def branch_to_loop_header(self, header: Self):
171
+ if not self.live:
172
+ return
173
+ assert len(self.outgoing) == 0
174
+ self.outgoing[None] = header
175
+ for name, target_value in header.loop_variables.items():
176
+ with using_ctx(self):
177
+ value = self.scope.get_value(name)
178
+ value = type(target_value)._accept_(value)
179
+ target_value._set_(value)
180
+
181
+ def map_constant(self, value: Any) -> int:
182
+ if value not in self.const_mappings:
183
+ self.const_mappings[value] = len(self.const_mappings)
184
+ return self.const_mappings[value]
185
+
186
+ def get_global_base(self, value: GlobalInfo | GlobalPlaceholder) -> BlockPlace:
187
+ block = value.blocks.get(self.global_state.mode)
188
+ if block is None:
189
+ raise RuntimeError(f"Global {value.name} is not available in '{self.global_state.mode.name}' mode")
190
+ if value not in self.global_state.environment_mappings:
191
+ if value.offset is None:
192
+ offset = self.global_state.environment_offsets.get(block, 0)
193
+ self.global_state.environment_mappings[value] = offset
194
+ self.global_state.environment_offsets[block] = offset + value.size
195
+ else:
196
+ self.global_state.environment_mappings[value] = value.offset
197
+ return BlockPlace(block, self.global_state.environment_mappings[value])
198
+
199
+ @classmethod
200
+ def meet(cls, contexts: list[Context]) -> Context:
201
+ if not contexts:
202
+ raise RuntimeError("Cannot meet empty list of contexts")
203
+ if not any(context.live for context in contexts):
204
+ return contexts[0].into_dead()
205
+ contexts = [context for context in contexts if context.live]
206
+ assert not any(context.outgoing for context in contexts)
207
+ assert all(len(context.outgoing) == 0 for context in contexts)
208
+ target = contexts[0].copy_with_scope(Scope())
209
+ Scope.apply_merge(target, contexts)
210
+ for context in contexts:
211
+ context.outgoing[None] = target
212
+ return target
213
+
214
+
215
+ def ctx() -> Context | None:
216
+ return context_var.get()
217
+
218
+
219
+ def set_ctx(value: Context | None):
220
+ return context_var.set(value)
221
+
222
+
223
+ @contextmanager
224
+ def using_ctx(value: Context | None):
225
+ token = context_var.set(value)
226
+ try:
227
+ yield
228
+ finally:
229
+ context_var.reset(token)
230
+
231
+
232
+ class ReadOnlyMemory:
233
+ values: list[float]
234
+ indexes: dict[tuple[float, ...], int]
235
+
236
+ def __init__(self):
237
+ self.values = []
238
+ self.indexes = {}
239
+
240
+ def __getitem__(self, item: tuple[float, ...]) -> BlockPlace:
241
+ if item not in self.indexes:
242
+ index = len(self.values)
243
+ self.indexes[item] = index
244
+ self.values.extend(item)
245
+ else:
246
+ index = self.indexes[item]
247
+ return BlockPlace(self.block, index)
248
+
249
+ @property
250
+ def block(self) -> Block:
251
+ if ctx():
252
+ return ctx().blocks.EngineRom
253
+ else:
254
+ return PlayBlock.EngineRom
255
+
256
+
257
+ @dataclass
258
+ class ValueBinding:
259
+ value: Value
260
+
261
+
262
+ @dataclass
263
+ class ConflictBinding:
264
+ pass
265
+
266
+
267
+ @dataclass
268
+ class EmptyBinding:
269
+ pass
270
+
271
+
272
+ Binding = ValueBinding | ConflictBinding | EmptyBinding
273
+
274
+
275
+ class Scope:
276
+ bindings: dict[str, Binding]
277
+
278
+ def __init__(self, bindings: dict[str, Binding] | None = None):
279
+ self.bindings = bindings or {}
280
+
281
+ def get_binding(self, name: str) -> Binding:
282
+ return self.bindings.get(name, EmptyBinding())
283
+
284
+ def set_binding(self, name: str, binding: Binding):
285
+ self.bindings[name] = binding
286
+
287
+ def get_value(self, name: str) -> Value:
288
+ binding = self.get_binding(name)
289
+ match binding:
290
+ case ValueBinding(value):
291
+ # we don't need to call _get_() here because _set_() is never called where it could be a problem
292
+ return value
293
+ case ConflictBinding():
294
+ raise RuntimeError(
295
+ f"Binding '{name}' has multiple conflicting definitions or may not be guaranteed to be defined"
296
+ )
297
+ case EmptyBinding():
298
+ raise RuntimeError(f"Binding '{name}' is not defined")
299
+
300
+ def set_value(self, name: str, value: Value):
301
+ from sonolus.script.internal.impl import validate_value
302
+
303
+ self.bindings[name] = ValueBinding(validate_value(value))
304
+
305
+ def delete_binding(self, name: str):
306
+ del self.bindings[name]
307
+
308
+ def copy(self) -> Scope:
309
+ return Scope(self.bindings.copy())
310
+
311
+ @classmethod
312
+ def apply_merge(cls, target: Context, incoming: list[Context]):
313
+ if not incoming:
314
+ return
315
+ assert all(len(inc.outgoing) == 0 for inc in incoming)
316
+ sources = [context.scope for context in incoming]
317
+ keys = {key for source in sources for key in source.bindings}
318
+ for key in keys:
319
+ bindings = [source.get_binding(key) for source in sources]
320
+ if not all(isinstance(binding, ValueBinding) for binding in bindings):
321
+ target.scope.set_binding(key, ConflictBinding())
322
+ continue
323
+ values = [binding.value for binding in bindings]
324
+ if len({id(value) for value in values}) == 1:
325
+ target.scope.set_binding(key, ValueBinding(values[0]))
326
+ continue
327
+ types = {type(value) for value in values}
328
+ if len(types) > 1:
329
+ target.scope.set_binding(key, ConflictBinding())
330
+ continue
331
+ common_type: type[Value] = types.pop()
332
+ if common_type._is_value_type_():
333
+ target_value = common_type._from_place_(target.alloc(size=common_type._size_()))
334
+ for inc in incoming:
335
+ with using_ctx(inc):
336
+ target_value._set_(inc.scope.get_value(key))
337
+ target.scope.set_value(key, target_value)
338
+ continue
339
+ else:
340
+ target.scope.set_binding(key, ConflictBinding())
341
+ continue
342
+
343
+
344
+ def iter_contexts(context: Context):
345
+ seen = set()
346
+ queue = [context]
347
+ while queue:
348
+ current = queue.pop()
349
+ if current in seen:
350
+ continue
351
+ seen.add(current)
352
+ yield current
353
+ queue.extend(current.outgoing.values())
354
+
355
+
356
+ def context_to_cfg(context: Context) -> BasicBlock:
357
+ blocks = {context: BasicBlock(statements=context.statements, test=context.test)}
358
+ for current in iter_contexts(context):
359
+ for condition, target in current.outgoing.items():
360
+ if target not in blocks:
361
+ blocks[target] = BasicBlock(statements=target.statements, test=target.test)
362
+ edge = FlowEdge(src=blocks[current], dst=blocks[target], cond=condition)
363
+ blocks[current].outgoing.add(edge)
364
+ blocks[target].incoming.add(edge)
365
+ return blocks[context]
@@ -0,0 +1,17 @@
1
+ from abc import abstractmethod
2
+
3
+
4
+ class SonolusDescriptor:
5
+ """Base class for Sonolus descriptors.
6
+
7
+ The compiler checks if a descriptor is an instance of a subclass of this class,
8
+ so it knows that it's a supported descriptor.
9
+ """
10
+
11
+ @abstractmethod
12
+ def __get__(self, instance, owner):
13
+ pass
14
+
15
+ @abstractmethod
16
+ def __set__(self, instance, value):
17
+ pass
@@ -0,0 +1,15 @@
1
+ class InternalError(RuntimeError):
2
+ """Represents an error occurring due to a violation of an internal invariant.
3
+
4
+ This indicates there is a bug in sonolus.py, or that internal details have been used incorrectly.
5
+ """
6
+
7
+ def __init__(self, message: str):
8
+ super().__init__(message)
9
+
10
+
11
+ class CompilationError(RuntimeError):
12
+ """Represents an error occurring during the compilation of a script."""
13
+
14
+ def __init__(self, message: str):
15
+ super().__init__(message)
@@ -0,0 +1,197 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import Annotated, Any, ClassVar, Literal, Self, TypeVar, get_origin
5
+
6
+ from sonolus.script.internal.impl import meta_fn
7
+ from sonolus.script.internal.value import Value
8
+
9
+ type AnyType = type[Value] | PartialGeneric | TypeVar
10
+
11
+
12
+ def validate_type_arg(arg: Any) -> Any:
13
+ from sonolus.script.internal.impl import validate_value
14
+
15
+ arg = validate_value(arg)
16
+ if not arg._is_py_():
17
+ raise TypeError(f"Expected a compile-time constant type argument, got {arg}")
18
+ result = arg._as_py_()
19
+ if get_origin(result) is Annotated:
20
+ return result.__args__[0]
21
+ if get_origin(result) is Literal:
22
+ return result.__args__[0]
23
+ return result
24
+
25
+
26
+ def validate_type_spec(spec: Any) -> PartialGeneric | TypeVar | type[Value]:
27
+ spec = validate_type_arg(spec)
28
+ if isinstance(spec, type) and issubclass(spec, Enum):
29
+ # For values like IntEnum subclasses, this will call validate_type_spec(IntEnum),
30
+ # which in turn will call it on int, so this works.
31
+ spec = validate_type_spec(spec.__mro__[1])
32
+ if isinstance(spec, PartialGeneric | TypeVar) or (isinstance(spec, type) and issubclass(spec, Value)):
33
+ return spec
34
+ raise TypeError(f"Invalid type spec: {spec}")
35
+
36
+
37
+ def validate_concrete_type(spec: Any) -> type[Value]:
38
+ spec = validate_type_spec(spec)
39
+ if isinstance(spec, type) and issubclass(spec, Value) and spec._is_concrete_():
40
+ return spec
41
+ raise TypeError(f"Expected a concrete type, got {spec}")
42
+
43
+
44
+ def validate_type_args(args) -> tuple[Any, ...]:
45
+ if not isinstance(args, tuple):
46
+ args = (args,)
47
+ return tuple(validate_type_arg(arg) for arg in args)
48
+
49
+
50
+ def contains_incomplete_type(args) -> bool:
51
+ if not isinstance(args, tuple):
52
+ args = (args,)
53
+ return any(isinstance(arg, TypeVar | PartialGeneric) for arg in args)
54
+
55
+
56
+ def format_type_arg(arg: Any) -> str:
57
+ if isinstance(arg, type):
58
+ return arg.__name__
59
+ return f"{arg}"
60
+
61
+
62
+ class GenericValue(Value):
63
+ _parameterized_: ClassVar[dict[tuple[Any, ...], type[Self]]] = {}
64
+ _type_args_: ClassVar[tuple[Any, ...] | None] = None
65
+ _type_vars_to_args_: ClassVar[dict[TypeVar, Any] | None] = None
66
+
67
+ def __init__(self):
68
+ if self._type_args_ is None:
69
+ raise TypeError(f"Missing type arguments for {self.__class__.__name__}")
70
+
71
+ @classmethod
72
+ def _validate__type_args_(cls, args: tuple[Any, ...]) -> tuple[Any, ...]:
73
+ """Validate the type arguments and return them as a tuple.
74
+
75
+ This may be called with PartialGeneric or TypeVar instances inside args.
76
+ """
77
+ if len(args) != len(cls.__type_params__):
78
+ raise TypeError(f"Expected {len(cls.__type_params__)} type arguments, got {len(args)}")
79
+ return args
80
+
81
+ @classmethod
82
+ def _is_concrete_(cls) -> bool:
83
+ return cls._type_args_ is not None
84
+
85
+ @classmethod
86
+ @meta_fn
87
+ def _get_type_arg_(cls, var: TypeVar) -> Any:
88
+ if isinstance(var, Value):
89
+ var = var._as_py_()
90
+ if cls._type_args_ is None:
91
+ raise TypeError(f"Type {cls.__name__} is not parameterized")
92
+ if var in cls._type_vars_to_args_:
93
+ return cls._type_vars_to_args_[var]
94
+ raise TypeError(f"Missing type argument for {var}")
95
+
96
+ @classmethod
97
+ def __class_getitem__(cls, args: Any) -> type[Self]:
98
+ if cls._type_args_ is not None:
99
+ raise TypeError(f"Type {cls.__name__} is already parameterized")
100
+ args = validate_type_args(args)
101
+ args = cls._validate__type_args_(args)
102
+ if contains_incomplete_type(args):
103
+ return PartialGeneric(cls, args)
104
+ if args not in cls._parameterized_:
105
+ cls._parameterized_[args] = cls._get_parameterized(args)
106
+ return cls._parameterized_[args]
107
+
108
+ @classmethod
109
+ def _get_parameterized(cls, args: tuple[Any, ...]) -> type[Self]:
110
+ class Parameterized(cls):
111
+ _type_args_ = args
112
+ _type_vars_to_args_ = dict(zip(cls.__type_params__, args, strict=True)) # noqa: RUF012
113
+
114
+ if args:
115
+ Parameterized.__name__ = f"{cls.__name__}[{', '.join(format_type_arg(arg) for arg in args)}]"
116
+ Parameterized.__qualname__ = f"{cls.__qualname__}[{', '.join(format_type_arg(arg) for arg in args)}]"
117
+ else:
118
+ Parameterized.__name__ = cls.__name__
119
+ Parameterized.__qualname__ = cls.__qualname__
120
+ Parameterized.__module__ = cls.__module__
121
+ return Parameterized
122
+
123
+
124
+ class PartialGeneric[T: GenericValue]:
125
+ def __init__(self, base: type[T], args: tuple[Any, ...]):
126
+ self.base = base
127
+ self.args = args
128
+
129
+ def __repr__(self):
130
+ return f"PartialGeneric({self.base!r}, {self.args!r})"
131
+
132
+ def __str__(self):
133
+ params = ", ".join(format_type_arg(arg) for arg in self.args)
134
+ return f"{self.base.__name__}?[{params}]"
135
+
136
+ def __call__(self, *args, **kwargs):
137
+ instance = self.base(*args, **kwargs)
138
+ # Throw an error if it fails
139
+ accept_and_infer_types(self, instance, {})
140
+ return instance
141
+
142
+
143
+ def infer_and_validate_types(dst: Any, src: Any, results: dict[TypeVar, Any] | None = None) -> dict[TypeVar, Any]:
144
+ results = results if results is not None else {}
145
+ match dst:
146
+ case TypeVar():
147
+ if dst in results:
148
+ if results[dst] != src:
149
+ raise TypeError(
150
+ f"Conflicting types for {dst}: {format_type_arg(results[dst])} and {format_type_arg(src)}"
151
+ )
152
+ else:
153
+ results[dst] = validate_type_arg(src)
154
+ case PartialGeneric():
155
+ if not isinstance(src, type) or not issubclass(src, dst.base):
156
+ raise TypeError(f"Expected type {dst.base.__name__}, got {format_type_arg(src)}")
157
+ assert issubclass(src, GenericValue)
158
+ for d, s in zip(dst.args, src._type_args_, strict=True):
159
+ infer_and_validate_types(d, s, results)
160
+ case _:
161
+ if (
162
+ src != dst # noqa: PLR1714
163
+ and dst != Any
164
+ and not (isinstance(dst, type) and isinstance(src, type) and issubclass(src, dst))
165
+ ):
166
+ raise TypeError(f"Expected {format_type_arg(dst)}, got {format_type_arg(src)}")
167
+ return results
168
+
169
+
170
+ def accept_and_infer_types(dst: Any, val: Any, results: dict[TypeVar, Any]) -> Value:
171
+ from sonolus.script.internal.impl import validate_value
172
+
173
+ val = validate_value(val)
174
+ match dst:
175
+ case TypeVar():
176
+ infer_and_validate_types(dst, type(val), results)
177
+ return val
178
+ case PartialGeneric():
179
+ val = dst.base._accept_(val)
180
+ infer_and_validate_types(dst, type(val), results)
181
+ return val
182
+ case type():
183
+ return dst._accept_(val)
184
+ case _:
185
+ raise TypeError(f"Expected a type, got {format_type_arg(dst)}")
186
+
187
+
188
+ def validate_and_resolve_type(dst: Any, results: dict[TypeVar, Any]) -> Any:
189
+ match dst:
190
+ case TypeVar():
191
+ if dst in results:
192
+ return results[dst]
193
+ raise TypeError(f"Missing type for {dst}")
194
+ case PartialGeneric():
195
+ return dst.base[tuple(validate_and_resolve_type(arg, results) for arg in dst.args)]
196
+ case _:
197
+ return dst
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from enum import Enum
5
+ from types import FunctionType, MethodType, NoneType, NotImplementedType
6
+ from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeVar, get_origin, overload
7
+
8
+ if TYPE_CHECKING:
9
+ from sonolus.script.internal.value import Value
10
+
11
+
12
+ @overload
13
+ def meta_fn[T: Callable](fn: T) -> T: ...
14
+
15
+
16
+ @overload
17
+ def meta_fn[T: Callable]() -> Callable[[T], T]: ...
18
+
19
+
20
+ def meta_fn(fn=None):
21
+ # noinspection PyShadowingNames
22
+ def decorator(fn):
23
+ fn._meta_fn_ = True
24
+ return fn
25
+
26
+ if fn is None:
27
+ return decorator
28
+ return decorator(fn)
29
+
30
+
31
+ def validate_value(value: Any) -> Value:
32
+ result = try_validate_value(value)
33
+ if result is None:
34
+ raise TypeError(f"Unsupported value: {value!r}")
35
+ return result
36
+
37
+
38
+ def try_validate_value(value: Any) -> Value | None:
39
+ from sonolus.script.comptime import Comptime
40
+ from sonolus.script.globals import GlobalPlaceholder
41
+ from sonolus.script.internal.generic import PartialGeneric
42
+ from sonolus.script.internal.value import Value
43
+ from sonolus.script.num import Num
44
+
45
+ match value:
46
+ case Enum():
47
+ return validate_value(value.value)
48
+ case Value():
49
+ return value
50
+ case type():
51
+ if value in {int, float, bool}:
52
+ return Comptime.accept_unchecked(Num)
53
+ return Comptime.accept_unchecked(value)
54
+ case int() | float() | bool():
55
+ return Num._accept_(value)
56
+ case tuple():
57
+ return Comptime.accept_unchecked(tuple(validate_value(v) for v in value))
58
+ case dict():
59
+ return Comptime.accept_unchecked({validate_value(k)._as_py_(): validate_value(v) for k, v in value.items()})
60
+ case PartialGeneric() | TypeVar() | FunctionType() | MethodType() | NotImplementedType() | str() | NoneType():
61
+ return Comptime.accept_unchecked(value)
62
+ case other_type if get_origin(value) in {Literal, Annotated}:
63
+ return Comptime.accept_unchecked(other_type)
64
+ case GlobalPlaceholder():
65
+ return value.get()
66
+ case comptime_value if getattr(comptime_value, "_is_comptime_value_", False):
67
+ return Comptime.accept_unchecked(comptime_value)
68
+ case _:
69
+ return None
@@ -0,0 +1,14 @@
1
+ import inspect
2
+ from typing import Annotated
3
+
4
+ _missing = object()
5
+
6
+
7
+ def get_field_specifiers(cls, *, globals=None, locals=None, eval_str=True): # noqa: A002
8
+ """Like inspect.get_annotations, but also turns class attributes into Annotated."""
9
+ results = inspect.get_annotations(cls, globals=globals, locals=locals, eval_str=eval_str)
10
+ for key, value in results.items():
11
+ class_value = getattr(cls, key, _missing)
12
+ if class_value is not _missing:
13
+ results[key] = Annotated[value, class_value]
14
+ return results