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,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
|