sonolus.py 0.3.4__py3-none-any.whl → 0.4.1__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 +44 -16
- sonolus/backend/visitor.py +288 -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 +12 -9
- sonolus/script/array.py +23 -18
- sonolus/script/array_like.py +26 -29
- sonolus/script/bucket.py +1 -1
- sonolus/script/containers.py +22 -26
- sonolus/script/debug.py +20 -43
- sonolus/script/effect.py +1 -1
- sonolus/script/globals.py +3 -3
- sonolus/script/instruction.py +2 -2
- 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/native.py +2 -2
- sonolus/script/internal/range.py +8 -11
- sonolus/script/internal/simulation_context.py +1 -1
- sonolus/script/internal/transient.py +2 -2
- sonolus/script/internal/value.py +41 -3
- sonolus/script/interval.py +13 -13
- sonolus/script/iterator.py +53 -107
- sonolus/script/level.py +2 -2
- sonolus/script/maybe.py +241 -0
- sonolus/script/num.py +29 -14
- sonolus/script/options.py +1 -1
- sonolus/script/particle.py +1 -1
- sonolus/script/project.py +24 -5
- sonolus/script/quad.py +15 -15
- sonolus/script/record.py +48 -44
- sonolus/script/runtime.py +22 -18
- sonolus/script/sprite.py +1 -1
- sonolus/script/stream.py +66 -82
- sonolus/script/transform.py +35 -34
- sonolus/script/values.py +10 -10
- sonolus/script/vec.py +21 -18
- {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.1.dist-info}/METADATA +1 -1
- sonolus_py-0.4.1.dist-info/RECORD +93 -0
- sonolus_py-0.3.4.dist-info/RECORD +0 -92
- {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.1.dist-info}/WHEEL +0 -0
- {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.1.dist-info}/entry_points.txt +0 -0
- {sonolus_py-0.3.4.dist-info → sonolus_py-0.4.1.dist-info}/licenses/LICENSE +0 -0
sonolus/backend/utils.py
CHANGED
|
@@ -23,29 +23,33 @@ def get_tree_from_file(file: str | Path) -> ast.Module:
|
|
|
23
23
|
class FindFunction(ast.NodeVisitor):
|
|
24
24
|
def __init__(self, line):
|
|
25
25
|
self.line = line
|
|
26
|
-
self.
|
|
26
|
+
self.results: list[ast.FunctionDef | ast.Lambda] = []
|
|
27
27
|
|
|
28
28
|
def visit_FunctionDef(self, node: ast.FunctionDef):
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
):
|
|
32
|
-
self.node = node
|
|
33
|
-
else:
|
|
34
|
-
self.generic_visit(node)
|
|
29
|
+
self.results.append(node)
|
|
30
|
+
self.generic_visit(node)
|
|
35
31
|
|
|
36
32
|
def visit_Lambda(self, node: ast.Lambda):
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
raise ValueError("Multiple functions defined on the same line are not supported")
|
|
40
|
-
self.node = node
|
|
41
|
-
else:
|
|
42
|
-
self.generic_visit(node)
|
|
33
|
+
self.results.append(node)
|
|
34
|
+
self.generic_visit(node)
|
|
43
35
|
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
@cache
|
|
38
|
+
def get_functions(tree: ast.Module) -> list[ast.FunctionDef | ast.Lambda]:
|
|
39
|
+
visitor = FindFunction(0)
|
|
47
40
|
visitor.visit(tree)
|
|
48
|
-
return visitor.
|
|
41
|
+
return visitor.results
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def find_function(tree: ast.Module, line: int):
|
|
45
|
+
for node in get_functions(tree):
|
|
46
|
+
if node.lineno == line or (
|
|
47
|
+
isinstance(node, ast.FunctionDef)
|
|
48
|
+
and node.decorator_list
|
|
49
|
+
and (node.decorator_list[-1].end_lineno <= line <= node.lineno)
|
|
50
|
+
):
|
|
51
|
+
return node
|
|
52
|
+
raise ValueError("Function not found")
|
|
49
53
|
|
|
50
54
|
|
|
51
55
|
class ScanWrites(ast.NodeVisitor):
|
|
@@ -61,3 +65,27 @@ def scan_writes(node: ast.AST) -> set[str]:
|
|
|
61
65
|
visitor = ScanWrites()
|
|
62
66
|
visitor.visit(node)
|
|
63
67
|
return set(visitor.writes)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class HasDirectYield(ast.NodeVisitor):
|
|
71
|
+
def __init__(self):
|
|
72
|
+
self.started = False
|
|
73
|
+
self.has_yield = False
|
|
74
|
+
|
|
75
|
+
def visit_Yield(self, node: ast.Yield):
|
|
76
|
+
self.has_yield = True
|
|
77
|
+
|
|
78
|
+
def visit_YieldFrom(self, node: ast.YieldFrom):
|
|
79
|
+
self.has_yield = True
|
|
80
|
+
|
|
81
|
+
def visit_FunctionDef(self, node: ast.FunctionDef):
|
|
82
|
+
if self.started:
|
|
83
|
+
return
|
|
84
|
+
self.started = True
|
|
85
|
+
self.generic_visit(node)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def has_yield(node: ast.AST) -> bool:
|
|
89
|
+
visitor = HasDirectYield()
|
|
90
|
+
visitor.visit(node)
|
|
91
|
+
return visitor.has_yield
|
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,75 @@ 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
|
+
first_generator = generators[0]
|
|
265
|
+
iterable = self.visit(first_generator.iter)
|
|
266
|
+
if isinstance(iterable, TupleImpl):
|
|
267
|
+
initial_iterator = iterable
|
|
268
|
+
else:
|
|
269
|
+
if not hasattr(iterable, "__iter__"):
|
|
270
|
+
raise TypeError(f"Object of type '{type(iterable).__name__}' is not iterable")
|
|
271
|
+
initial_iterator = self.handle_call(first_generator.iter, iterable.__iter__)
|
|
272
|
+
if not isinstance(initial_iterator, SonolusIterator):
|
|
273
|
+
raise ValueError("Unsupported iterator")
|
|
274
|
+
# The initial iterator is evaluated eagerly in Python
|
|
275
|
+
before_ctx = ctx().branch_with_scope(None, before_ctx.scope.copy())
|
|
276
|
+
start_ctx = before_ctx.branch_with_scope(None, Scope())
|
|
277
|
+
set_ctx(start_ctx)
|
|
278
|
+
self.construct_genexpr(generators, elt, initial_iterator)
|
|
279
|
+
ctx().scope.set_value("$return", validate_value(None))
|
|
227
280
|
case _:
|
|
228
281
|
raise NotImplementedError("Unsupported syntax")
|
|
282
|
+
if has_yield(node) or isinstance(node, ast.GeneratorExp):
|
|
283
|
+
return_ctx = Context.meet([*self.return_ctxs, ctx()])
|
|
284
|
+
result_binding = return_ctx.scope.get_binding("$return")
|
|
285
|
+
if not isinstance(result_binding, ValueBinding):
|
|
286
|
+
raise ValueError("Function has conflicting return values")
|
|
287
|
+
if not result_binding.value._is_py_() and result_binding.value._as_py_() is not None:
|
|
288
|
+
raise ValueError("Generator function return statements must return None")
|
|
289
|
+
with using_ctx(start_ctx):
|
|
290
|
+
state_var = Num._alloc_()
|
|
291
|
+
is_present_var = Num._alloc_()
|
|
292
|
+
with using_ctx(before_ctx):
|
|
293
|
+
state_var._set_(0)
|
|
294
|
+
with using_ctx(return_ctx):
|
|
295
|
+
state_var._set_(len(self.return_ctxs) + 1)
|
|
296
|
+
is_present_var._set_(0)
|
|
297
|
+
del before_ctx.outgoing[None] # Unlink the state machine body from the call site
|
|
298
|
+
entry = before_ctx.new_empty_disconnected()
|
|
299
|
+
entry.test = state_var.ir()
|
|
300
|
+
for i, tgt in enumerate([start_ctx, *self.resume_ctxs]):
|
|
301
|
+
entry.outgoing[i] = tgt
|
|
302
|
+
entry.outgoing[None] = return_ctx
|
|
303
|
+
yield_between_ctxs = []
|
|
304
|
+
for i, out in enumerate(self.yield_ctxs, start=1):
|
|
305
|
+
between = out.branch(None)
|
|
306
|
+
with using_ctx(between):
|
|
307
|
+
state_var._set_(i)
|
|
308
|
+
yield_between_ctxs.append(between)
|
|
309
|
+
if yield_between_ctxs:
|
|
310
|
+
yield_merge_ctx = Context.meet(yield_between_ctxs)
|
|
311
|
+
else:
|
|
312
|
+
yield_merge_ctx = before_ctx.new_empty_disconnected()
|
|
313
|
+
# Making it default to a number for convenience when used with stuff like min, etc.
|
|
314
|
+
yield_merge_ctx.scope.set_value("$yield", validate_value(0))
|
|
315
|
+
yield_binding = yield_merge_ctx.scope.get_binding("$yield")
|
|
316
|
+
if not isinstance(yield_binding, ValueBinding):
|
|
317
|
+
raise ValueError("Function has conflicting yield values")
|
|
318
|
+
with using_ctx(yield_merge_ctx):
|
|
319
|
+
is_present_var._set_(1)
|
|
320
|
+
next_result_ctx = Context.meet([yield_merge_ctx, return_ctx])
|
|
321
|
+
set_ctx(before_ctx)
|
|
322
|
+
return_test = Num._alloc_()
|
|
323
|
+
next_result_ctx.test = return_test.ir()
|
|
324
|
+
return Generator(
|
|
325
|
+
return_test,
|
|
326
|
+
entry,
|
|
327
|
+
next_result_ctx,
|
|
328
|
+
Maybe(present=is_present_var, value=yield_binding.value),
|
|
329
|
+
self.used_parent_binding_values,
|
|
330
|
+
self,
|
|
331
|
+
)
|
|
229
332
|
after_ctx = Context.meet([*self.return_ctxs, ctx()])
|
|
230
333
|
self.active_ctx = after_ctx
|
|
231
334
|
result_binding = after_ctx.scope.get_binding("$return")
|
|
@@ -234,6 +337,69 @@ class Visitor(ast.NodeVisitor):
|
|
|
234
337
|
set_ctx(after_ctx.branch_with_scope(None, before_ctx.scope.copy()))
|
|
235
338
|
return result_binding.value
|
|
236
339
|
|
|
340
|
+
def construct_genexpr(
|
|
341
|
+
self, generators: Iterable[ast.comprehension], elt: ast.expr, initial_iterator: Value | None = None
|
|
342
|
+
):
|
|
343
|
+
if not generators:
|
|
344
|
+
# Note that there may effectively be multiple yields in an expression since
|
|
345
|
+
# tuples are unrolled.
|
|
346
|
+
value = self.visit(elt)
|
|
347
|
+
ctx().scope.set_value("$yield", validate_value(value))
|
|
348
|
+
self.yield_ctxs.append(ctx())
|
|
349
|
+
resume_ctx = ctx().new_disconnected()
|
|
350
|
+
self.resume_ctxs.append(resume_ctx)
|
|
351
|
+
set_ctx(resume_ctx)
|
|
352
|
+
return
|
|
353
|
+
generator, *others = generators
|
|
354
|
+
if initial_iterator is not None:
|
|
355
|
+
iterable = initial_iterator
|
|
356
|
+
else:
|
|
357
|
+
iterable = self.visit(generator.iter)
|
|
358
|
+
if isinstance(iterable, TupleImpl):
|
|
359
|
+
for value in iterable.value:
|
|
360
|
+
set_ctx(ctx().branch(None))
|
|
361
|
+
self.handle_assign(generator.target, validate_value(value))
|
|
362
|
+
self.construct_genexpr(others, elt)
|
|
363
|
+
else:
|
|
364
|
+
if initial_iterator is not None:
|
|
365
|
+
iterator = initial_iterator
|
|
366
|
+
else:
|
|
367
|
+
if not hasattr(iterable, "__iter__"):
|
|
368
|
+
raise TypeError(f"Object of type '{type(iterable).__name__}' is not iterable")
|
|
369
|
+
iterator = self.handle_call(generator.iter, iterable.__iter__)
|
|
370
|
+
if not isinstance(iterator, SonolusIterator):
|
|
371
|
+
raise ValueError("Unsupported iterator")
|
|
372
|
+
header_ctx = ctx().branch(None)
|
|
373
|
+
set_ctx(header_ctx)
|
|
374
|
+
next_value = self.handle_call(generator.iter, iterator.next)
|
|
375
|
+
if not isinstance(next_value, Maybe):
|
|
376
|
+
raise ValueError("Iterator next must return a Maybe")
|
|
377
|
+
if next_value._present._is_py_() and not next_value._present._as_py_():
|
|
378
|
+
# This will never run
|
|
379
|
+
return
|
|
380
|
+
ctx().test = next_value._present.ir()
|
|
381
|
+
body_ctx = ctx().branch(None)
|
|
382
|
+
else_ctx = ctx().branch(0)
|
|
383
|
+
set_ctx(body_ctx)
|
|
384
|
+
self.handle_assign(generator.target, next_value._value)
|
|
385
|
+
for if_expr in generator.ifs:
|
|
386
|
+
test = self.convert_to_boolean_num(if_expr, self.visit(if_expr))
|
|
387
|
+
if test._is_py_():
|
|
388
|
+
if test._as_py_():
|
|
389
|
+
continue
|
|
390
|
+
else:
|
|
391
|
+
ctx().outgoing[None] = header_ctx
|
|
392
|
+
set_ctx(ctx().into_dead())
|
|
393
|
+
else:
|
|
394
|
+
if_then_ctx = ctx().branch(None)
|
|
395
|
+
if_else_ctx = ctx().branch(0)
|
|
396
|
+
ctx().test = test.ir()
|
|
397
|
+
if_else_ctx.outgoing[None] = header_ctx
|
|
398
|
+
set_ctx(if_then_ctx)
|
|
399
|
+
self.construct_genexpr(others, elt)
|
|
400
|
+
ctx().outgoing[None] = header_ctx
|
|
401
|
+
set_ctx(else_ctx)
|
|
402
|
+
|
|
237
403
|
def visit(self, node):
|
|
238
404
|
"""Visit a node."""
|
|
239
405
|
# We want this here so this is filtered out of tracebacks
|
|
@@ -351,6 +517,8 @@ class Visitor(ast.NodeVisitor):
|
|
|
351
517
|
if break_ctxs:
|
|
352
518
|
set_ctx(Context.meet([*break_ctxs, ctx()]))
|
|
353
519
|
return
|
|
520
|
+
if not hasattr(iterable, "__iter__"):
|
|
521
|
+
raise TypeError(f"Object of type '{type(iterable).__name__}' is not iterable")
|
|
354
522
|
iterator = self.handle_call(node, iterable.__iter__)
|
|
355
523
|
if not isinstance(iterator, SonolusIterator):
|
|
356
524
|
raise ValueError("Unsupported iterator")
|
|
@@ -359,8 +527,10 @@ class Visitor(ast.NodeVisitor):
|
|
|
359
527
|
self.loop_head_ctxs.append(header_ctx)
|
|
360
528
|
self.break_ctxs.append([])
|
|
361
529
|
set_ctx(header_ctx)
|
|
362
|
-
|
|
363
|
-
if
|
|
530
|
+
next_value = self.handle_call(node, iterator.next)
|
|
531
|
+
if not isinstance(next_value, Maybe):
|
|
532
|
+
raise ValueError("Iterator next must return a Maybe")
|
|
533
|
+
if next_value._present._is_py_() and not next_value._present._as_py_():
|
|
364
534
|
# The loop will never run, continue after evaluating the condition
|
|
365
535
|
self.loop_head_ctxs.pop()
|
|
366
536
|
self.break_ctxs.pop()
|
|
@@ -369,12 +539,12 @@ class Visitor(ast.NodeVisitor):
|
|
|
369
539
|
break
|
|
370
540
|
self.visit(stmt)
|
|
371
541
|
return
|
|
372
|
-
ctx().test =
|
|
542
|
+
ctx().test = next_value._present.ir()
|
|
373
543
|
body_ctx = ctx().branch(None)
|
|
374
544
|
else_ctx = ctx().branch(0)
|
|
375
545
|
|
|
376
546
|
set_ctx(body_ctx)
|
|
377
|
-
self.handle_assign(node.target,
|
|
547
|
+
self.handle_assign(node.target, next_value._value)
|
|
378
548
|
for stmt in node.body:
|
|
379
549
|
if not ctx().live:
|
|
380
550
|
break
|
|
@@ -811,16 +981,50 @@ class Visitor(ast.NodeVisitor):
|
|
|
811
981
|
raise NotImplementedError("Dict comprehensions are not supported")
|
|
812
982
|
|
|
813
983
|
def visit_GeneratorExp(self, node):
|
|
814
|
-
|
|
984
|
+
self.active_ctx = ctx()
|
|
985
|
+
return Visitor(self.source_file, inspect.Signature([]).bind(), self.globals, self).run(node)
|
|
815
986
|
|
|
816
987
|
def visit_Await(self, node):
|
|
817
988
|
raise NotImplementedError("Await expressions are not supported")
|
|
818
989
|
|
|
819
990
|
def visit_Yield(self, node):
|
|
820
|
-
|
|
991
|
+
value = self.visit(node.value) if node.value else validate_value(None)
|
|
992
|
+
ctx().scope.set_value("$yield", value)
|
|
993
|
+
self.yield_ctxs.append(ctx())
|
|
994
|
+
resume_ctx = ctx().new_disconnected()
|
|
995
|
+
self.resume_ctxs.append(resume_ctx)
|
|
996
|
+
set_ctx(resume_ctx)
|
|
997
|
+
return validate_value(None) # send() is unsupported, so yield returns None
|
|
821
998
|
|
|
822
999
|
def visit_YieldFrom(self, node):
|
|
823
|
-
|
|
1000
|
+
value = self.visit(node.value)
|
|
1001
|
+
if isinstance(value, TupleImpl):
|
|
1002
|
+
for entry in value.value:
|
|
1003
|
+
ctx().scope.set_value("$yield", validate_value(entry))
|
|
1004
|
+
self.yield_ctxs.append(ctx())
|
|
1005
|
+
resume_ctx = ctx().new_disconnected()
|
|
1006
|
+
self.resume_ctxs.append(resume_ctx)
|
|
1007
|
+
set_ctx(resume_ctx)
|
|
1008
|
+
return validate_value(None)
|
|
1009
|
+
if not hasattr(value, "__iter__"):
|
|
1010
|
+
raise TypeError(f"Object of type '{type(value).__name__}' is not iterable")
|
|
1011
|
+
iterator = self.handle_call(node, value.__iter__)
|
|
1012
|
+
if not isinstance(iterator, SonolusIterator):
|
|
1013
|
+
raise ValueError("Expected a SonolusIterator")
|
|
1014
|
+
header = ctx().branch(None)
|
|
1015
|
+
set_ctx(header)
|
|
1016
|
+
result = self.handle_call(node, iterator.next)
|
|
1017
|
+
nothing_branch = ctx().branch(0)
|
|
1018
|
+
some_branch = ctx().branch(None)
|
|
1019
|
+
ctx().test = result._present.ir()
|
|
1020
|
+
set_ctx(some_branch)
|
|
1021
|
+
ctx().scope.set_value("$yield", result._value)
|
|
1022
|
+
self.yield_ctxs.append(ctx())
|
|
1023
|
+
resume_ctx = ctx().new_disconnected()
|
|
1024
|
+
self.resume_ctxs.append(resume_ctx)
|
|
1025
|
+
resume_ctx.outgoing[None] = header
|
|
1026
|
+
set_ctx(nothing_branch)
|
|
1027
|
+
return validate_value(None)
|
|
824
1028
|
|
|
825
1029
|
def _has_real_method(self, obj: Value, method_name: str) -> bool:
|
|
826
1030
|
return hasattr(obj, method_name) and not isinstance(getattr(obj, method_name), MethodWrapperType)
|
|
@@ -929,11 +1133,16 @@ class Visitor(ast.NodeVisitor):
|
|
|
929
1133
|
raise NotImplementedError("Starred expressions are not supported")
|
|
930
1134
|
|
|
931
1135
|
def visit_Name(self, node):
|
|
1136
|
+
if node.id in self.used_parent_binding_values:
|
|
1137
|
+
return self.used_parent_binding_values[node.id]
|
|
932
1138
|
self.active_ctx = ctx()
|
|
933
1139
|
v = self
|
|
934
1140
|
while v:
|
|
935
1141
|
if not isinstance(v.active_ctx.scope.get_binding(node.id), EmptyBinding):
|
|
936
|
-
|
|
1142
|
+
result = v.active_ctx.scope.get_value(node.id)
|
|
1143
|
+
if v is not self:
|
|
1144
|
+
self.used_parent_binding_values[node.id] = result
|
|
1145
|
+
return result
|
|
937
1146
|
v = v.parent
|
|
938
1147
|
if node.id in self.globals:
|
|
939
1148
|
value = self.globals[node.id]
|
|
@@ -1141,6 +1350,9 @@ class Visitor(ast.NodeVisitor):
|
|
|
1141
1350
|
if length._is_py_():
|
|
1142
1351
|
return Num._accept_(length._as_py_() > 0)
|
|
1143
1352
|
return length > Num._accept_(0)
|
|
1353
|
+
if isinstance(value, Record):
|
|
1354
|
+
return Num._accept_(1)
|
|
1355
|
+
# Not allowing other types to default to truthy for now in case there's any edge cases.
|
|
1144
1356
|
raise TypeError(f"Converting {type(value).__name__} to bool is not supported")
|
|
1145
1357
|
|
|
1146
1358
|
def arguments_to_signature(self, arguments: ast.arguments) -> inspect.Signature:
|
|
@@ -1269,3 +1481,62 @@ class ReportingErrorsAtNode:
|
|
|
1269
1481
|
|
|
1270
1482
|
if exc_value is not None:
|
|
1271
1483
|
self.compiler.raise_exception_at_node(self.node, exc_value)
|
|
1484
|
+
|
|
1485
|
+
|
|
1486
|
+
class Generator(TransientValue, SonolusIterator):
|
|
1487
|
+
def __init__(
|
|
1488
|
+
self,
|
|
1489
|
+
return_test: Num,
|
|
1490
|
+
entry: Context,
|
|
1491
|
+
exit_: Context,
|
|
1492
|
+
value: Maybe,
|
|
1493
|
+
used_bindings: dict[str, Value],
|
|
1494
|
+
parent: Visitor,
|
|
1495
|
+
):
|
|
1496
|
+
self.i = 0
|
|
1497
|
+
self.return_test = return_test
|
|
1498
|
+
self.entry = entry
|
|
1499
|
+
self.exit = exit_
|
|
1500
|
+
self.value = value
|
|
1501
|
+
self.used_bindings = used_bindings
|
|
1502
|
+
self.parent = parent
|
|
1503
|
+
|
|
1504
|
+
@meta_fn
|
|
1505
|
+
def next(self):
|
|
1506
|
+
self._validate_bindings()
|
|
1507
|
+
self.return_test._set_(self.i)
|
|
1508
|
+
after_ctx = ctx().new_disconnected()
|
|
1509
|
+
ctx().outgoing[None] = self.entry
|
|
1510
|
+
self.exit.outgoing[self.i] = after_ctx
|
|
1511
|
+
self.i += 1
|
|
1512
|
+
set_ctx(after_ctx)
|
|
1513
|
+
return self.value
|
|
1514
|
+
|
|
1515
|
+
def _validate_bindings(self):
|
|
1516
|
+
for key, value in self.used_bindings.items():
|
|
1517
|
+
v = self.parent
|
|
1518
|
+
while v:
|
|
1519
|
+
if not isinstance(v.active_ctx.scope.get_binding(key), EmptyBinding):
|
|
1520
|
+
result = v.active_ctx.scope.get_value(key)
|
|
1521
|
+
if result is not value:
|
|
1522
|
+
raise ValueError(f"Binding '{key}' has been modified since the generator was created")
|
|
1523
|
+
v = v.parent
|
|
1524
|
+
|
|
1525
|
+
def __iter__(self):
|
|
1526
|
+
return self
|
|
1527
|
+
|
|
1528
|
+
@classmethod
|
|
1529
|
+
def _accepts_(cls, value: Any) -> bool:
|
|
1530
|
+
return isinstance(value, cls)
|
|
1531
|
+
|
|
1532
|
+
@classmethod
|
|
1533
|
+
def _accept_(cls, value: Any) -> Generator:
|
|
1534
|
+
if not cls._accepts_(value):
|
|
1535
|
+
raise TypeError(f"Cannot accept value of type {type(value).__name__} as {cls.__name__}")
|
|
1536
|
+
return value
|
|
1537
|
+
|
|
1538
|
+
def _is_py_(self) -> bool:
|
|
1539
|
+
return False
|
|
1540
|
+
|
|
1541
|
+
def _as_py_(self) -> Any:
|
|
1542
|
+
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)
|