sonolus.py 0.9.3__py3-none-any.whl → 0.10.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.

@@ -158,7 +158,8 @@ class SparseConditionalConstantPropagation(CompilerPass):
158
158
  test_value = values[block]
159
159
  new_test_value = self.evaluate_stmt(block.test, values)
160
160
  if new_test_value != test_value:
161
- assert new_test_value is not UNDEF
161
+ if new_test_value is UNDEF:
162
+ continue
162
163
  values[block] = new_test_value
163
164
  if new_test_value is NAC:
164
165
  flow_worklist.update(block.outgoing)
@@ -195,7 +196,8 @@ class SparseConditionalConstantPropagation(CompilerPass):
195
196
  test_value = values[p]
196
197
  new_test_value = self.evaluate_stmt(defn, values)
197
198
  if new_test_value != test_value:
198
- assert new_test_value is not UNDEF
199
+ if new_test_value is UNDEF:
200
+ continue
199
201
  values[p] = new_test_value
200
202
  if new_test_value is NAC:
201
203
  flow_worklist.update(p.outgoing)
@@ -10,6 +10,7 @@ from sonolus.backend.optimize.inlining import InlineVars
10
10
  from sonolus.backend.optimize.simplify import (
11
11
  CoalesceFlow,
12
12
  CoalesceSmallConditionalBlocks,
13
+ CombineExitBlocks,
13
14
  NormalizeSwitch,
14
15
  RemoveRedundantArguments,
15
16
  RewriteToSwitch,
@@ -55,5 +56,6 @@ STANDARD_PASSES = (
55
56
  AdvancedDeadCodeElimination(),
56
57
  CoalesceFlow(),
57
58
  NormalizeSwitch(),
59
+ CombineExitBlocks(),
58
60
  Allocate(),
59
61
  )
@@ -77,6 +77,21 @@ class CoalesceFlow(CompilerPass):
77
77
  return entry
78
78
 
79
79
 
80
+ class CombineExitBlocks(CompilerPass):
81
+ def run(self, entry: BasicBlock, config: OptimizerConfig) -> BasicBlock:
82
+ first_exit_block = None
83
+ for block in traverse_cfg_preorder(entry):
84
+ if not block.outgoing and not block.phis and not block.statements:
85
+ if first_exit_block is None:
86
+ first_exit_block = block
87
+ else:
88
+ for edge in [*block.incoming]:
89
+ edge.dst = first_exit_block
90
+ first_exit_block.incoming.add(edge)
91
+ block.incoming.clear()
92
+ return entry
93
+
94
+
80
95
  class CoalesceSmallConditionalBlocks(CompilerPass):
81
96
  def run(self, entry: BasicBlock, config: OptimizerConfig) -> BasicBlock:
82
97
  queue = [entry]
@@ -47,9 +47,14 @@ def compile_and_call[**P, R](fn: Callable[P, R], /, *args: P.args, **kwargs: P.k
47
47
  def compile_and_call_at_definition[**P, R](fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs) -> R:
48
48
  if not ctx():
49
49
  return fn(*args, **kwargs)
50
- if ctx().no_eval:
51
- return compile_and_call(fn, *args, **kwargs)
52
50
  source_file, node = get_function(fn)
51
+ if ctx().no_eval:
52
+ debug_stack = ctx().callback_state.debug_stack
53
+ try:
54
+ debug_stack.append(f'File "{source_file}", line {node.lineno}, in <callback>')
55
+ return compile_and_call(fn, *args, **kwargs)
56
+ finally:
57
+ debug_stack.pop()
53
58
  location_args = {
54
59
  "lineno": node.lineno,
55
60
  "col_offset": node.col_offset,
@@ -64,10 +69,16 @@ def compile_and_call_at_definition[**P, R](fn: Callable[P, R], /, *args: P.args,
64
69
  **location_args,
65
70
  )
66
71
  )
67
- return eval(
68
- compile(expr, filename=source_file, mode="eval"),
69
- {"fn": lambda: compile_and_call(fn, *args, **kwargs), "_filter_traceback_": True, "_traceback_root_": True},
70
- )
72
+
73
+ debug_stack = ctx().callback_state.debug_stack
74
+ try:
75
+ debug_stack.append(f'File "{source_file}", line {node.lineno}, in <callback>')
76
+ return eval(
77
+ compile(expr, filename=source_file, mode="eval"),
78
+ {"fn": lambda: compile_and_call(fn, *args, **kwargs), "_filter_traceback_": True, "_traceback_root_": True},
79
+ )
80
+ finally:
81
+ debug_stack.pop()
71
82
 
72
83
 
73
84
  def generate_fn_impl(fn: Callable):
@@ -111,7 +122,9 @@ def eval_fn(fn: Callable, /, *args, **kwargs):
111
122
  **fn.__globals__,
112
123
  **nonlocal_vars,
113
124
  }
114
- return Visitor(source_file, bound_args, global_vars).run(node)
125
+ return Visitor(
126
+ source_file, bound_args, global_vars, parent=None, function_name=getattr(fn, "__name__", "<unnamed>")
127
+ ).run(node)
115
128
 
116
129
 
117
130
  unary_ops = {
@@ -232,14 +245,16 @@ class Visitor(ast.NodeVisitor):
232
245
  resume_ctxs: list[Context] # Contexts after yield statements
233
246
  active_ctx: Context | None # The active context for use in nested functions
234
247
  parent: Visitor | None # The parent visitor for use in nested functions
235
- used_parent_binding_values: dict[str, Value] # Values of parent bindings used in this function
248
+ used_parent_binding_values: dict[str, Value] # Values of parent bindings used in this
249
+ function_name: str
236
250
 
237
251
  def __init__(
238
252
  self,
239
253
  source_file: str,
240
254
  bound_args: inspect.BoundArguments,
241
255
  global_vars: dict[str, Any],
242
- parent: Visitor | None = None,
256
+ parent: Visitor | None,
257
+ function_name: str,
243
258
  ):
244
259
  self.source_file = source_file
245
260
  self.globals = global_vars
@@ -253,6 +268,7 @@ class Visitor(ast.NodeVisitor):
253
268
  self.active_ctx = None
254
269
  self.parent = parent
255
270
  self.used_parent_binding_values = {}
271
+ self.function_name = function_name
256
272
 
257
273
  def run(self, node):
258
274
  before_ctx = ctx()
@@ -433,7 +449,8 @@ class Visitor(ast.NodeVisitor):
433
449
  self.source_file,
434
450
  bound,
435
451
  self.globals,
436
- self,
452
+ parent=self,
453
+ function_name=name,
437
454
  ).run(node)
438
455
 
439
456
  fn._meta_fn_ = True
@@ -951,7 +968,8 @@ class Visitor(ast.NodeVisitor):
951
968
  self.source_file,
952
969
  bound,
953
970
  self.globals,
954
- self,
971
+ parent=self,
972
+ function_name="<lambda>",
955
973
  ).run(node)
956
974
 
957
975
  fn._meta_fn_ = True
@@ -1002,7 +1020,9 @@ class Visitor(ast.NodeVisitor):
1002
1020
 
1003
1021
  def visit_GeneratorExp(self, node):
1004
1022
  self.active_ctx = ctx()
1005
- return Visitor(self.source_file, inspect.Signature([]).bind(), self.globals, self).run(node)
1023
+ return Visitor(
1024
+ self.source_file, inspect.Signature([]).bind(), self.globals, parent=self, function_name="<genexp>"
1025
+ ).run(node)
1006
1026
 
1007
1027
  def visit_Await(self, node):
1008
1028
  raise NotImplementedError("Await expressions are not supported")
@@ -1326,15 +1346,20 @@ class Visitor(ast.NodeVisitor):
1326
1346
  ) -> R | Value:
1327
1347
  """Handles a call to the given callable."""
1328
1348
  self.active_ctx = ctx()
1329
- if (
1330
- isinstance(fn, Value)
1331
- and fn._is_py_()
1332
- and isinstance(fn._as_py_(), type)
1333
- and issubclass(fn._as_py_(), Value)
1334
- ):
1335
- return validate_value(self.execute_at_node(node, fn._as_py_(), *args, **kwargs))
1336
- else:
1337
- return self.execute_at_node(node, lambda: validate_value(compile_and_call(fn, *args, **kwargs)))
1349
+ debug_stack = self.active_ctx.callback_state.debug_stack
1350
+ try:
1351
+ debug_stack.append(f'File "{self.source_file}", line {node.lineno}, in {self.function_name}')
1352
+ if (
1353
+ isinstance(fn, Value)
1354
+ and fn._is_py_()
1355
+ and isinstance(fn._as_py_(), type)
1356
+ and issubclass(fn._as_py_(), Value)
1357
+ ):
1358
+ return validate_value(self.execute_at_node(node, fn._as_py_(), *args, **kwargs))
1359
+ else:
1360
+ return self.execute_at_node(node, lambda: validate_value(compile_and_call(fn, *args, **kwargs)))
1361
+ finally:
1362
+ debug_stack.pop()
1338
1363
 
1339
1364
  def handle_getitem(self, node: ast.stmt | ast.expr, target: Value, key: Value) -> Value:
1340
1365
  with self.reporting_errors_at_node(node):
sonolus/build/cli.py CHANGED
@@ -14,6 +14,7 @@ from sonolus.build.dev_server import run_server
14
14
  from sonolus.build.engine import no_gil, package_engine, validate_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
+ from sonolus.script.internal.context import ProjectContextState
17
18
  from sonolus.script.internal.error import CompilationError
18
19
  from sonolus.script.project import BuildConfig, Project
19
20
 
@@ -88,12 +89,18 @@ def validate_project(project: Project, config: BuildConfig):
88
89
  validate_engine(project.engine.data, config)
89
90
 
90
91
 
91
- def build_collection(project: Project, build_dir: Path, config: BuildConfig | None, cache: CompileCache | None = None):
92
+ def build_collection(
93
+ project: Project,
94
+ build_dir: Path,
95
+ config: BuildConfig | None,
96
+ cache: CompileCache | None = None,
97
+ project_state: ProjectContextState | None = None,
98
+ ):
92
99
  site_dir = build_dir / "site"
93
100
  shutil.rmtree(site_dir, ignore_errors=True)
94
101
  site_dir.mkdir(parents=True, exist_ok=True)
95
102
 
96
- collection = build_project_to_collection(project, config, cache=cache)
103
+ collection = build_project_to_collection(project, config, cache=cache, project_state=project_state)
97
104
  collection.write(site_dir)
98
105
 
99
106
 
@@ -217,14 +224,9 @@ def main():
217
224
  print(f"Project built successfully to '{build_dir.resolve()}' in {end_time - start_time:.2f}s")
218
225
  elif args.command == "dev":
219
226
  build_dir = Path(args.build_dir)
220
- start_time = perf_counter()
221
227
  config = get_config(args)
222
- cache = CompileCache()
223
- build_collection(project, build_dir, config, cache=cache)
224
- end_time = perf_counter()
225
- print(f"Build finished in {end_time - start_time:.2f}s")
226
228
  run_server(
227
- build_dir / "site", args.port, project_module.__name__, core_module_names, build_dir, config, cache
229
+ build_dir / "site", args.port, project_module.__name__, core_module_names, build_dir, config, project
228
230
  )
229
231
  elif args.command == "schema":
230
232
  print(json.dumps(get_project_schema(project), indent=2))
sonolus/build/compile.py CHANGED
@@ -17,8 +17,8 @@ from sonolus.script.internal.callbacks import CallbackInfo
17
17
  from sonolus.script.internal.context import (
18
18
  CallbackContextState,
19
19
  Context,
20
- GlobalContextState,
21
- ReadOnlyMemory,
20
+ ModeContextState,
21
+ ProjectContextState,
22
22
  context_to_cfg,
23
23
  ctx,
24
24
  using_ctx,
@@ -54,7 +54,7 @@ class CompileCache:
54
54
 
55
55
  def compile_mode(
56
56
  mode: Mode,
57
- rom: ReadOnlyMemory,
57
+ project_state: ProjectContextState,
58
58
  archetypes: list[type[_BaseArchetype]] | None,
59
59
  global_callbacks: list[tuple[CallbackInfo, Callable]] | None,
60
60
  passes: Sequence[CompilerPass] | None = None,
@@ -65,10 +65,9 @@ def compile_mode(
65
65
  if passes is None:
66
66
  passes = STANDARD_PASSES
67
67
 
68
- global_state = GlobalContextState(
68
+ mode_state = ModeContextState(
69
69
  mode,
70
70
  {a: i for i, a in enumerate(archetypes)} if archetypes is not None else None,
71
- rom,
72
71
  )
73
72
  nodes = OutputNodeGenerator()
74
73
  results = {}
@@ -84,7 +83,7 @@ def compile_mode(
84
83
  - (cb_info.name, node_index) for global callbacks, or
85
84
  - (cb_info.name, {"index": node_index, "order": cb_order}) for archetype callbacks.
86
85
  """
87
- cfg = callback_to_cfg(global_state, cb, cb_info.name, arch)
86
+ cfg = callback_to_cfg(project_state, mode_state, cb, cb_info.name, arch)
88
87
  if validate_only:
89
88
  if arch is not None:
90
89
  cb_order = getattr(cb, "_callback_order_", 0)
@@ -185,27 +184,29 @@ def compile_mode(
185
184
 
186
185
 
187
186
  def callback_to_cfg(
188
- global_state: GlobalContextState,
187
+ project_state: ProjectContextState,
188
+ mode_state: ModeContextState,
189
189
  callback: Callable,
190
190
  name: str,
191
191
  archetype: type[_BaseArchetype] | None = None,
192
192
  ) -> BasicBlock:
193
193
  try:
194
194
  # Default to no_eval=True for performance unless there's an error.
195
- return _callback_to_cfg(global_state, callback, name, archetype, no_eval=True)
195
+ return _callback_to_cfg(project_state, mode_state, callback, name, archetype, no_eval=True)
196
196
  except CompilationError:
197
- return _callback_to_cfg(global_state, callback, name, archetype, no_eval=False)
197
+ return _callback_to_cfg(project_state, mode_state, callback, name, archetype, no_eval=False)
198
198
 
199
199
 
200
200
  def _callback_to_cfg(
201
- global_state: GlobalContextState,
201
+ project_state: ProjectContextState,
202
+ mode_state: ModeContextState,
202
203
  callback: Callable,
203
204
  name: str,
204
205
  archetype: type[_BaseArchetype] | None,
205
206
  no_eval: bool,
206
207
  ) -> BasicBlock:
207
208
  callback_state = CallbackContextState(name, no_eval=no_eval)
208
- context = Context(global_state, callback_state)
209
+ context = Context(project_state, mode_state, callback_state)
209
210
  with using_ctx(context):
210
211
  if archetype is not None:
211
212
  result = compile_and_call_at_definition(callback, archetype._for_compilation())
@@ -6,6 +6,7 @@ import http.server
6
6
  import importlib
7
7
  import queue
8
8
  import shlex
9
+ import shutil
9
10
  import socket
10
11
  import socketserver
11
12
  import sys
@@ -15,53 +16,97 @@ import traceback
15
16
  from dataclasses import dataclass
16
17
  from pathlib import Path
17
18
  from time import perf_counter
18
- from typing import TYPE_CHECKING, Protocol
19
+ from typing import TYPE_CHECKING, NamedTuple, Protocol
19
20
 
20
21
  from sonolus.backend.excepthook import print_simple_traceback
21
22
  from sonolus.backend.utils import get_function, get_functions, get_tree_from_file
22
23
  from sonolus.build.compile import CompileCache
24
+ from sonolus.script.internal.context import ProjectContextState
23
25
  from sonolus.script.internal.error import CompilationError
24
26
 
25
27
  if TYPE_CHECKING:
26
- from sonolus.script.project import BuildConfig
28
+ from sonolus.script.project import BuildConfig, Project
27
29
 
28
30
  HELP_TEXT = """
29
31
  [r]ebuild
32
+ [d]ecode <message_code>
33
+ [h]elp
30
34
  [q]uit
31
35
  """.strip()
32
36
 
33
37
  HELP_TEXT = textwrap.dedent(HELP_TEXT)
34
38
 
35
39
 
40
+ class CommandHelp(NamedTuple):
41
+ alias: str
42
+ command: str
43
+ description: list[str]
44
+
45
+
46
+ DETAILED_HELP_TEXT = [
47
+ CommandHelp(
48
+ alias="r",
49
+ command="rebuild",
50
+ description=[
51
+ ("Rebuild the project with the latest changes from source files."),
52
+ ],
53
+ ),
54
+ CommandHelp(
55
+ alias="d",
56
+ command="decode <message_code>",
57
+ description=[
58
+ (
59
+ "Decode a debug message code to its original text with a stack trace. "
60
+ "Message codes are generated by assert statements, debug.error(), and debug.notify() calls. "
61
+ "The game is automatically paused when these are triggered in debug mode and the message code "
62
+ "can be found in the debug log."
63
+ ),
64
+ "Example: d 42",
65
+ ],
66
+ ),
67
+ CommandHelp(
68
+ alias="h",
69
+ command="help",
70
+ description=[
71
+ "Show this help message.",
72
+ ],
73
+ ),
74
+ CommandHelp(
75
+ alias="q",
76
+ command="quit",
77
+ description=[
78
+ "Exit the development server.",
79
+ ],
80
+ ),
81
+ ]
82
+
83
+
84
+ @dataclass
85
+ class ServerState:
86
+ project: Project
87
+ project_module_name: str
88
+ core_module_names: set[str]
89
+ build_dir: Path
90
+ config: BuildConfig
91
+ cache: CompileCache
92
+ project_state: ProjectContextState
93
+
94
+
36
95
  class Command(Protocol):
37
- def execute(
38
- self,
39
- project_module_name: str,
40
- core_module_names: set[str],
41
- build_dir: Path,
42
- config: BuildConfig,
43
- cache: CompileCache,
44
- ) -> None: ...
96
+ def execute(self, server_state: ServerState) -> None: ...
45
97
 
46
98
 
47
99
  @dataclass
48
100
  class RebuildCommand:
49
- def execute(
50
- self,
51
- project_module_name: str,
52
- core_module_names: set[str],
53
- build_dir: Path,
54
- config: BuildConfig,
55
- cache: CompileCache,
56
- ):
101
+ def execute(self, server_state: ServerState):
57
102
  from sonolus.build.cli import build_collection
58
103
 
59
104
  for module_name in tuple(sys.modules):
60
- if module_name not in core_module_names:
105
+ if module_name not in server_state.core_module_names:
61
106
  del sys.modules[module_name]
62
107
 
63
108
  try:
64
- project_module = importlib.import_module(project_module_name)
109
+ project_module = importlib.import_module(server_state.project_module_name)
65
110
  except Exception:
66
111
  print(traceback.format_exc())
67
112
  return
@@ -72,7 +117,15 @@ class RebuildCommand:
72
117
  print("Rebuilding...")
73
118
  try:
74
119
  start_time = perf_counter()
75
- build_collection(project_module.project, build_dir, config, cache=cache)
120
+ server_state.project_state = ProjectContextState(dev=True)
121
+ server_state.project = project_module.project
122
+ build_collection(
123
+ server_state.project,
124
+ server_state.build_dir,
125
+ server_state.config,
126
+ cache=server_state.cache,
127
+ project_state=server_state.project_state,
128
+ )
76
129
  end_time = perf_counter()
77
130
  print(f"Rebuild completed in {end_time - start_time:.2f} seconds")
78
131
  except CompilationError:
@@ -80,16 +133,47 @@ class RebuildCommand:
80
133
  print_simple_traceback(*exc_info)
81
134
 
82
135
 
136
+ @dataclass
137
+ class DecodeCommand:
138
+ message_code: int
139
+
140
+ def execute(self, server_state: ServerState):
141
+ debug_str_mappings = server_state.project_state.debug_str_mappings
142
+ message = next((msg for msg, code in debug_str_mappings.items() if code == self.message_code), None)
143
+
144
+ if message is not None:
145
+ print(message)
146
+ else:
147
+ print(f"Unknown message code: {self.message_code}")
148
+
149
+
150
+ @dataclass
151
+ class HelpCommand:
152
+ def execute(self, server_state: ServerState):
153
+ terminal_width = shutil.get_terminal_size().columns
154
+ max_width = min(terminal_width, 120)
155
+
156
+ print("Available Commands:\n")
157
+
158
+ for entry in DETAILED_HELP_TEXT:
159
+ print(f"[{entry.alias}] {entry.command}")
160
+
161
+ for paragraph in entry.description:
162
+ initial_indent = " "
163
+ subsequent_indent = " "
164
+ wrapped = textwrap.fill(
165
+ paragraph,
166
+ width=max_width - len(initial_indent),
167
+ initial_indent=initial_indent,
168
+ subsequent_indent=subsequent_indent,
169
+ )
170
+ print(wrapped)
171
+ print()
172
+
173
+
83
174
  @dataclass
84
175
  class ExitCommand:
85
- def execute(
86
- self,
87
- project_module_name: str,
88
- core_module_names: set[str],
89
- build_dir: Path,
90
- config: BuildConfig,
91
- cache: CompileCache,
92
- ):
176
+ def execute(self, server_state: ServerState):
93
177
  print("Exiting...")
94
178
  sys.exit(0)
95
179
 
@@ -99,12 +183,19 @@ def parse_dev_command(command_line: str) -> Command | None:
99
183
  subparsers = parser.add_subparsers(dest="cmd")
100
184
 
101
185
  subparsers.add_parser("rebuild", aliases=["r"])
186
+ decode_parser = subparsers.add_parser("decode", aliases=["d"])
187
+ decode_parser.add_argument("message_code", type=int, help="Message code to decode")
188
+ subparsers.add_parser("help", aliases=["h"])
102
189
  subparsers.add_parser("quit", aliases=["q"])
103
190
 
104
191
  try:
105
192
  args = parser.parse_args(shlex.split(command_line))
106
193
  if args.cmd in {"rebuild", "r"}:
107
194
  return RebuildCommand()
195
+ elif args.cmd in {"decode", "d"}:
196
+ return DecodeCommand(message_code=args.message_code)
197
+ elif args.cmd in {"help", "h"}:
198
+ return HelpCommand()
108
199
  elif args.cmd in {"quit", "q"}:
109
200
  return ExitCommand()
110
201
  return None
@@ -164,8 +255,18 @@ def run_server(
164
255
  core_module_names: set[str] | None,
165
256
  build_dir: Path,
166
257
  config: BuildConfig,
167
- cache: CompileCache,
258
+ project,
168
259
  ):
260
+ from sonolus.build.cli import build_collection
261
+
262
+ cache = CompileCache()
263
+ project_state = ProjectContextState(dev=True)
264
+
265
+ start_time = perf_counter()
266
+ build_collection(project, build_dir, config, cache=cache, project_state=project_state)
267
+ end_time = perf_counter()
268
+ print(f"Build finished in {end_time - start_time:.2f}s")
269
+
169
270
  interactive = project_module_name is not None and core_module_names is not None
170
271
 
171
272
  class DirectoryHandler(http.server.SimpleHTTPRequestHandler):
@@ -187,6 +288,16 @@ def run_server(
187
288
  print(f" http://{ip}:{port}")
188
289
 
189
290
  if interactive:
291
+ server_state = ServerState(
292
+ project=project,
293
+ project_module_name=project_module_name,
294
+ core_module_names=core_module_names,
295
+ build_dir=build_dir,
296
+ config=config,
297
+ cache=cache,
298
+ project_state=project_state,
299
+ )
300
+
190
301
  threading.Thread(target=httpd.serve_forever, daemon=True).start()
191
302
 
192
303
  command_queue = queue.Queue()
@@ -202,12 +313,12 @@ def run_server(
202
313
  while True:
203
314
  try:
204
315
  cmd = command_queue.get(timeout=0.5)
205
- cmd.execute(project_module_name, core_module_names, build_dir, config, cache)
316
+ cmd.execute(server_state)
206
317
  prompt_event.set()
207
318
  except queue.Empty:
208
319
  continue
209
320
  except KeyboardInterrupt:
210
- print("\nStopping server...")
321
+ print("Exiting...")
211
322
  sys.exit(0)
212
323
  finally:
213
324
  httpd.shutdown()