sonolus.py 0.3.1__py3-none-any.whl → 0.3.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sonolus.py might be problematic. Click here for more details.

Files changed (36) hide show
  1. sonolus/backend/finalize.py +16 -4
  2. sonolus/backend/node.py +13 -5
  3. sonolus/backend/optimize/allocate.py +41 -4
  4. sonolus/backend/optimize/flow.py +24 -7
  5. sonolus/backend/optimize/optimize.py +2 -9
  6. sonolus/backend/utils.py +6 -1
  7. sonolus/backend/visitor.py +72 -23
  8. sonolus/build/cli.py +6 -1
  9. sonolus/build/engine.py +1 -1
  10. sonolus/script/archetype.py +52 -24
  11. sonolus/script/array.py +20 -8
  12. sonolus/script/array_like.py +30 -3
  13. sonolus/script/containers.py +27 -7
  14. sonolus/script/debug.py +66 -8
  15. sonolus/script/globals.py +17 -0
  16. sonolus/script/internal/builtin_impls.py +12 -8
  17. sonolus/script/internal/context.py +55 -1
  18. sonolus/script/internal/range.py +25 -2
  19. sonolus/script/internal/simulation_context.py +131 -0
  20. sonolus/script/internal/tuple_impl.py +18 -11
  21. sonolus/script/interval.py +60 -2
  22. sonolus/script/iterator.py +3 -2
  23. sonolus/script/num.py +11 -2
  24. sonolus/script/options.py +24 -1
  25. sonolus/script/quad.py +41 -3
  26. sonolus/script/record.py +24 -3
  27. sonolus/script/runtime.py +411 -0
  28. sonolus/script/stream.py +133 -16
  29. sonolus/script/transform.py +291 -2
  30. sonolus/script/values.py +9 -3
  31. sonolus/script/vec.py +14 -2
  32. {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/METADATA +1 -1
  33. {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/RECORD +36 -35
  34. {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/WHEEL +0 -0
  35. {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/entry_points.txt +0 -0
  36. {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,3 +1,5 @@
1
+ from math import isfinite, isinf, isnan
2
+
1
3
  from sonolus.backend.ir import IRConst, IRGet, IRInstr, IRPureInstr, IRSet
2
4
  from sonolus.backend.node import ConstantNode, EngineNode, FunctionNode
3
5
  from sonolus.backend.ops import Op
@@ -54,10 +56,20 @@ def cfg_to_engine_node(entry: BasicBlock):
54
56
 
55
57
  def ir_to_engine_node(stmt) -> EngineNode:
56
58
  match stmt:
57
- case int() | float():
58
- return ConstantNode(value=float(stmt))
59
- case IRConst(value=int(value) | float(value)):
60
- return ConstantNode(value=value)
59
+ case int(value) | float(value) | IRConst(value=int(value) | float(value)):
60
+ value = float(value)
61
+ if value.is_integer():
62
+ return ConstantNode(value=int(value))
63
+ elif isfinite(value):
64
+ return ConstantNode(value=value)
65
+ elif isinf(value):
66
+ # Read values from ROM
67
+ return FunctionNode(Op.Get, args=[ConstantNode(value=3000), ConstantNode(value=1 if value > 0 else 2)])
68
+ elif isnan(value):
69
+ # Read value from ROM
70
+ return FunctionNode(Op.Get, args=[ConstantNode(value=3000), ConstantNode(value=0)])
71
+ else:
72
+ raise ValueError(f"Invalid constant value: {value}")
61
73
  case IRPureInstr(op=op, args=args) | IRInstr(op=op, args=args):
62
74
  return FunctionNode(func=op, args=[ir_to_engine_node(arg) for arg in args])
63
75
  case IRGet(place=place):
sonolus/backend/node.py CHANGED
@@ -1,26 +1,34 @@
1
1
  import textwrap
2
- from dataclasses import dataclass
2
+ from dataclasses import dataclass, field
3
3
 
4
4
  from sonolus.backend.ops import Op
5
5
 
6
6
  type EngineNode = ConstantNode | FunctionNode
7
7
 
8
8
 
9
- @dataclass
9
+ @dataclass(slots=True)
10
10
  class ConstantNode:
11
11
  value: float
12
+ _hash: int = field(init=False, repr=False)
13
+
14
+ def __post_init__(self):
15
+ self._hash = hash(self.value)
12
16
 
13
17
  def __hash__(self):
14
18
  return hash(self.value)
15
19
 
16
20
 
17
- @dataclass
21
+ @dataclass(slots=True)
18
22
  class FunctionNode:
19
23
  func: Op
20
24
  args: list[EngineNode]
25
+ _hash: int = field(init=False, repr=False)
26
+
27
+ def __post_init__(self):
28
+ self._hash = hash((self.func, tuple(self.args)))
21
29
 
22
30
  def __hash__(self):
23
- return hash((self.func, tuple(self.args)))
31
+ return self._hash
24
32
 
25
33
 
26
34
  def format_engine_node(node: EngineNode) -> str:
@@ -34,7 +42,7 @@ def format_engine_node(node: EngineNode) -> str:
34
42
  return f"{node.func.name}({format_engine_node(node.args[0])})"
35
43
  case _:
36
44
  return f"{node.func.name}(\n{
37
- textwrap.indent("\n".join(format_engine_node(arg) for arg in node.args), " ")
45
+ textwrap.indent('\n'.join(format_engine_node(arg) for arg in node.args), ' ')
38
46
  }\n)"
39
47
  else:
40
48
  raise ValueError(f"Invalid engine node: {node}")
@@ -65,7 +65,8 @@ class Allocate(CompilerPass):
65
65
  def run(self, entry: BasicBlock):
66
66
  mapping = self.get_mapping(entry)
67
67
  for block in traverse_cfg_preorder(entry):
68
- block.statements = [self.update_stmt(statement, mapping) for statement in block.statements]
68
+ updated_statements = [self.update_stmt(statement, mapping) for statement in block.statements]
69
+ block.statements = [stmt for stmt in updated_statements if stmt is not None]
69
70
  block.test = self.update_stmt(block.test, mapping)
70
71
  return entry
71
72
 
@@ -80,7 +81,19 @@ class Allocate(CompilerPass):
80
81
  case IRGet(place=place):
81
82
  return IRGet(place=self.update_stmt(place, mapping))
82
83
  case IRSet(place=place, value=value):
83
- return IRSet(place=self.update_stmt(place, mapping), value=self.update_stmt(value, mapping))
84
+ # Do some dead code elimination here which is pretty much free since we already have liveness analysis,
85
+ # and prevents an error from the dead block place being missing from the mapping.
86
+ live = get_live(stmt)
87
+ is_live = not (
88
+ (isinstance(place, BlockPlace) and isinstance(place.block, TempBlock) and place.block not in live)
89
+ or (isinstance(value, IRGet) and place == value.place)
90
+ )
91
+ if is_live:
92
+ return IRSet(place=self.update_stmt(place, mapping), value=self.update_stmt(value, mapping))
93
+ elif isinstance(value, IRInstr) and value.op.side_effects:
94
+ return self.update_stmt(value, mapping)
95
+ else:
96
+ return None
84
97
  case BlockPlace(block=block, index=index, offset=offset):
85
98
  if isinstance(block, TempBlock):
86
99
  if block.size == 0:
@@ -119,8 +132,32 @@ class Allocate(CompilerPass):
119
132
  def get_interference(self, entry: BasicBlock) -> dict[TempBlock, set[TempBlock]]:
120
133
  result = {}
121
134
  for block in traverse_cfg_preorder(entry):
122
- for stmt in [*block.statements, block.test]:
135
+ for stmt in block.statements:
136
+ if not isinstance(stmt, IRSet):
137
+ continue
123
138
  live = {p for p in get_live(stmt) if isinstance(p, TempBlock) and p.size > 0}
124
139
  for place in live:
125
- result.setdefault(place, set()).update(live - {place})
140
+ if place not in result:
141
+ result[place] = set(live)
142
+ else:
143
+ result[place].update(live)
126
144
  return result
145
+
146
+
147
+ class AllocateFast(Allocate):
148
+ """A bit faster than Allocate but a bit less optimal."""
149
+
150
+ def get_mapping(self, entry: BasicBlock) -> dict[TempBlock, int]:
151
+ interference = self.get_interference(entry)
152
+ offsets: dict[TempBlock, int] = dict.fromkeys(interference, 0)
153
+ end_offsets: dict[TempBlock, int] = dict.fromkeys(interference, 0)
154
+
155
+ for block, others in interference.items():
156
+ size = block.size
157
+ offset = max((end_offsets[other] for other in others), default=0)
158
+ if offset + size > TEMP_SIZE:
159
+ raise ValueError("Temporary memory limit exceeded")
160
+ offsets[block] = offset
161
+ end_offsets[block] = offset + size
162
+
163
+ return offsets
@@ -91,12 +91,29 @@ def cfg_to_mermaid(entry: BasicBlock):
91
91
 
92
92
  lines = ["Entry([Entry]) --> 0"]
93
93
  for block, index in block_indexes.items():
94
- lines.append(f"{index}[{pre(fmt([f'#{index}', *(
95
- f"{dst} := phi({", ".join(f"{block_indexes.get(src_block, "<dead>")}: {src_place}"
96
- for src_block, src_place
97
- in sorted(phis.items(), key=lambda x: block_indexes.get(x[0])))})"
98
- for dst, phis in block.phis.items()
99
- ), *block.statements]))}]")
94
+ lines.append(
95
+ f"{index}[{
96
+ pre(
97
+ fmt(
98
+ [
99
+ f'#{index}',
100
+ *(
101
+ f'{dst} := phi({
102
+ ", ".join(
103
+ f"{block_indexes.get(src_block, '<dead>')}: {src_place}"
104
+ for src_block, src_place in sorted(
105
+ phis.items(), key=lambda x: block_indexes.get(x[0])
106
+ )
107
+ )
108
+ })'
109
+ for dst, phis in block.phis.items()
110
+ ),
111
+ *block.statements,
112
+ ]
113
+ )
114
+ )
115
+ }]"
116
+ )
100
117
 
101
118
  outgoing = {edge.cond: edge.dst for edge in block.outgoing}
102
119
  match outgoing:
@@ -114,7 +131,7 @@ def cfg_to_mermaid(entry: BasicBlock):
114
131
  lines.append(f"{index} --> {index}_")
115
132
  for cond, target in tgt.items():
116
133
  lines.append(
117
- f"{index}_ --> |{pre(fmt([cond if cond is not None else "default"]))}| {block_indexes[target]}"
134
+ f"{index}_ --> |{pre(fmt([cond if cond is not None else 'default']))}| {block_indexes[target]}"
118
135
  )
119
136
  lines.append("Exit([Exit])")
120
137
 
@@ -1,4 +1,4 @@
1
- from sonolus.backend.optimize.allocate import Allocate, AllocateBasic
1
+ from sonolus.backend.optimize.allocate import Allocate, AllocateBasic, AllocateFast
2
2
  from sonolus.backend.optimize.constant_evaluation import SparseConditionalConstantPropagation
3
3
  from sonolus.backend.optimize.copy_coalesce import CopyCoalesce
4
4
  from sonolus.backend.optimize.dead_code import (
@@ -6,9 +6,7 @@ from sonolus.backend.optimize.dead_code import (
6
6
  DeadCodeElimination,
7
7
  UnreachableCodeElimination,
8
8
  )
9
- from sonolus.backend.optimize.flow import BasicBlock
10
9
  from sonolus.backend.optimize.inlining import InlineVars
11
- from sonolus.backend.optimize.passes import run_passes
12
10
  from sonolus.backend.optimize.simplify import CoalesceFlow, NormalizeSwitch, RewriteToSwitch
13
11
  from sonolus.backend.optimize.ssa import FromSSA, ToSSA
14
12
 
@@ -21,9 +19,8 @@ MINIMAL_PASSES = (
21
19
  FAST_PASSES = (
22
20
  CoalesceFlow(),
23
21
  UnreachableCodeElimination(),
24
- AdvancedDeadCodeElimination(),
22
+ AllocateFast(), # Does dead code elimination too, so no need for a separate pass
25
23
  CoalesceFlow(),
26
- Allocate(),
27
24
  )
28
25
 
29
26
  STANDARD_PASSES = (
@@ -46,7 +43,3 @@ STANDARD_PASSES = (
46
43
  NormalizeSwitch(),
47
44
  Allocate(),
48
45
  )
49
-
50
-
51
- def optimize_and_allocate(cfg: BasicBlock):
52
- return run_passes(cfg, STANDARD_PASSES)
sonolus/backend/utils.py CHANGED
@@ -11,10 +11,15 @@ def get_function(fn: Callable) -> tuple[str, ast.FunctionDef]:
11
11
  # This preserves both line number and column number in the returned node
12
12
  source_file = inspect.getsourcefile(fn)
13
13
  _, start_line = inspect.getsourcelines(fn)
14
- base_tree = ast.parse(Path(source_file).read_text(encoding="utf-8"))
14
+ base_tree = get_tree_from_file(source_file)
15
15
  return source_file, find_function(base_tree, start_line)
16
16
 
17
17
 
18
+ @cache
19
+ def get_tree_from_file(file: str | Path) -> ast.Module:
20
+ return ast.parse(Path(file).read_text(encoding="utf-8"))
21
+
22
+
18
23
  class FindFunction(ast.NodeVisitor):
19
24
  def __init__(self, line):
20
25
  self.line = line
@@ -4,6 +4,7 @@ import builtins
4
4
  import functools
5
5
  import inspect
6
6
  from collections.abc import Callable, Sequence
7
+ from inspect import ismethod
7
8
  from types import FunctionType, MethodType, MethodWrapperType
8
9
  from typing import Any, Never, Self
9
10
 
@@ -54,10 +55,22 @@ def eval_fn(fn: Callable, /, *args, **kwargs):
54
55
  source_file, node = get_function(fn)
55
56
  bound_args = inspect.signature(fn).bind(*args, **kwargs)
56
57
  bound_args.apply_defaults()
58
+ if ismethod(fn):
59
+ code = fn.__func__.__code__
60
+ closure = fn.__func__.__closure__
61
+ else:
62
+ code = fn.__code__
63
+ closure = fn.__closure__
64
+ if closure is None:
65
+ nonlocal_vars = {}
66
+ else:
67
+ nonlocal_vars = {
68
+ var: cell.cell_contents for var, cell in zip(code.co_freevars, closure, strict=True) if cell is not None
69
+ }
57
70
  global_vars = {
58
71
  **builtins.__dict__,
59
72
  **fn.__globals__,
60
- **inspect.getclosurevars(fn).nonlocals,
73
+ **nonlocal_vars,
61
74
  }
62
75
  return Visitor(source_file, bound_args, global_vars).run(node)
63
76
 
@@ -346,7 +359,7 @@ class Visitor(ast.NodeVisitor):
346
359
  self.loop_head_ctxs.append(header_ctx)
347
360
  self.break_ctxs.append([])
348
361
  set_ctx(header_ctx)
349
- has_next = self.ensure_boolean_num(self.handle_call(node, iterator.has_next))
362
+ has_next = self.convert_to_boolean_num(node, self.handle_call(node, iterator.has_next))
350
363
  if has_next._is_py_() and not has_next._as_py_():
351
364
  # The loop will never run, continue after evaluating the condition
352
365
  self.loop_head_ctxs.pop()
@@ -387,7 +400,7 @@ class Visitor(ast.NodeVisitor):
387
400
  self.loop_head_ctxs.append(header_ctx)
388
401
  self.break_ctxs.append([])
389
402
  set_ctx(header_ctx)
390
- test = self.ensure_boolean_num(self.visit(node.test))
403
+ test = self.convert_to_boolean_num(node.test, self.visit(node.test))
391
404
  if test._is_py_():
392
405
  if test._as_py_():
393
406
  # The loop will run until a break / return
@@ -441,7 +454,7 @@ class Visitor(ast.NodeVisitor):
441
454
  set_ctx(after_ctx)
442
455
 
443
456
  def visit_If(self, node):
444
- test = self.ensure_boolean_num(self.visit(node.test))
457
+ test = self.convert_to_boolean_num(node.test, self.visit(node.test))
445
458
 
446
459
  if test._is_py_():
447
460
  if test._as_py_():
@@ -494,7 +507,9 @@ class Visitor(ast.NodeVisitor):
494
507
  set_ctx(false_ctx)
495
508
  continue
496
509
  set_ctx(true_ctx)
497
- guard = self.ensure_boolean_num(self.visit(case.guard)) if case.guard else validate_value(True)
510
+ guard = (
511
+ self.convert_to_boolean_num(case.guard, self.visit(case.guard)) if case.guard else validate_value(True)
512
+ )
498
513
  if guard._is_py_():
499
514
  if guard._as_py_():
500
515
  for stmt in case.body:
@@ -531,7 +546,7 @@ class Visitor(ast.NodeVisitor):
531
546
  match pattern:
532
547
  case ast.MatchValue(value=value):
533
548
  value = self.visit(value)
534
- test = self.ensure_boolean_num(validate_value(subject == value))
549
+ test = self.convert_to_boolean_num(pattern, validate_value(subject == value))
535
550
  if test._is_py_():
536
551
  if test._as_py_():
537
552
  return ctx(), ctx().into_dead()
@@ -561,7 +576,7 @@ class Visitor(ast.NodeVisitor):
561
576
  target_len = len(patterns)
562
577
  if not (isinstance(subject, Sequence | TupleImpl)):
563
578
  return ctx().into_dead(), ctx()
564
- length_test = self.ensure_boolean_num(validate_value(_len(subject) == target_len))
579
+ length_test = self.convert_to_boolean_num(pattern, validate_value(_len(subject) == target_len))
565
580
  ctx_init = ctx()
566
581
  if not length_test._is_py_():
567
582
  ctx_init.test = length_test.ir()
@@ -726,8 +741,12 @@ class Visitor(ast.NodeVisitor):
726
741
  def visit_UnaryOp(self, node):
727
742
  operand = self.visit(node.operand)
728
743
  if isinstance(node.op, ast.Not):
729
- return self.ensure_boolean_num(operand).not_()
744
+ return self.convert_to_boolean_num(node, operand).not_()
730
745
  op = unary_ops[type(node.op)]
746
+ if operand._is_py_():
747
+ operand_py = operand._as_py_()
748
+ if isinstance(operand_py, type) and hasattr(type(operand_py), op):
749
+ return self.handle_call(node, getattr(type(operand_py), op), operand_py)
731
750
  if hasattr(operand, op):
732
751
  return self.handle_call(node, getattr(operand, op))
733
752
  raise TypeError(f"bad operand type for unary {op_to_symbol[type(node.op)]}: '{type(operand).__name__}'")
@@ -751,7 +770,7 @@ class Visitor(ast.NodeVisitor):
751
770
  return validate_value(fn)
752
771
 
753
772
  def visit_IfExp(self, node):
754
- test = self.ensure_boolean_num(self.visit(node.test))
773
+ test = self.convert_to_boolean_num(node.test, self.visit(node.test))
755
774
 
756
775
  if test._is_py_():
757
776
  if test._as_py_():
@@ -831,10 +850,11 @@ class Visitor(ast.NodeVisitor):
831
850
  ):
832
851
  result = self.handle_call(node, getattr(r_val, rcomp_ops[type(op)]), l_val)
833
852
  if result is None or self.is_not_implemented(result):
834
- if type(op) is ast.Eq:
835
- result = Num._accept_(l_val is r_val)
836
- elif type(op) is ast.NotEq:
837
- result = Num._accept_(l_val is not r_val)
853
+ # Can't defer to the default object.__eq__ or similar since reference equality (is) is not reliable
854
+ if type(op) is ast.Eq and type(l_val) is not type(r_val):
855
+ return Num._accept_(False)
856
+ elif type(op) is ast.NotEq and type(l_val) is not type(r_val):
857
+ return Num._accept_(True)
838
858
  else:
839
859
  raise TypeError(
840
860
  f"'{op_to_symbol[type(op)]}' not supported between instances of '{type(l_val).__name__}' and "
@@ -1058,7 +1078,7 @@ class Visitor(ast.NodeVisitor):
1058
1078
 
1059
1079
  def handle_call[**P, R](
1060
1080
  self, node: ast.stmt | ast.expr, fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs
1061
- ) -> R:
1081
+ ) -> R | Value:
1062
1082
  """Handles a call to the given callable."""
1063
1083
  self.active_ctx = ctx()
1064
1084
  if (
@@ -1109,6 +1129,20 @@ class Visitor(ast.NodeVisitor):
1109
1129
  raise TypeError(f"Invalid type where a bool (Num) was expected: {type(value).__name__}")
1110
1130
  return value
1111
1131
 
1132
+ def convert_to_boolean_num(self, node, value: Value) -> Num:
1133
+ if _is_num(value):
1134
+ return value
1135
+ if hasattr(type(value), "__bool__"):
1136
+ return self.ensure_boolean_num(self.handle_call(node, type(value).__bool__, validate_value(value)))
1137
+ if hasattr(type(value), "__len__"):
1138
+ length = self.handle_call(node, type(value).__len__, validate_value(value))
1139
+ if not _is_num(length):
1140
+ raise TypeError(f"Invalid type for __len__: {type(length).__name__}")
1141
+ if length._is_py_():
1142
+ return Num._accept_(length._as_py_() > 0)
1143
+ return length > Num._accept_(0)
1144
+ raise TypeError(f"Converting {type(value).__name__} to bool is not supported")
1145
+
1112
1146
  def arguments_to_signature(self, arguments: ast.arguments) -> inspect.Signature:
1113
1147
  parameters: list[inspect.Parameter] = []
1114
1148
  pos_only_count = len(arguments.posonlyargs)
@@ -1177,18 +1211,33 @@ class Visitor(ast.NodeVisitor):
1177
1211
  self, node: ast.stmt | ast.expr, fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs
1178
1212
  ) -> R:
1179
1213
  """Executes the given function at the given node for a better traceback."""
1214
+ location_args = {
1215
+ "lineno": node.lineno,
1216
+ "col_offset": node.col_offset,
1217
+ "end_lineno": node.end_lineno,
1218
+ "end_col_offset": node.end_col_offset,
1219
+ }
1220
+
1180
1221
  expr = ast.Expression(
1181
1222
  body=ast.Call(
1182
- func=ast.Name(id="fn", ctx=ast.Load()),
1183
- args=[ast.Starred(value=ast.Name(id="args", ctx=ast.Load()), ctx=ast.Load())],
1184
- keywords=[ast.keyword(value=ast.Name(id="kwargs", ctx=ast.Load()), arg=None)],
1185
- lineno=node.lineno,
1186
- col_offset=node.col_offset,
1187
- end_lineno=node.end_lineno,
1188
- end_col_offset=node.end_col_offset,
1189
- ),
1223
+ func=ast.Name(id="fn", ctx=ast.Load(), **location_args),
1224
+ args=[
1225
+ ast.Starred(
1226
+ value=ast.Name(id="args", ctx=ast.Load(), **location_args),
1227
+ ctx=ast.Load(),
1228
+ **location_args,
1229
+ )
1230
+ ],
1231
+ keywords=[
1232
+ ast.keyword(
1233
+ value=ast.Name(id="kwargs", ctx=ast.Load(), **location_args),
1234
+ arg=None,
1235
+ **location_args,
1236
+ )
1237
+ ],
1238
+ **location_args,
1239
+ )
1190
1240
  )
1191
- expr = ast.fix_missing_locations(expr)
1192
1241
  return eval(
1193
1242
  compile(expr, filename=self.source_file, mode="eval"),
1194
1243
  {"fn": fn, "args": args, "kwargs": kwargs, "_filter_traceback_": True},
sonolus/build/cli.py CHANGED
@@ -11,7 +11,7 @@ from pathlib import Path
11
11
  from time import perf_counter
12
12
 
13
13
  from sonolus.backend.optimize.optimize import FAST_PASSES, MINIMAL_PASSES, STANDARD_PASSES
14
- from sonolus.build.engine import package_engine
14
+ from sonolus.build.engine import no_gil, package_engine
15
15
  from sonolus.build.level import package_level_data
16
16
  from sonolus.build.project import build_project_to_collection, get_project_schema
17
17
  from sonolus.script.project import BuildConfig, Project
@@ -215,6 +215,11 @@ def main():
215
215
  else:
216
216
  parser.error("Module argument is required when multiple or no modules are found")
217
217
 
218
+ if no_gil():
219
+ print("Multithreading is enabled")
220
+ if hasattr(sys, "_jit") and sys._jit.is_enabled():
221
+ print("Python JIT is enabled")
222
+
218
223
  project = import_project(args.module)
219
224
  if project is None:
220
225
  sys.exit(1)
sonolus/build/engine.py CHANGED
@@ -32,7 +32,7 @@ from sonolus.script.project import BuildConfig
32
32
  from sonolus.script.sprite import Skin
33
33
  from sonolus.script.ui import UiConfig
34
34
 
35
- type JsonValue = None | bool | int | float | str | list[JsonValue] | dict[str, JsonValue]
35
+ type JsonValue = bool | int | float | str | list[JsonValue] | dict[str, JsonValue] | None
36
36
 
37
37
 
38
38
  @dataclass