sonolus.py 0.3.3__py3-none-any.whl → 0.4.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/backend/excepthook.py +30 -0
- sonolus/backend/finalize.py +15 -1
- sonolus/backend/ops.py +4 -0
- sonolus/backend/optimize/allocate.py +5 -5
- sonolus/backend/optimize/constant_evaluation.py +124 -19
- sonolus/backend/optimize/copy_coalesce.py +15 -12
- sonolus/backend/optimize/dead_code.py +7 -6
- sonolus/backend/optimize/dominance.py +2 -2
- sonolus/backend/optimize/flow.py +54 -8
- sonolus/backend/optimize/inlining.py +137 -30
- sonolus/backend/optimize/liveness.py +2 -2
- sonolus/backend/optimize/optimize.py +15 -1
- sonolus/backend/optimize/passes.py +11 -3
- sonolus/backend/optimize/simplify.py +137 -8
- sonolus/backend/optimize/ssa.py +47 -13
- sonolus/backend/place.py +5 -4
- sonolus/backend/utils.py +24 -0
- sonolus/backend/visitor.py +260 -17
- sonolus/build/cli.py +47 -19
- sonolus/build/compile.py +12 -5
- sonolus/build/engine.py +70 -1
- sonolus/build/level.py +3 -3
- sonolus/build/project.py +2 -2
- sonolus/script/archetype.py +27 -24
- sonolus/script/array.py +25 -19
- sonolus/script/array_like.py +46 -49
- sonolus/script/bucket.py +1 -1
- sonolus/script/containers.py +22 -26
- sonolus/script/debug.py +24 -47
- sonolus/script/effect.py +1 -1
- sonolus/script/engine.py +2 -2
- sonolus/script/globals.py +3 -3
- sonolus/script/instruction.py +3 -3
- sonolus/script/internal/builtin_impls.py +155 -28
- sonolus/script/internal/constant.py +13 -3
- sonolus/script/internal/context.py +46 -15
- sonolus/script/internal/impl.py +9 -3
- sonolus/script/internal/introspection.py +8 -1
- sonolus/script/internal/math_impls.py +17 -0
- sonolus/script/internal/native.py +5 -5
- sonolus/script/internal/range.py +14 -17
- sonolus/script/internal/simulation_context.py +1 -1
- sonolus/script/internal/transient.py +2 -2
- sonolus/script/internal/value.py +42 -4
- sonolus/script/interval.py +15 -15
- sonolus/script/iterator.py +38 -107
- sonolus/script/maybe.py +139 -0
- sonolus/script/num.py +30 -15
- sonolus/script/options.py +1 -1
- sonolus/script/particle.py +1 -1
- sonolus/script/pointer.py +1 -1
- sonolus/script/project.py +24 -5
- sonolus/script/quad.py +15 -15
- sonolus/script/record.py +21 -12
- sonolus/script/runtime.py +22 -18
- sonolus/script/sprite.py +1 -1
- sonolus/script/stream.py +69 -85
- sonolus/script/transform.py +35 -34
- sonolus/script/values.py +10 -10
- sonolus/script/vec.py +23 -20
- {sonolus_py-0.3.3.dist-info → sonolus_py-0.4.0.dist-info}/METADATA +1 -1
- sonolus_py-0.4.0.dist-info/RECORD +93 -0
- sonolus_py-0.3.3.dist-info/RECORD +0 -92
- {sonolus_py-0.3.3.dist-info → sonolus_py-0.4.0.dist-info}/WHEEL +0 -0
- {sonolus_py-0.3.3.dist-info → sonolus_py-0.4.0.dist-info}/entry_points.txt +0 -0
- {sonolus_py-0.3.3.dist-info → sonolus_py-0.4.0.dist-info}/licenses/LICENSE +0 -0
sonolus/backend/visitor.py
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
# ruff: noqa: N802
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
2
4
|
import ast
|
|
3
5
|
import builtins
|
|
4
6
|
import functools
|
|
5
7
|
import inspect
|
|
6
|
-
from collections.abc import Callable, Sequence
|
|
8
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
7
9
|
from inspect import ismethod
|
|
8
10
|
from types import FunctionType, MethodType, MethodWrapperType
|
|
9
|
-
from typing import Any, Never
|
|
11
|
+
from typing import Any, Never
|
|
10
12
|
|
|
11
13
|
from sonolus.backend.excepthook import install_excepthook
|
|
12
|
-
from sonolus.backend.utils import get_function, scan_writes
|
|
14
|
+
from sonolus.backend.utils import get_function, has_yield, scan_writes
|
|
13
15
|
from sonolus.script.debug import assert_true
|
|
14
16
|
from sonolus.script.internal.builtin_impls import BUILTIN_IMPLS, _bool, _float, _int, _len
|
|
15
17
|
from sonolus.script.internal.constant import ConstantValue
|
|
16
|
-
from sonolus.script.internal.context import Context, EmptyBinding, Scope, ValueBinding, ctx, set_ctx
|
|
18
|
+
from sonolus.script.internal.context import Context, EmptyBinding, Scope, ValueBinding, ctx, set_ctx, using_ctx
|
|
17
19
|
from sonolus.script.internal.descriptor import SonolusDescriptor
|
|
18
20
|
from sonolus.script.internal.error import CompilationError
|
|
19
|
-
from sonolus.script.internal.impl import validate_value
|
|
21
|
+
from sonolus.script.internal.impl import meta_fn, validate_value
|
|
22
|
+
from sonolus.script.internal.transient import TransientValue
|
|
20
23
|
from sonolus.script.internal.tuple_impl import TupleImpl
|
|
21
24
|
from sonolus.script.internal.value import Value
|
|
22
25
|
from sonolus.script.iterator import SonolusIterator
|
|
26
|
+
from sonolus.script.maybe import Maybe
|
|
23
27
|
from sonolus.script.num import Num, _is_num
|
|
28
|
+
from sonolus.script.record import Record
|
|
24
29
|
|
|
25
30
|
_compiler_internal_ = True
|
|
26
31
|
|
|
@@ -31,6 +36,30 @@ def compile_and_call[**P, R](fn: Callable[P, R], /, *args: P.args, **kwargs: P.k
|
|
|
31
36
|
return validate_value(generate_fn_impl(fn)(*args, **kwargs))
|
|
32
37
|
|
|
33
38
|
|
|
39
|
+
def compile_and_call_at_definition[**P, R](fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
40
|
+
if not ctx():
|
|
41
|
+
return fn(*args, **kwargs)
|
|
42
|
+
source_file, node = get_function(fn)
|
|
43
|
+
location_args = {
|
|
44
|
+
"lineno": node.lineno,
|
|
45
|
+
"col_offset": node.col_offset,
|
|
46
|
+
"end_lineno": node.lineno,
|
|
47
|
+
"end_col_offset": node.col_offset + 1,
|
|
48
|
+
}
|
|
49
|
+
expr = ast.Expression(
|
|
50
|
+
body=ast.Call(
|
|
51
|
+
func=ast.Name(id="fn", ctx=ast.Load(), **location_args),
|
|
52
|
+
args=[],
|
|
53
|
+
keywords=[],
|
|
54
|
+
**location_args,
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
return eval(
|
|
58
|
+
compile(expr, filename=source_file, mode="eval"),
|
|
59
|
+
{"fn": lambda: compile_and_call(fn, *args, **kwargs), "_filter_traceback_": True, "_traceback_root_": True},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
34
63
|
def generate_fn_impl(fn: Callable):
|
|
35
64
|
install_excepthook()
|
|
36
65
|
match fn:
|
|
@@ -189,15 +218,18 @@ class Visitor(ast.NodeVisitor):
|
|
|
189
218
|
Context | list[Context]
|
|
190
219
|
] # Contexts at loop heads, from outer to inner. Contains a list for unrolled (tuple) loops
|
|
191
220
|
break_ctxs: list[list[Context]] # Contexts at break statements, from outer to inner
|
|
192
|
-
|
|
193
|
-
|
|
221
|
+
yield_ctxs: list[Context] # Contexts at yield statements, which will branch to the exit
|
|
222
|
+
resume_ctxs: list[Context] # Contexts after yield statements
|
|
223
|
+
active_ctx: Context | None # The active context for use in nested functions
|
|
224
|
+
parent: Visitor | None # The parent visitor for use in nested functions
|
|
225
|
+
used_parent_binding_values: dict[str, Value] # Values of parent bindings used in this function
|
|
194
226
|
|
|
195
227
|
def __init__(
|
|
196
228
|
self,
|
|
197
229
|
source_file: str,
|
|
198
230
|
bound_args: inspect.BoundArguments,
|
|
199
231
|
global_vars: dict[str, Any],
|
|
200
|
-
parent:
|
|
232
|
+
parent: Visitor | None = None,
|
|
201
233
|
):
|
|
202
234
|
self.source_file = source_file
|
|
203
235
|
self.globals = global_vars
|
|
@@ -206,12 +238,16 @@ class Visitor(ast.NodeVisitor):
|
|
|
206
238
|
self.return_ctxs = []
|
|
207
239
|
self.loop_head_ctxs = []
|
|
208
240
|
self.break_ctxs = []
|
|
241
|
+
self.yield_ctxs = []
|
|
242
|
+
self.resume_ctxs = []
|
|
209
243
|
self.active_ctx = None
|
|
210
244
|
self.parent = parent
|
|
245
|
+
self.used_parent_binding_values = {}
|
|
211
246
|
|
|
212
247
|
def run(self, node):
|
|
213
248
|
before_ctx = ctx()
|
|
214
|
-
|
|
249
|
+
start_ctx = before_ctx.branch_with_scope(None, Scope())
|
|
250
|
+
set_ctx(start_ctx)
|
|
215
251
|
for name, value in self.bound_args.arguments.items():
|
|
216
252
|
ctx().scope.set_value(name, validate_value(value))
|
|
217
253
|
match node:
|
|
@@ -224,8 +260,61 @@ class Visitor(ast.NodeVisitor):
|
|
|
224
260
|
case ast.Lambda(body=body):
|
|
225
261
|
result = self.visit(body)
|
|
226
262
|
ctx().scope.set_value("$return", result)
|
|
263
|
+
case ast.GeneratorExp(elt=elt, generators=generators):
|
|
264
|
+
self.construct_genexpr(generators, elt)
|
|
265
|
+
ctx().scope.set_value("$return", validate_value(None))
|
|
227
266
|
case _:
|
|
228
267
|
raise NotImplementedError("Unsupported syntax")
|
|
268
|
+
if has_yield(node) or isinstance(node, ast.GeneratorExp):
|
|
269
|
+
return_ctx = Context.meet([*self.return_ctxs, ctx()])
|
|
270
|
+
result_binding = return_ctx.scope.get_binding("$return")
|
|
271
|
+
if not isinstance(result_binding, ValueBinding):
|
|
272
|
+
raise ValueError("Function has conflicting return values")
|
|
273
|
+
if not result_binding.value._is_py_() and result_binding.value._as_py_() is not None:
|
|
274
|
+
raise ValueError("Generator function return statements must return None")
|
|
275
|
+
with using_ctx(start_ctx):
|
|
276
|
+
state_var = Num._alloc_()
|
|
277
|
+
is_present_var = Num._alloc_()
|
|
278
|
+
with using_ctx(before_ctx):
|
|
279
|
+
state_var._set_(0)
|
|
280
|
+
with using_ctx(return_ctx):
|
|
281
|
+
state_var._set_(len(self.return_ctxs) + 1)
|
|
282
|
+
is_present_var._set_(0)
|
|
283
|
+
del before_ctx.outgoing[None] # Unlink the state machine body from the call site
|
|
284
|
+
entry = before_ctx.new_empty_disconnected()
|
|
285
|
+
entry.test = state_var.ir()
|
|
286
|
+
for i, tgt in enumerate([start_ctx, *self.resume_ctxs]):
|
|
287
|
+
entry.outgoing[i] = tgt
|
|
288
|
+
entry.outgoing[None] = return_ctx
|
|
289
|
+
yield_between_ctxs = []
|
|
290
|
+
for i, out in enumerate(self.yield_ctxs, start=1):
|
|
291
|
+
between = out.branch(None)
|
|
292
|
+
with using_ctx(between):
|
|
293
|
+
state_var._set_(i)
|
|
294
|
+
yield_between_ctxs.append(between)
|
|
295
|
+
if yield_between_ctxs:
|
|
296
|
+
yield_merge_ctx = Context.meet(yield_between_ctxs)
|
|
297
|
+
else:
|
|
298
|
+
yield_merge_ctx = before_ctx.new_empty_disconnected()
|
|
299
|
+
# Making it default to a number for convenience when used with stuff like min, etc.
|
|
300
|
+
yield_merge_ctx.scope.set_value("$yield", validate_value(0))
|
|
301
|
+
yield_binding = yield_merge_ctx.scope.get_binding("$yield")
|
|
302
|
+
if not isinstance(yield_binding, ValueBinding):
|
|
303
|
+
raise ValueError("Function has conflicting yield values")
|
|
304
|
+
with using_ctx(yield_merge_ctx):
|
|
305
|
+
is_present_var._set_(1)
|
|
306
|
+
next_result_ctx = Context.meet([yield_merge_ctx, return_ctx])
|
|
307
|
+
set_ctx(before_ctx)
|
|
308
|
+
return_test = Num._alloc_()
|
|
309
|
+
next_result_ctx.test = return_test.ir()
|
|
310
|
+
return Generator(
|
|
311
|
+
return_test,
|
|
312
|
+
entry,
|
|
313
|
+
next_result_ctx,
|
|
314
|
+
Maybe(present=is_present_var, value=yield_binding.value),
|
|
315
|
+
self.used_parent_binding_values,
|
|
316
|
+
self,
|
|
317
|
+
)
|
|
229
318
|
after_ctx = Context.meet([*self.return_ctxs, ctx()])
|
|
230
319
|
self.active_ctx = after_ctx
|
|
231
320
|
result_binding = after_ctx.scope.get_binding("$return")
|
|
@@ -234,6 +323,59 @@ class Visitor(ast.NodeVisitor):
|
|
|
234
323
|
set_ctx(after_ctx.branch_with_scope(None, before_ctx.scope.copy()))
|
|
235
324
|
return result_binding.value
|
|
236
325
|
|
|
326
|
+
def construct_genexpr(self, generators: Iterable[ast.comprehension], elt: ast.expr):
|
|
327
|
+
if not generators:
|
|
328
|
+
# Note that there may effectively be multiple yields in an expression since
|
|
329
|
+
# tuples are unrolled.
|
|
330
|
+
value = self.visit(elt)
|
|
331
|
+
ctx().scope.set_value("$yield", validate_value(value))
|
|
332
|
+
self.yield_ctxs.append(ctx())
|
|
333
|
+
resume_ctx = ctx().new_disconnected()
|
|
334
|
+
self.resume_ctxs.append(resume_ctx)
|
|
335
|
+
set_ctx(resume_ctx)
|
|
336
|
+
return
|
|
337
|
+
generator, *others = generators
|
|
338
|
+
iterable = self.visit(generator.iter)
|
|
339
|
+
if isinstance(iterable, TupleImpl):
|
|
340
|
+
for value in iterable.value:
|
|
341
|
+
set_ctx(ctx().branch(None))
|
|
342
|
+
self.handle_assign(generator.target, validate_value(value))
|
|
343
|
+
self.construct_genexpr(others, elt)
|
|
344
|
+
else:
|
|
345
|
+
iterator = self.handle_call(generator.iter, iterable.__iter__)
|
|
346
|
+
if not isinstance(iterator, SonolusIterator):
|
|
347
|
+
raise ValueError("Unsupported iterator")
|
|
348
|
+
header_ctx = ctx().branch(None)
|
|
349
|
+
set_ctx(header_ctx)
|
|
350
|
+
next_value = self.handle_call(generator.iter, iterator.next)
|
|
351
|
+
if not isinstance(next_value, Maybe):
|
|
352
|
+
raise ValueError("Iterator next must return a Maybe")
|
|
353
|
+
if next_value._present._is_py_() and not next_value._present._as_py_():
|
|
354
|
+
# This will never run
|
|
355
|
+
return
|
|
356
|
+
ctx().test = next_value._present.ir()
|
|
357
|
+
body_ctx = ctx().branch(None)
|
|
358
|
+
else_ctx = ctx().branch(0)
|
|
359
|
+
set_ctx(body_ctx)
|
|
360
|
+
self.handle_assign(generator.target, next_value._value)
|
|
361
|
+
for if_expr in generator.ifs:
|
|
362
|
+
test = self.convert_to_boolean_num(if_expr, self.visit(if_expr))
|
|
363
|
+
if test._is_py_():
|
|
364
|
+
if test._as_py_():
|
|
365
|
+
continue
|
|
366
|
+
else:
|
|
367
|
+
ctx().outgoing[None] = header_ctx
|
|
368
|
+
set_ctx(ctx().into_dead())
|
|
369
|
+
else:
|
|
370
|
+
if_then_ctx = ctx().branch(None)
|
|
371
|
+
if_else_ctx = ctx().branch(0)
|
|
372
|
+
ctx().test = test.ir()
|
|
373
|
+
if_else_ctx.outgoing[None] = header_ctx
|
|
374
|
+
set_ctx(if_then_ctx)
|
|
375
|
+
self.construct_genexpr(others, elt)
|
|
376
|
+
ctx().outgoing[None] = header_ctx
|
|
377
|
+
set_ctx(else_ctx)
|
|
378
|
+
|
|
237
379
|
def visit(self, node):
|
|
238
380
|
"""Visit a node."""
|
|
239
381
|
# We want this here so this is filtered out of tracebacks
|
|
@@ -359,8 +501,10 @@ class Visitor(ast.NodeVisitor):
|
|
|
359
501
|
self.loop_head_ctxs.append(header_ctx)
|
|
360
502
|
self.break_ctxs.append([])
|
|
361
503
|
set_ctx(header_ctx)
|
|
362
|
-
|
|
363
|
-
if
|
|
504
|
+
next_value = self.handle_call(node, iterator.next)
|
|
505
|
+
if not isinstance(next_value, Maybe):
|
|
506
|
+
raise ValueError("Iterator next must return a Maybe")
|
|
507
|
+
if next_value._present._is_py_() and not next_value._present._as_py_():
|
|
364
508
|
# The loop will never run, continue after evaluating the condition
|
|
365
509
|
self.loop_head_ctxs.pop()
|
|
366
510
|
self.break_ctxs.pop()
|
|
@@ -369,12 +513,12 @@ class Visitor(ast.NodeVisitor):
|
|
|
369
513
|
break
|
|
370
514
|
self.visit(stmt)
|
|
371
515
|
return
|
|
372
|
-
ctx().test =
|
|
516
|
+
ctx().test = next_value._present.ir()
|
|
373
517
|
body_ctx = ctx().branch(None)
|
|
374
518
|
else_ctx = ctx().branch(0)
|
|
375
519
|
|
|
376
520
|
set_ctx(body_ctx)
|
|
377
|
-
self.handle_assign(node.target,
|
|
521
|
+
self.handle_assign(node.target, next_value._value)
|
|
378
522
|
for stmt in node.body:
|
|
379
523
|
if not ctx().live:
|
|
380
524
|
break
|
|
@@ -811,16 +955,48 @@ class Visitor(ast.NodeVisitor):
|
|
|
811
955
|
raise NotImplementedError("Dict comprehensions are not supported")
|
|
812
956
|
|
|
813
957
|
def visit_GeneratorExp(self, node):
|
|
814
|
-
|
|
958
|
+
self.active_ctx = ctx()
|
|
959
|
+
return Visitor(self.source_file, inspect.Signature([]).bind(), self.globals, self).run(node)
|
|
815
960
|
|
|
816
961
|
def visit_Await(self, node):
|
|
817
962
|
raise NotImplementedError("Await expressions are not supported")
|
|
818
963
|
|
|
819
964
|
def visit_Yield(self, node):
|
|
820
|
-
|
|
965
|
+
value = self.visit(node.value) if node.value else validate_value(None)
|
|
966
|
+
ctx().scope.set_value("$yield", value)
|
|
967
|
+
self.yield_ctxs.append(ctx())
|
|
968
|
+
resume_ctx = ctx().new_disconnected()
|
|
969
|
+
self.resume_ctxs.append(resume_ctx)
|
|
970
|
+
set_ctx(resume_ctx)
|
|
971
|
+
return validate_value(None) # send() is unsupported, so yield returns None
|
|
821
972
|
|
|
822
973
|
def visit_YieldFrom(self, node):
|
|
823
|
-
|
|
974
|
+
value = self.visit(node.value)
|
|
975
|
+
if isinstance(value, TupleImpl):
|
|
976
|
+
for entry in value.value:
|
|
977
|
+
ctx().scope.set_value("$yield", validate_value(entry))
|
|
978
|
+
self.yield_ctxs.append(ctx())
|
|
979
|
+
resume_ctx = ctx().new_disconnected()
|
|
980
|
+
self.resume_ctxs.append(resume_ctx)
|
|
981
|
+
set_ctx(resume_ctx)
|
|
982
|
+
return validate_value(None)
|
|
983
|
+
iterator = self.handle_call(node, value.__iter__)
|
|
984
|
+
if not isinstance(iterator, SonolusIterator):
|
|
985
|
+
raise ValueError("Expected a SonolusIterator")
|
|
986
|
+
header = ctx().branch(None)
|
|
987
|
+
set_ctx(header)
|
|
988
|
+
result = self.handle_call(node, iterator.next)
|
|
989
|
+
nothing_branch = ctx().branch(0)
|
|
990
|
+
some_branch = ctx().branch(None)
|
|
991
|
+
ctx().test = result._present.ir()
|
|
992
|
+
set_ctx(some_branch)
|
|
993
|
+
ctx().scope.set_value("$yield", result._value)
|
|
994
|
+
self.yield_ctxs.append(ctx())
|
|
995
|
+
resume_ctx = ctx().new_disconnected()
|
|
996
|
+
self.resume_ctxs.append(resume_ctx)
|
|
997
|
+
resume_ctx.outgoing[None] = header
|
|
998
|
+
set_ctx(nothing_branch)
|
|
999
|
+
return validate_value(None)
|
|
824
1000
|
|
|
825
1001
|
def _has_real_method(self, obj: Value, method_name: str) -> bool:
|
|
826
1002
|
return hasattr(obj, method_name) and not isinstance(getattr(obj, method_name), MethodWrapperType)
|
|
@@ -929,11 +1105,16 @@ class Visitor(ast.NodeVisitor):
|
|
|
929
1105
|
raise NotImplementedError("Starred expressions are not supported")
|
|
930
1106
|
|
|
931
1107
|
def visit_Name(self, node):
|
|
1108
|
+
if node.id in self.used_parent_binding_values:
|
|
1109
|
+
return self.used_parent_binding_values[node.id]
|
|
932
1110
|
self.active_ctx = ctx()
|
|
933
1111
|
v = self
|
|
934
1112
|
while v:
|
|
935
1113
|
if not isinstance(v.active_ctx.scope.get_binding(node.id), EmptyBinding):
|
|
936
|
-
|
|
1114
|
+
result = v.active_ctx.scope.get_value(node.id)
|
|
1115
|
+
if v is not self:
|
|
1116
|
+
self.used_parent_binding_values[node.id] = result
|
|
1117
|
+
return result
|
|
937
1118
|
v = v.parent
|
|
938
1119
|
if node.id in self.globals:
|
|
939
1120
|
value = self.globals[node.id]
|
|
@@ -1141,6 +1322,9 @@ class Visitor(ast.NodeVisitor):
|
|
|
1141
1322
|
if length._is_py_():
|
|
1142
1323
|
return Num._accept_(length._as_py_() > 0)
|
|
1143
1324
|
return length > Num._accept_(0)
|
|
1325
|
+
if isinstance(value, Record):
|
|
1326
|
+
return Num._accept_(1)
|
|
1327
|
+
# Not allowing other types to default to truthy for now in case there's any edge cases.
|
|
1144
1328
|
raise TypeError(f"Converting {type(value).__name__} to bool is not supported")
|
|
1145
1329
|
|
|
1146
1330
|
def arguments_to_signature(self, arguments: ast.arguments) -> inspect.Signature:
|
|
@@ -1269,3 +1453,62 @@ class ReportingErrorsAtNode:
|
|
|
1269
1453
|
|
|
1270
1454
|
if exc_value is not None:
|
|
1271
1455
|
self.compiler.raise_exception_at_node(self.node, exc_value)
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
class Generator(TransientValue, SonolusIterator):
|
|
1459
|
+
def __init__(
|
|
1460
|
+
self,
|
|
1461
|
+
return_test: Num,
|
|
1462
|
+
entry: Context,
|
|
1463
|
+
exit_: Context,
|
|
1464
|
+
value: Maybe,
|
|
1465
|
+
used_bindings: dict[str, Value],
|
|
1466
|
+
parent: Visitor,
|
|
1467
|
+
):
|
|
1468
|
+
self.i = 0
|
|
1469
|
+
self.return_test = return_test
|
|
1470
|
+
self.entry = entry
|
|
1471
|
+
self.exit = exit_
|
|
1472
|
+
self.value = value
|
|
1473
|
+
self.used_bindings = used_bindings
|
|
1474
|
+
self.parent = parent
|
|
1475
|
+
|
|
1476
|
+
@meta_fn
|
|
1477
|
+
def next(self):
|
|
1478
|
+
self._validate_bindings()
|
|
1479
|
+
self.return_test._set_(self.i)
|
|
1480
|
+
after_ctx = ctx().new_disconnected()
|
|
1481
|
+
ctx().outgoing[None] = self.entry
|
|
1482
|
+
self.exit.outgoing[self.i] = after_ctx
|
|
1483
|
+
self.i += 1
|
|
1484
|
+
set_ctx(after_ctx)
|
|
1485
|
+
return self.value
|
|
1486
|
+
|
|
1487
|
+
def _validate_bindings(self):
|
|
1488
|
+
for key, value in self.used_bindings.items():
|
|
1489
|
+
v = self.parent
|
|
1490
|
+
while v:
|
|
1491
|
+
if not isinstance(v.active_ctx.scope.get_binding(key), EmptyBinding):
|
|
1492
|
+
result = v.active_ctx.scope.get_value(key)
|
|
1493
|
+
if result is not value:
|
|
1494
|
+
raise ValueError(f"Binding '{key}' has been modified since the generator was created")
|
|
1495
|
+
v = v.parent
|
|
1496
|
+
|
|
1497
|
+
def __iter__(self):
|
|
1498
|
+
return self
|
|
1499
|
+
|
|
1500
|
+
@classmethod
|
|
1501
|
+
def _accepts_(cls, value: Any) -> bool:
|
|
1502
|
+
return isinstance(value, cls)
|
|
1503
|
+
|
|
1504
|
+
@classmethod
|
|
1505
|
+
def _accept_(cls, value: Any) -> Generator:
|
|
1506
|
+
if not cls._accepts_(value):
|
|
1507
|
+
raise TypeError(f"Cannot accept value of type {type(value).__name__} as {cls.__name__}")
|
|
1508
|
+
return value
|
|
1509
|
+
|
|
1510
|
+
def _is_py_(self) -> bool:
|
|
1511
|
+
return False
|
|
1512
|
+
|
|
1513
|
+
def _as_py_(self) -> Any:
|
|
1514
|
+
raise NotImplementedError
|
sonolus/build/cli.py
CHANGED
|
@@ -10,10 +10,12 @@ import sys
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from time import perf_counter
|
|
12
12
|
|
|
13
|
+
from sonolus.backend.excepthook import print_simple_traceback
|
|
13
14
|
from sonolus.backend.optimize.optimize import FAST_PASSES, MINIMAL_PASSES, STANDARD_PASSES
|
|
14
|
-
from sonolus.build.engine import no_gil, package_engine
|
|
15
|
+
from sonolus.build.engine import no_gil, package_engine, validate_engine
|
|
15
16
|
from sonolus.build.level import package_level_data
|
|
16
17
|
from sonolus.build.project import build_project_to_collection, get_project_schema
|
|
18
|
+
from sonolus.script.internal.error import CompilationError
|
|
17
19
|
from sonolus.script.project import BuildConfig, Project
|
|
18
20
|
|
|
19
21
|
|
|
@@ -81,7 +83,11 @@ def build_project(project: Project, build_dir: Path, config: BuildConfig):
|
|
|
81
83
|
level_path.write_bytes(package_level_data(level.data))
|
|
82
84
|
|
|
83
85
|
|
|
84
|
-
def
|
|
86
|
+
def validate_project(project: Project, config: BuildConfig):
|
|
87
|
+
validate_engine(project.engine.data, config)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def build_collection(project: Project, build_dir: Path, config: BuildConfig | None):
|
|
85
91
|
site_dir = build_dir / "site"
|
|
86
92
|
shutil.rmtree(site_dir, ignore_errors=True)
|
|
87
93
|
site_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -204,6 +210,15 @@ def main():
|
|
|
204
210
|
help="Module path (e.g., 'module.name'). If omitted, will auto-detect if only one module exists.",
|
|
205
211
|
)
|
|
206
212
|
|
|
213
|
+
check_parser = subparsers.add_parser("check")
|
|
214
|
+
check_parser.add_argument(
|
|
215
|
+
"module",
|
|
216
|
+
type=str,
|
|
217
|
+
nargs="?",
|
|
218
|
+
help="Module path (e.g., 'module.name'). If omitted, will auto-detect if only one module exists.",
|
|
219
|
+
)
|
|
220
|
+
add_common_arguments(check_parser)
|
|
221
|
+
|
|
207
222
|
args = parser.parse_args()
|
|
208
223
|
|
|
209
224
|
if not args.module:
|
|
@@ -220,24 +235,37 @@ def main():
|
|
|
220
235
|
if hasattr(sys, "_jit") and sys._jit.is_enabled():
|
|
221
236
|
print("Python JIT is enabled")
|
|
222
237
|
|
|
238
|
+
start_time = perf_counter()
|
|
223
239
|
project = import_project(args.module)
|
|
240
|
+
end_time = perf_counter()
|
|
224
241
|
if project is None:
|
|
225
242
|
sys.exit(1)
|
|
243
|
+
print(f"Project imported in {end_time - start_time:.2f}s")
|
|
226
244
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
245
|
+
try:
|
|
246
|
+
if args.command == "build":
|
|
247
|
+
build_dir = Path(args.build_dir)
|
|
248
|
+
start_time = perf_counter()
|
|
249
|
+
config = get_config(args)
|
|
250
|
+
build_project(project, build_dir, config)
|
|
251
|
+
end_time = perf_counter()
|
|
252
|
+
print(f"Project built successfully to '{build_dir.resolve()}' in {end_time - start_time:.2f}s")
|
|
253
|
+
elif args.command == "dev":
|
|
254
|
+
build_dir = Path(args.build_dir)
|
|
255
|
+
start_time = perf_counter()
|
|
256
|
+
config = get_config(args)
|
|
257
|
+
build_collection(project, build_dir, config)
|
|
258
|
+
end_time = perf_counter()
|
|
259
|
+
print(f"Build finished in {end_time - start_time:.2f}s")
|
|
260
|
+
run_server(build_dir / "site", port=args.port)
|
|
261
|
+
elif args.command == "schema":
|
|
262
|
+
print(json.dumps(get_project_schema(project), indent=2))
|
|
263
|
+
elif args.command == "check":
|
|
264
|
+
start_time = perf_counter()
|
|
265
|
+
config = get_config(args)
|
|
266
|
+
validate_project(project, config)
|
|
267
|
+
end_time = perf_counter()
|
|
268
|
+
print(f"Project validation completed successfully in {end_time - start_time:.2f}s")
|
|
269
|
+
except CompilationError:
|
|
270
|
+
exc_info = sys.exc_info()
|
|
271
|
+
print_simple_traceback(*exc_info)
|
sonolus/build/compile.py
CHANGED
|
@@ -7,8 +7,8 @@ from sonolus.backend.mode import Mode
|
|
|
7
7
|
from sonolus.backend.ops import Op
|
|
8
8
|
from sonolus.backend.optimize.flow import BasicBlock
|
|
9
9
|
from sonolus.backend.optimize.optimize import STANDARD_PASSES
|
|
10
|
-
from sonolus.backend.optimize.passes import CompilerPass, run_passes
|
|
11
|
-
from sonolus.backend.visitor import
|
|
10
|
+
from sonolus.backend.optimize.passes import CompilerPass, OptimizerConfig, run_passes
|
|
11
|
+
from sonolus.backend.visitor import compile_and_call_at_definition
|
|
12
12
|
from sonolus.build.node import OutputNodeGenerator
|
|
13
13
|
from sonolus.script.archetype import _BaseArchetype
|
|
14
14
|
from sonolus.script.internal.callbacks import CallbackInfo
|
|
@@ -31,6 +31,7 @@ def compile_mode(
|
|
|
31
31
|
global_callbacks: list[tuple[CallbackInfo, Callable]] | None,
|
|
32
32
|
passes: Sequence[CompilerPass] | None = None,
|
|
33
33
|
thread_pool: Executor | None = None,
|
|
34
|
+
validate_only: bool = False,
|
|
34
35
|
) -> dict:
|
|
35
36
|
if passes is None:
|
|
36
37
|
passes = STANDARD_PASSES
|
|
@@ -55,7 +56,13 @@ def compile_mode(
|
|
|
55
56
|
- (cb_info.name, {"index": node_index, "order": cb_order}) for archetype callbacks.
|
|
56
57
|
"""
|
|
57
58
|
cfg = callback_to_cfg(global_state, cb, cb_info.name, arch)
|
|
58
|
-
|
|
59
|
+
if validate_only:
|
|
60
|
+
if arch is not None:
|
|
61
|
+
cb_order = getattr(cb, "_callback_order_", 0)
|
|
62
|
+
return cb_info.name, {"index": 0, "order": cb_order}
|
|
63
|
+
else:
|
|
64
|
+
return cb_info.name, 0
|
|
65
|
+
cfg = run_passes(cfg, passes, OptimizerConfig(mode=mode, callback=cb_info.name))
|
|
59
66
|
node = cfg_to_engine_node(cfg)
|
|
60
67
|
node_index = nodes.add(node)
|
|
61
68
|
|
|
@@ -137,9 +144,9 @@ def callback_to_cfg(
|
|
|
137
144
|
context = Context(global_state, callback_state)
|
|
138
145
|
with using_ctx(context):
|
|
139
146
|
if archetype is not None:
|
|
140
|
-
result =
|
|
147
|
+
result = compile_and_call_at_definition(callback, archetype._for_compilation())
|
|
141
148
|
else:
|
|
142
|
-
result =
|
|
149
|
+
result = compile_and_call_at_definition(callback)
|
|
143
150
|
if _is_num(result):
|
|
144
151
|
ctx().add_statements(IRInstr(Op.Break, [IRConst(1), result.ir()]))
|
|
145
152
|
return context_to_cfg(context)
|
sonolus/build/engine.py
CHANGED
|
@@ -6,7 +6,6 @@ from collections.abc import Callable
|
|
|
6
6
|
from concurrent.futures import Executor
|
|
7
7
|
from concurrent.futures.thread import ThreadPoolExecutor
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
-
from os import process_cpu_count
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
|
|
12
11
|
from sonolus.backend.mode import Mode
|
|
@@ -74,6 +73,9 @@ def package_engine(
|
|
|
74
73
|
rom = ReadOnlyMemory()
|
|
75
74
|
configuration = build_engine_configuration(engine.options, engine.ui)
|
|
76
75
|
if no_gil():
|
|
76
|
+
# process_cpu_count is available in Python 3.13+
|
|
77
|
+
from os import process_cpu_count
|
|
78
|
+
|
|
77
79
|
thread_pool = ThreadPoolExecutor(process_cpu_count() or 1)
|
|
78
80
|
else:
|
|
79
81
|
thread_pool = None
|
|
@@ -189,6 +191,65 @@ def package_engine(
|
|
|
189
191
|
)
|
|
190
192
|
|
|
191
193
|
|
|
194
|
+
def validate_engine(
|
|
195
|
+
engine: EngineData,
|
|
196
|
+
config: BuildConfig | None = None,
|
|
197
|
+
):
|
|
198
|
+
config = config or BuildConfig()
|
|
199
|
+
rom = ReadOnlyMemory()
|
|
200
|
+
|
|
201
|
+
play_mode = engine.play if config.build_play else empty_play_mode()
|
|
202
|
+
watch_mode = engine.watch if config.build_watch else empty_watch_mode()
|
|
203
|
+
preview_mode = engine.preview if config.build_preview else empty_preview_mode()
|
|
204
|
+
tutorial_mode = engine.tutorial if config.build_tutorial else empty_tutorial_mode()
|
|
205
|
+
|
|
206
|
+
build_play_mode(
|
|
207
|
+
archetypes=play_mode.archetypes,
|
|
208
|
+
skin=play_mode.skin,
|
|
209
|
+
effects=play_mode.effects,
|
|
210
|
+
particles=play_mode.particles,
|
|
211
|
+
buckets=play_mode.buckets,
|
|
212
|
+
rom=rom,
|
|
213
|
+
config=config,
|
|
214
|
+
thread_pool=None,
|
|
215
|
+
validate_only=True,
|
|
216
|
+
)
|
|
217
|
+
build_watch_mode(
|
|
218
|
+
archetypes=watch_mode.archetypes,
|
|
219
|
+
skin=watch_mode.skin,
|
|
220
|
+
effects=watch_mode.effects,
|
|
221
|
+
particles=watch_mode.particles,
|
|
222
|
+
buckets=watch_mode.buckets,
|
|
223
|
+
rom=rom,
|
|
224
|
+
update_spawn=watch_mode.update_spawn,
|
|
225
|
+
config=config,
|
|
226
|
+
thread_pool=None,
|
|
227
|
+
validate_only=True,
|
|
228
|
+
)
|
|
229
|
+
build_preview_mode(
|
|
230
|
+
archetypes=preview_mode.archetypes,
|
|
231
|
+
skin=preview_mode.skin,
|
|
232
|
+
rom=rom,
|
|
233
|
+
config=config,
|
|
234
|
+
thread_pool=None,
|
|
235
|
+
validate_only=True,
|
|
236
|
+
)
|
|
237
|
+
build_tutorial_mode(
|
|
238
|
+
skin=tutorial_mode.skin,
|
|
239
|
+
effects=tutorial_mode.effects,
|
|
240
|
+
particles=tutorial_mode.particles,
|
|
241
|
+
instructions=tutorial_mode.instructions,
|
|
242
|
+
instruction_icons=tutorial_mode.instruction_icons,
|
|
243
|
+
preprocess=tutorial_mode.preprocess,
|
|
244
|
+
navigate=tutorial_mode.navigate,
|
|
245
|
+
update=tutorial_mode.update,
|
|
246
|
+
rom=rom,
|
|
247
|
+
config=config,
|
|
248
|
+
thread_pool=None,
|
|
249
|
+
validate_only=True,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
192
253
|
def build_engine_configuration(
|
|
193
254
|
options: Options,
|
|
194
255
|
ui: UiConfig,
|
|
@@ -208,6 +269,7 @@ def build_play_mode(
|
|
|
208
269
|
rom: ReadOnlyMemory,
|
|
209
270
|
config: BuildConfig,
|
|
210
271
|
thread_pool: Executor | None = None,
|
|
272
|
+
validate_only: bool = False,
|
|
211
273
|
):
|
|
212
274
|
return {
|
|
213
275
|
**compile_mode(
|
|
@@ -217,6 +279,7 @@ def build_play_mode(
|
|
|
217
279
|
global_callbacks=None,
|
|
218
280
|
passes=config.passes,
|
|
219
281
|
thread_pool=thread_pool,
|
|
282
|
+
validate_only=validate_only,
|
|
220
283
|
),
|
|
221
284
|
"skin": build_skin(skin),
|
|
222
285
|
"effect": build_effects(effects),
|
|
@@ -235,6 +298,7 @@ def build_watch_mode(
|
|
|
235
298
|
update_spawn: Callable[[], float],
|
|
236
299
|
config: BuildConfig,
|
|
237
300
|
thread_pool: Executor | None = None,
|
|
301
|
+
validate_only: bool = False,
|
|
238
302
|
):
|
|
239
303
|
return {
|
|
240
304
|
**compile_mode(
|
|
@@ -244,6 +308,7 @@ def build_watch_mode(
|
|
|
244
308
|
global_callbacks=[(update_spawn_callback, update_spawn)],
|
|
245
309
|
passes=config.passes,
|
|
246
310
|
thread_pool=thread_pool,
|
|
311
|
+
validate_only=validate_only,
|
|
247
312
|
),
|
|
248
313
|
"skin": build_skin(skin),
|
|
249
314
|
"effect": build_effects(effects),
|
|
@@ -258,6 +323,7 @@ def build_preview_mode(
|
|
|
258
323
|
rom: ReadOnlyMemory,
|
|
259
324
|
config: BuildConfig,
|
|
260
325
|
thread_pool: Executor | None = None,
|
|
326
|
+
validate_only: bool = False,
|
|
261
327
|
):
|
|
262
328
|
return {
|
|
263
329
|
**compile_mode(
|
|
@@ -267,6 +333,7 @@ def build_preview_mode(
|
|
|
267
333
|
global_callbacks=None,
|
|
268
334
|
passes=config.passes,
|
|
269
335
|
thread_pool=thread_pool,
|
|
336
|
+
validate_only=validate_only,
|
|
270
337
|
),
|
|
271
338
|
"skin": build_skin(skin),
|
|
272
339
|
}
|
|
@@ -284,6 +351,7 @@ def build_tutorial_mode(
|
|
|
284
351
|
rom: ReadOnlyMemory,
|
|
285
352
|
config: BuildConfig,
|
|
286
353
|
thread_pool: Executor | None = None,
|
|
354
|
+
validate_only: bool = False,
|
|
287
355
|
):
|
|
288
356
|
return {
|
|
289
357
|
**compile_mode(
|
|
@@ -297,6 +365,7 @@ def build_tutorial_mode(
|
|
|
297
365
|
],
|
|
298
366
|
passes=config.passes,
|
|
299
367
|
thread_pool=thread_pool,
|
|
368
|
+
validate_only=validate_only,
|
|
300
369
|
),
|
|
301
370
|
"skin": build_skin(skin),
|
|
302
371
|
"effect": build_effects(effects),
|