sonolus.py 0.10.7__py3-none-any.whl → 0.12.5__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.

@@ -1,7 +1,7 @@
1
1
  from math import isfinite, isinf, isnan
2
2
 
3
3
  from sonolus.backend.ir import IRConst, IRGet, IRInstr, IRPureInstr, IRSet
4
- from sonolus.backend.node import ConstantNode, EngineNode, FunctionNode
4
+ from sonolus.backend.node import EngineNode, FunctionNode
5
5
  from sonolus.backend.ops import Op
6
6
  from sonolus.backend.optimize.flow import BasicBlock, traverse_cfg_reverse_postorder
7
7
  from sonolus.backend.place import BlockPlace
@@ -18,18 +18,18 @@ def cfg_to_engine_node(entry: BasicBlock):
18
18
  }
19
19
  match outgoing:
20
20
  case {**other} if not other:
21
- statements.append(ConstantNode(value=len(block_indexes)))
21
+ statements.append(len(block_indexes))
22
22
  case {None: target, **other} if not other:
23
- statements.append(ConstantNode(value=block_indexes[target]))
23
+ statements.append(block_indexes[target])
24
24
  case {0: f_branch, None: t_branch, **other} if not other:
25
25
  statements.append(
26
26
  FunctionNode(
27
27
  func=Op.If,
28
- args=[
28
+ args=(
29
29
  ir_to_engine_node(block.test),
30
- ConstantNode(value=block_indexes[t_branch]),
31
- ConstantNode(value=block_indexes[f_branch]),
32
- ],
30
+ block_indexes[t_branch],
31
+ block_indexes[f_branch],
32
+ ),
33
33
  )
34
34
  )
35
35
  case {None: default_branch, **other} if len(other) == 1:
@@ -37,11 +37,11 @@ def cfg_to_engine_node(entry: BasicBlock):
37
37
  statements.append(
38
38
  FunctionNode(
39
39
  func=Op.If,
40
- args=[
40
+ args=(
41
41
  ir_to_engine_node(IRPureInstr(Op.Equal, args=[block.test, IRConst(cond)])),
42
- ConstantNode(value=block_indexes[cond_branch]),
43
- ConstantNode(value=block_indexes[default_branch]),
44
- ],
42
+ block_indexes[cond_branch],
43
+ block_indexes[default_branch],
44
+ ),
45
45
  )
46
46
  )
47
47
  case dict() as targets:
@@ -49,23 +49,31 @@ def cfg_to_engine_node(entry: BasicBlock):
49
49
  default = len(block_indexes)
50
50
  conds = [cond for cond in targets if cond is not None]
51
51
  if min(conds) == 0 and max(conds) == len(conds) - 1 and all(int(cond) == cond for cond in conds):
52
- args.extend(ConstantNode(value=block_indexes[targets[cond]]) for cond in range(len(conds)))
52
+ args.extend(block_indexes[targets[cond]] for cond in range(len(conds)))
53
53
  if None in targets:
54
54
  default = block_indexes[targets[None]]
55
- args.append(ConstantNode(value=default))
56
- statements.append(FunctionNode(Op.SwitchIntegerWithDefault, args))
55
+ args.append(default)
56
+ statements.append(FunctionNode(Op.SwitchIntegerWithDefault, tuple(args)))
57
57
  else:
58
58
  for cond, target in targets.items():
59
59
  if cond is None:
60
60
  default = block_indexes[target]
61
61
  continue
62
- args.append(ConstantNode(value=cond))
63
- args.append(ConstantNode(value=block_indexes[target]))
64
- args.append(ConstantNode(value=default))
65
- statements.append(FunctionNode(Op.SwitchWithDefault, args))
66
- block_statements.append(FunctionNode(Op.Execute, statements))
67
- block_statements.append(ConstantNode(value=0))
68
- return FunctionNode(Op.Block, [FunctionNode(Op.JumpLoop, block_statements)])
62
+ args.append(cond)
63
+ args.append(block_indexes[target])
64
+ args.append(default)
65
+ statements.append(FunctionNode(Op.SwitchWithDefault, tuple(args)))
66
+ block_statements.append(FunctionNode(Op.Execute, tuple(statements)))
67
+ block_statements.append(0)
68
+ result = FunctionNode(Op.Block, (FunctionNode(Op.JumpLoop, tuple(block_statements)),))
69
+ for block in block_indexes:
70
+ # Clean up without relying on gc
71
+ del block.incoming
72
+ del block.outgoing
73
+ del block.phis
74
+ del block.statements
75
+ del block.test
76
+ return result
69
77
 
70
78
 
71
79
  def ir_to_engine_node(stmt) -> EngineNode:
@@ -73,32 +81,30 @@ def ir_to_engine_node(stmt) -> EngineNode:
73
81
  case int(value) | float(value) | IRConst(value=int(value) | float(value)):
74
82
  value = float(value)
75
83
  if value.is_integer():
76
- return ConstantNode(value=int(value))
84
+ return int(value)
77
85
  elif isfinite(value):
78
- return ConstantNode(value=value)
86
+ return value
79
87
  elif isinf(value):
80
88
  # Read values from ROM
81
- return FunctionNode(Op.Get, args=[ConstantNode(value=3000), ConstantNode(value=1 if value > 0 else 2)])
89
+ return FunctionNode(Op.Get, args=(3000, 1 if value > 0 else 2))
82
90
  elif isnan(value):
83
91
  # Read value from ROM
84
- return FunctionNode(Op.Get, args=[ConstantNode(value=3000), ConstantNode(value=0)])
92
+ return FunctionNode(Op.Get, args=(3000, 0))
85
93
  else:
86
94
  raise ValueError(f"Invalid constant value: {value}")
87
95
  case IRPureInstr(op=op, args=args) | IRInstr(op=op, args=args):
88
- return FunctionNode(func=op, args=[ir_to_engine_node(arg) for arg in args])
96
+ return FunctionNode(func=op, args=tuple(ir_to_engine_node(arg) for arg in args))
89
97
  case IRGet(place=place):
90
98
  return ir_to_engine_node(place)
91
99
  case BlockPlace() as place:
92
100
  if place.offset == 0:
93
101
  index = ir_to_engine_node(place.index)
94
102
  elif place.index == 0:
95
- index = ConstantNode(value=place.offset)
103
+ index = place.offset
96
104
  else:
97
- index = FunctionNode(
98
- func=Op.Add, args=[ir_to_engine_node(place.index), ConstantNode(value=place.offset)]
99
- )
100
- return FunctionNode(func=Op.Get, args=[ir_to_engine_node(place.block), index])
105
+ index = FunctionNode(func=Op.Add, args=(ir_to_engine_node(place.index), place.offset))
106
+ return FunctionNode(func=Op.Get, args=(ir_to_engine_node(place.block), index))
101
107
  case IRSet(place=place, value=value):
102
- return FunctionNode(func=Op.Set, args=[*ir_to_engine_node(place).args, ir_to_engine_node(value)])
108
+ return FunctionNode(func=Op.Set, args=(*ir_to_engine_node(place).args, ir_to_engine_node(value)))
103
109
  case _:
104
110
  raise TypeError(f"Unsupported IR statement: {stmt}")
@@ -2,7 +2,7 @@ import math
2
2
  import operator
3
3
  import random
4
4
 
5
- from sonolus.backend.node import ConstantNode, EngineNode
5
+ from sonolus.backend.node import EngineNode, FunctionNode
6
6
  from sonolus.backend.ops import Op
7
7
 
8
8
 
@@ -24,8 +24,8 @@ class Interpreter:
24
24
  self.log = []
25
25
 
26
26
  def run(self, node: EngineNode) -> float:
27
- if isinstance(node, ConstantNode):
28
- return node.value
27
+ if not isinstance(node, FunctionNode):
28
+ return node
29
29
  func = node.func
30
30
  args = node.args
31
31
  match func:
sonolus/backend/node.py CHANGED
@@ -1,40 +1,18 @@
1
1
  import textwrap
2
- from dataclasses import dataclass, field
2
+ from typing import NamedTuple
3
3
 
4
4
  from sonolus.backend.ops import Op
5
5
 
6
- type EngineNode = ConstantNode | FunctionNode
6
+ type EngineNode = int | float | FunctionNode
7
7
 
8
8
 
9
- @dataclass(slots=True)
10
- class ConstantNode:
11
- value: float
12
- _hash: int = field(init=False, repr=False)
13
-
14
- def __post_init__(self):
15
- self._hash = hash(self.value)
16
-
17
- def __hash__(self):
18
- return hash(self.value)
19
-
20
-
21
- @dataclass(slots=True)
22
- class FunctionNode:
9
+ class FunctionNode(NamedTuple):
23
10
  func: Op
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)))
29
-
30
- def __hash__(self):
31
- return self._hash
11
+ args: tuple[EngineNode, ...]
32
12
 
33
13
 
34
14
  def format_engine_node(node: EngineNode) -> str:
35
- if isinstance(node, ConstantNode):
36
- return str(node.value)
37
- elif isinstance(node, FunctionNode):
15
+ if isinstance(node, FunctionNode):
38
16
  match len(node.args):
39
17
  case 0:
40
18
  return f"{node.func.name}()"
@@ -45,4 +23,4 @@ def format_engine_node(node: EngineNode) -> str:
45
23
  textwrap.indent('\n'.join(format_engine_node(arg) for arg in node.args), ' ')
46
24
  }\n)"
47
25
  else:
48
- raise ValueError(f"Invalid engine node: {node}")
26
+ return str(node)
@@ -24,15 +24,18 @@ class LivenessAnalysis(CompilerPass):
24
24
  block.live_phi_targets = set()
25
25
  block.array_defs_in = set()
26
26
  block.array_defs_out = None
27
+ last_live_set = set()
27
28
  for statement in block.statements:
28
- statement.live = set()
29
+ if isinstance(statement, IRSet):
30
+ last_live_set = set()
31
+ statement.live = last_live_set
29
32
  statement.visited = False
30
33
  statement.uses = self.get_uses(statement, set())
31
34
  statement.defs = self.get_defs(statement)
32
35
  statement.is_array_init = False # True if this may be the first assignment to an array
33
36
  statement.array_defs = self.get_array_defs(statement)
34
37
  if not isinstance(block.test, IRConst):
35
- block.test.live = set()
38
+ block.test.live = last_live_set
36
39
  block.test.uses = self.get_uses(block.test, set())
37
40
  self.preprocess_arrays(entry)
38
41
 
@@ -319,7 +319,6 @@ class Visitor(ast.NodeVisitor):
319
319
  with using_ctx(before_ctx):
320
320
  state_var._set_(0)
321
321
  with using_ctx(return_ctx):
322
- state_var._set_(len(self.return_ctxs) + 1)
323
322
  is_present_var._set_(0)
324
323
  del before_ctx.outgoing[None] # Unlink the state machine body from the call site
325
324
  entry = before_ctx.new_empty_disconnected()
@@ -1007,7 +1006,9 @@ class Visitor(ast.NodeVisitor):
1007
1006
  return validate_value({self.visit(k): self.visit(v) for k, v in zip(node.keys, node.values, strict=True)})
1008
1007
 
1009
1008
  def visit_Set(self, node):
1010
- raise NotImplementedError("Set literals are not supported")
1009
+ from sonolus.script.containers import FrozenNumSet
1010
+
1011
+ return self.handle_call(node, FrozenNumSet.of, *(self.visit(elt) for elt in node.elts))
1011
1012
 
1012
1013
  def visit_ListComp(self, node):
1013
1014
  raise NotImplementedError("List comprehensions are not supported")
@@ -1307,6 +1308,7 @@ class Visitor(ast.NodeVisitor):
1307
1308
  raise NotImplementedError(f"Unsupported syntax: {type(node).__name__}")
1308
1309
 
1309
1310
  def handle_getattr(self, node: ast.stmt | ast.expr, target: Value, key: str) -> Value:
1311
+ # If this is changed, remember to update the getattr impl too
1310
1312
  with self.reporting_errors_at_node(node):
1311
1313
  if isinstance(target, ConstantValue):
1312
1314
  # Unwrap so we can access fields
@@ -1327,6 +1329,7 @@ class Visitor(ast.NodeVisitor):
1327
1329
  raise TypeError(f"Unsupported field or descriptor {key}")
1328
1330
 
1329
1331
  def handle_setattr(self, node: ast.stmt | ast.expr, target: Value, key: str, value: Value):
1332
+ # If this is changed, remember to update the setattr impl too
1330
1333
  with self.reporting_errors_at_node(node):
1331
1334
  if target._is_py_():
1332
1335
  target = target._as_py_()
sonolus/build/cli.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import argparse
2
+ import gc
2
3
  import importlib
3
4
  import json
4
5
  import shutil
@@ -129,6 +130,7 @@ def get_config(args: argparse.Namespace) -> BuildConfig:
129
130
  build_preview=build_preview,
130
131
  build_tutorial=build_tutorial,
131
132
  runtime_checks=get_runtime_checks(args),
133
+ verbose=hasattr(args, "verbose") and args.verbose,
132
134
  )
133
135
 
134
136
 
@@ -164,12 +166,18 @@ def main():
164
166
  help="Runtime error checking mode (default: none for build, notify for dev)",
165
167
  )
166
168
 
169
+ gc_group = parser.add_mutually_exclusive_group()
170
+ gc_group.add_argument("--no-gc", action="store_true", default=True, help="Disable garbage collection (default)")
171
+ gc_group.add_argument("--gc", action="store_true", help="Enable garbage collection")
172
+
167
173
  build_components = parser.add_argument_group("build components")
168
174
  build_components.add_argument("--play", action="store_true", help="Build play component")
169
175
  build_components.add_argument("--watch", action="store_true", help="Build watch component")
170
176
  build_components.add_argument("--preview", action="store_true", help="Build preview component")
171
177
  build_components.add_argument("--tutorial", action="store_true", help="Build tutorial component")
172
178
 
179
+ parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output")
180
+
173
181
  build_parser = subparsers.add_parser("build")
174
182
  build_parser.add_argument(
175
183
  "module",
@@ -219,6 +227,12 @@ def main():
219
227
  else:
220
228
  parser.error("Module argument is required when multiple or no modules are found")
221
229
 
230
+ if args.command in {"build", "check", "dev"}:
231
+ if hasattr(args, "gc") and args.gc:
232
+ gc.enable()
233
+ elif hasattr(args, "no_gc") and args.no_gc:
234
+ gc.disable()
235
+
222
236
  if no_gil():
223
237
  print("Multithreading is enabled")
224
238
  if hasattr(sys, "_jit") and sys._jit.is_enabled():
@@ -254,5 +268,9 @@ def main():
254
268
  end_time = perf_counter()
255
269
  print(f"Project validation completed successfully in {end_time - start_time:.2f}s")
256
270
  except CompilationError:
271
+ if args.verbose:
272
+ raise
257
273
  exc_info = sys.exc_info()
258
274
  print_simple_traceback(*exc_info)
275
+ print("\nFor more details, run with the --verbose (-v) flag.")
276
+ sys.exit(1)
sonolus/build/compile.py CHANGED
@@ -32,9 +32,11 @@ class CompileCache:
32
32
  self._cache = {}
33
33
  self._lock = Lock()
34
34
  self._event = Event()
35
+ self._accessed_hashes = set()
35
36
 
36
37
  def get(self, entry_hash: int) -> EngineNode | None:
37
38
  with self._lock:
39
+ self._accessed_hashes.add(entry_hash)
38
40
  if entry_hash not in self._cache:
39
41
  self._cache[entry_hash] = None # Mark as being compiled
40
42
  return None
@@ -47,10 +49,21 @@ class CompileCache:
47
49
 
48
50
  def set(self, entry_hash: int, node: EngineNode) -> None:
49
51
  with self._lock:
52
+ self._accessed_hashes.add(entry_hash)
50
53
  self._cache[entry_hash] = node
51
54
  self._event.set()
52
55
  self._event.clear()
53
56
 
57
+ def reset_accessed(self) -> None:
58
+ with self._lock:
59
+ self._accessed_hashes.clear()
60
+
61
+ def prune_unaccessed(self) -> None:
62
+ with self._lock:
63
+ unaccessed_hashes = set(self._cache.keys()) - self._accessed_hashes
64
+ for h in unaccessed_hashes:
65
+ del self._cache[h]
66
+
54
67
 
55
68
  def compile_mode(
56
69
  mode: Mode,
@@ -67,7 +80,7 @@ def compile_mode(
67
80
 
68
81
  mode_state = ModeContextState(
69
82
  mode,
70
- {a: i for i, a in enumerate(archetypes)} if archetypes is not None else None,
83
+ archetypes,
71
84
  )
72
85
  nodes = OutputNodeGenerator()
73
86
  results = {}
@@ -134,9 +147,10 @@ def compile_mode(
134
147
  archetype_data["exports"] = [*archetype._exported_keys_]
135
148
 
136
149
  callback_items = [
137
- (cb_name, cb_info, getattr(archetype, cb_name))
150
+ (cb_name, cb_info, archetype._callbacks_[cb_name])
138
151
  for cb_name, cb_info in archetype._supported_callbacks_.items()
139
- if getattr(archetype, cb_name) not in archetype._default_callbacks_
152
+ if cb_name in archetype._callbacks_
153
+ and archetype._callbacks_[cb_name] not in archetype._default_callbacks_
140
154
  ]
141
155
 
142
156
  if thread_pool is not None:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import contextlib
5
+ import gc
5
6
  import http.server
6
7
  import importlib
7
8
  import queue
@@ -118,6 +119,7 @@ class RebuildCommand:
118
119
  print("Rebuilding...")
119
120
  try:
120
121
  start_time = perf_counter()
122
+ server_state.cache.reset_accessed()
121
123
  server_state.project_state = ProjectContextState.from_build_config(server_state.config)
122
124
  server_state.project = project_module.project
123
125
  build_collection(
@@ -127,11 +129,16 @@ class RebuildCommand:
127
129
  cache=server_state.cache,
128
130
  project_state=server_state.project_state,
129
131
  )
132
+ server_state.cache.prune_unaccessed()
130
133
  end_time = perf_counter()
131
134
  print(f"Rebuild completed in {end_time - start_time:.2f} seconds")
132
135
  except CompilationError:
133
136
  exc_info = sys.exc_info()
134
- print_simple_traceback(*exc_info)
137
+ if server_state.config.verbose:
138
+ print(traceback.format_exc())
139
+ else:
140
+ print_simple_traceback(*exc_info)
141
+ print("\nFor more details, run with the --verbose (-v) flag.")
135
142
 
136
143
 
137
144
  @dataclass
@@ -331,6 +338,7 @@ def run_server(
331
338
  cmd = command_queue.get()
332
339
  try:
333
340
  cmd.execute(server_state)
341
+ gc.collect()
334
342
  except Exception:
335
343
  print(f"{traceback.format_exc()}\n")
336
344
  prompt_event.set()
sonolus/build/node.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from threading import Lock
2
2
  from typing import TypedDict
3
3
 
4
- from sonolus.backend.node import ConstantNode, EngineNode, FunctionNode
4
+ from sonolus.backend.node import EngineNode, FunctionNode
5
5
 
6
6
 
7
7
  class ValueOutputNode(TypedDict):
@@ -32,19 +32,17 @@ class OutputNodeGenerator:
32
32
  return self.indexes[node]
33
33
 
34
34
  match node:
35
- case ConstantNode(value):
36
- index = len(self.nodes)
37
- self.nodes.append({"value": value})
38
- self.indexes[node] = index
39
- return index
40
35
  case FunctionNode(func, args):
41
36
  arg_indexes = [self._add(arg) for arg in args]
42
37
  index = len(self.nodes)
43
38
  self.nodes.append({"func": func.value, "args": arg_indexes})
44
39
  self.indexes[node] = index
45
40
  return index
46
- case _:
47
- raise ValueError("Invalid node")
41
+ case constant:
42
+ index = len(self.nodes)
43
+ self.nodes.append({"value": constant})
44
+ self.indexes[node] = index
45
+ return index
48
46
 
49
47
  def get(self):
50
48
  return self.nodes