sonolus.py 0.1.8__py3-none-any.whl → 0.2.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (48) hide show
  1. sonolus/backend/finalize.py +2 -1
  2. sonolus/backend/optimize/constant_evaluation.py +2 -2
  3. sonolus/backend/optimize/copy_coalesce.py +16 -7
  4. sonolus/backend/optimize/optimize.py +12 -4
  5. sonolus/backend/optimize/passes.py +2 -1
  6. sonolus/backend/place.py +95 -14
  7. sonolus/backend/visitor.py +83 -12
  8. sonolus/build/cli.py +60 -9
  9. sonolus/build/collection.py +68 -26
  10. sonolus/build/compile.py +87 -30
  11. sonolus/build/engine.py +166 -40
  12. sonolus/build/level.py +2 -1
  13. sonolus/build/node.py +8 -1
  14. sonolus/build/project.py +30 -11
  15. sonolus/script/archetype.py +169 -51
  16. sonolus/script/array.py +12 -1
  17. sonolus/script/bucket.py +26 -8
  18. sonolus/script/debug.py +2 -2
  19. sonolus/script/effect.py +2 -2
  20. sonolus/script/engine.py +123 -15
  21. sonolus/script/internal/builtin_impls.py +21 -2
  22. sonolus/script/internal/constant.py +6 -2
  23. sonolus/script/internal/context.py +30 -25
  24. sonolus/script/internal/introspection.py +8 -1
  25. sonolus/script/internal/math_impls.py +2 -1
  26. sonolus/script/internal/transient.py +5 -1
  27. sonolus/script/internal/value.py +10 -2
  28. sonolus/script/interval.py +16 -0
  29. sonolus/script/iterator.py +17 -0
  30. sonolus/script/level.py +130 -8
  31. sonolus/script/metadata.py +32 -0
  32. sonolus/script/num.py +11 -2
  33. sonolus/script/options.py +5 -3
  34. sonolus/script/pointer.py +2 -0
  35. sonolus/script/project.py +41 -5
  36. sonolus/script/record.py +8 -3
  37. sonolus/script/runtime.py +61 -10
  38. sonolus/script/sprite.py +18 -1
  39. sonolus/script/ui.py +7 -3
  40. sonolus/script/values.py +8 -5
  41. sonolus/script/vec.py +28 -0
  42. {sonolus_py-0.1.8.dist-info → sonolus_py-0.2.0.dist-info}/METADATA +3 -2
  43. sonolus_py-0.2.0.dist-info/RECORD +90 -0
  44. {sonolus_py-0.1.8.dist-info → sonolus_py-0.2.0.dist-info}/WHEEL +1 -1
  45. sonolus_py-0.1.8.dist-info/RECORD +0 -89
  46. /sonolus/script/{print.py → printing.py} +0 -0
  47. {sonolus_py-0.1.8.dist-info → sonolus_py-0.2.0.dist-info}/entry_points.txt +0 -0
  48. {sonolus_py-0.1.8.dist-info → sonolus_py-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -42,6 +42,7 @@ def cfg_to_engine_node(entry: BasicBlock):
42
42
  for cond, target in targets.items():
43
43
  if cond is None:
44
44
  default = block_indexes[target]
45
+ continue
45
46
  args.append(ConstantNode(value=cond))
46
47
  args.append(ConstantNode(value=block_indexes[target]))
47
48
  args.append(ConstantNode(value=default))
@@ -55,7 +56,7 @@ def ir_to_engine_node(stmt) -> EngineNode:
55
56
  match stmt:
56
57
  case int() | float():
57
58
  return ConstantNode(value=float(stmt))
58
- case IRConst(value=value):
59
+ case IRConst(value=int(value) | float(value)):
59
60
  return ConstantNode(value=value)
60
61
  case IRPureInstr(op=op, args=args) | IRInstr(op=op, args=args):
61
62
  return FunctionNode(func=op, args=[ir_to_engine_node(arg) for arg in args])
@@ -315,8 +315,8 @@ class SparseConditionalConstantPropagation(CompilerPass):
315
315
  return 1
316
316
  return functools.reduce(operator.pow, args)
317
317
  case Op.Log:
318
- assert len(args) == 2
319
- return math.log(args[0], args[1])
318
+ assert len(args) == 1
319
+ return math.log(args[0])
320
320
  case Op.Ceil:
321
321
  assert len(args) == 1
322
322
  return math.ceil(args[0])
@@ -39,20 +39,29 @@ class CopyCoalesce(CompilerPass):
39
39
  interference = self.get_interference(entry)
40
40
  copies = self.get_copies(entry)
41
41
 
42
- mapping = {}
42
+ mapping: dict[TempBlock, set[TempBlock]] = {}
43
43
 
44
44
  for target, sources in copies.items():
45
45
  for source in sources:
46
- if source in mapping:
47
- continue
48
46
  if source in interference.get(target, set()):
49
47
  continue
50
- mapping[source] = mapping.get(target, target)
48
+ combined_mapping = mapping.get(target, {target}) | mapping.get(source, {source})
51
49
  combined_interference = interference.get(target, set()) | interference.get(source, set())
52
- interference[source] = combined_interference
53
- interference[target] = combined_interference
50
+ for place in combined_mapping:
51
+ mapping[place] = combined_mapping
52
+ interference[place] = combined_interference
53
+ for place in combined_interference:
54
+ interference[place].update(combined_mapping)
55
+
56
+ canonical_mapping = {}
57
+ for place, group in mapping.items():
58
+ if place in canonical_mapping:
59
+ continue
60
+ canonical = min(group)
61
+ for member in group:
62
+ canonical_mapping[member] = canonical
54
63
 
55
- return mapping
64
+ return canonical_mapping
56
65
 
57
66
  def get_interference(self, entry: BasicBlock) -> dict[TempBlock, set[TempBlock]]:
58
67
  result = {}
@@ -12,13 +12,21 @@ from sonolus.backend.optimize.passes import run_passes
12
12
  from sonolus.backend.optimize.simplify import CoalesceFlow, NormalizeSwitch, RewriteToSwitch
13
13
  from sonolus.backend.optimize.ssa import FromSSA, ToSSA
14
14
 
15
- MINIMAL_PASSES = [
15
+ MINIMAL_PASSES = (
16
16
  CoalesceFlow(),
17
17
  UnreachableCodeElimination(),
18
18
  AllocateBasic(),
19
- ]
19
+ )
20
20
 
21
- STANDARD_PASSES = [
21
+ FAST_PASSES = (
22
+ CoalesceFlow(),
23
+ UnreachableCodeElimination(),
24
+ AdvancedDeadCodeElimination(),
25
+ CoalesceFlow(),
26
+ Allocate(),
27
+ )
28
+
29
+ STANDARD_PASSES = (
22
30
  CoalesceFlow(),
23
31
  UnreachableCodeElimination(),
24
32
  DeadCodeElimination(),
@@ -37,7 +45,7 @@ STANDARD_PASSES = [
37
45
  CoalesceFlow(),
38
46
  NormalizeSwitch(),
39
47
  Allocate(),
40
- ]
48
+ )
41
49
 
42
50
 
43
51
  def optimize_and_allocate(cfg: BasicBlock):
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  from collections import deque
5
+ from collections.abc import Sequence
5
6
 
6
7
  from sonolus.backend.optimize.flow import BasicBlock
7
8
 
@@ -35,7 +36,7 @@ class CompilerPass(ABC):
35
36
  pass
36
37
 
37
38
 
38
- def run_passes(entry: BasicBlock, passes: list[CompilerPass]) -> BasicBlock:
39
+ def run_passes(entry: BasicBlock, passes: Sequence[CompilerPass]) -> BasicBlock:
39
40
  active_passes = set()
40
41
  queue = deque(passes)
41
42
  while queue:
sonolus/backend/place.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from collections.abc import Iterator
2
- from typing import NamedTuple, Self
2
+ from typing import Self
3
3
 
4
4
  from sonolus.backend.blocks import Block
5
5
 
@@ -8,9 +8,13 @@ type BlockValue = Block | int | TempBlock | Place
8
8
  type IndexValue = int | Place
9
9
 
10
10
 
11
- class TempBlock(NamedTuple):
12
- name: str
13
- size: int = 1
11
+ class TempBlock:
12
+ __slots__ = ("__hash", "name", "size")
13
+
14
+ def __init__(self, name: str, size: int = 1):
15
+ self.name = name
16
+ self.size = size
17
+ self.__hash = hash(name) # Precompute hash based on name alone
14
18
 
15
19
  def __repr__(self):
16
20
  return f"TempBlock(name={self.name!r}, size={self.size!r})"
@@ -28,14 +32,38 @@ class TempBlock(NamedTuple):
28
32
  def __eq__(self, other):
29
33
  return isinstance(other, TempBlock) and self.name == other.name and self.size == other.size
30
34
 
35
+ def __lt__(self, other):
36
+ if not isinstance(other, TempBlock):
37
+ return NotImplemented
38
+ return str(self) < str(other)
39
+
40
+ def __le__(self, other):
41
+ if not isinstance(other, TempBlock):
42
+ return NotImplemented
43
+ return str(self) <= str(other)
44
+
45
+ def __gt__(self, other):
46
+ if not isinstance(other, TempBlock):
47
+ return NotImplemented
48
+ return str(self) > str(other)
49
+
50
+ def __ge__(self, other):
51
+ if not isinstance(other, TempBlock):
52
+ return NotImplemented
53
+ return str(self) >= str(other)
54
+
31
55
  def __hash__(self):
32
- return hash(self.name) # Typically will be unique by name alone
56
+ return self.__hash
33
57
 
34
58
 
35
- class BlockPlace(NamedTuple):
36
- block: BlockValue
37
- index: IndexValue = 0
38
- offset: int = 0
59
+ class BlockPlace:
60
+ __slots__ = ("__hash", "block", "index", "offset")
61
+
62
+ def __init__(self, block: BlockValue, index: IndexValue = 0, offset: int = 0):
63
+ self.block = block
64
+ self.index = index
65
+ self.offset = offset
66
+ self.__hash = hash((block, index, offset))
39
67
 
40
68
  def __repr__(self):
41
69
  return f"BlockPlace(block={self.block!r}, index={self.index!r}, offset={self.offset!r})"
@@ -61,13 +89,42 @@ class BlockPlace(NamedTuple):
61
89
  and self.offset == other.offset
62
90
  )
63
91
 
92
+ def __lt__(self, other):
93
+ if not isinstance(other, BlockPlace):
94
+ return NotImplemented
95
+ return str(self) < str(other)
96
+
97
+ def __le__(self, other):
98
+ if not isinstance(other, BlockPlace):
99
+ return NotImplemented
100
+ return str(self) <= str(other)
101
+
102
+ def __gt__(self, other):
103
+ if not isinstance(other, BlockPlace):
104
+ return NotImplemented
105
+ return str(self) > str(other)
106
+
107
+ def __ge__(self, other):
108
+ if not isinstance(other, BlockPlace):
109
+ return NotImplemented
110
+ return str(self) >= str(other)
111
+
64
112
  def __hash__(self):
65
- return hash((self.block, self.index, self.offset))
113
+ return self.__hash
114
+
115
+ def __iter__(self):
116
+ yield self.block
117
+ yield self.index
118
+ yield self.offset
66
119
 
67
120
 
68
- class SSAPlace(NamedTuple):
69
- name: str
70
- num: int
121
+ class SSAPlace:
122
+ __slots__ = ("__hash", "name", "num")
123
+
124
+ def __init__(self, name: str, num: int):
125
+ self.name = name
126
+ self.num = num
127
+ self.__hash = hash((name, num))
71
128
 
72
129
  def __repr__(self):
73
130
  return f"SSAPlace(name={self.name!r}, num={self.num!r})"
@@ -78,5 +135,29 @@ class SSAPlace(NamedTuple):
78
135
  def __eq__(self, other):
79
136
  return isinstance(other, SSAPlace) and self.name == other.name and self.num == other.num
80
137
 
138
+ def __lt__(self, other):
139
+ if not isinstance(other, SSAPlace):
140
+ return NotImplemented
141
+ return str(self) < str(other)
142
+
143
+ def __le__(self, other):
144
+ if not isinstance(other, SSAPlace):
145
+ return NotImplemented
146
+ return str(self) <= str(other)
147
+
148
+ def __gt__(self, other):
149
+ if not isinstance(other, SSAPlace):
150
+ return NotImplemented
151
+ return str(self) > str(other)
152
+
153
+ def __ge__(self, other):
154
+ if not isinstance(other, SSAPlace):
155
+ return NotImplemented
156
+ return str(self) >= str(other)
157
+
81
158
  def __hash__(self):
82
- return hash((self.name, self.num))
159
+ return self.__hash
160
+
161
+ def __iter__(self):
162
+ yield self.name
163
+ yield self.num
@@ -172,7 +172,9 @@ class Visitor(ast.NodeVisitor):
172
172
  bound_args: inspect.BoundArguments
173
173
  used_names: dict[str, int]
174
174
  return_ctxs: list[Context] # Contexts at return statements, which will branch to the exit
175
- loop_head_ctxs: list[Context] # Contexts at loop heads, from outer to inner
175
+ loop_head_ctxs: list[
176
+ Context | list[Context]
177
+ ] # Contexts at loop heads, from outer to inner. Contains a list for unrolled (tuple) loops
176
178
  break_ctxs: list[list[Context]] # Contexts at break statements, from outer to inner
177
179
  active_ctx: Context | None # The active context for use in nested functions=
178
180
  parent: Self | None # The parent visitor for use in nested functions
@@ -203,6 +205,8 @@ class Visitor(ast.NodeVisitor):
203
205
  case ast.FunctionDef(body=body):
204
206
  ctx().scope.set_value("$return", validate_value(None))
205
207
  for stmt in body:
208
+ if not ctx().live:
209
+ break
206
210
  self.visit(stmt)
207
211
  case ast.Lambda(body=body):
208
212
  result = self.visit(body)
@@ -318,11 +322,21 @@ class Visitor(ast.NodeVisitor):
318
322
  iterable = self.visit(node.iter)
319
323
  if isinstance(iterable, TupleImpl):
320
324
  # Unroll the loop
325
+ break_ctxs = []
321
326
  for value in iterable.value:
322
327
  set_ctx(ctx().branch(None))
328
+ self.loop_head_ctxs.append([])
329
+ self.break_ctxs.append([])
323
330
  self.handle_assign(node.target, validate_value(value))
324
331
  for stmt in node.body:
332
+ if not ctx().live:
333
+ break
325
334
  self.visit(stmt)
335
+ continue_ctxs = [*self.loop_head_ctxs.pop(), ctx()]
336
+ break_ctxs.extend(self.break_ctxs.pop())
337
+ set_ctx(Context.meet(continue_ctxs))
338
+ if break_ctxs:
339
+ set_ctx(Context.meet([*break_ctxs, ctx()]))
326
340
  return
327
341
  iterator = self.handle_call(node, iterable.__iter__)
328
342
  if not isinstance(iterator, SonolusIterator):
@@ -335,7 +349,11 @@ class Visitor(ast.NodeVisitor):
335
349
  has_next = self.ensure_boolean_num(self.handle_call(node, iterator.has_next))
336
350
  if has_next._is_py_() and not has_next._as_py_():
337
351
  # The loop will never run, continue after evaluating the condition
352
+ self.loop_head_ctxs.pop()
353
+ self.break_ctxs.pop()
338
354
  for stmt in node.orelse:
355
+ if not ctx().live:
356
+ break
339
357
  self.visit(stmt)
340
358
  return
341
359
  ctx().test = has_next.ir()
@@ -345,16 +363,21 @@ class Visitor(ast.NodeVisitor):
345
363
  set_ctx(body_ctx)
346
364
  self.handle_assign(node.target, self.handle_call(node, iterator.next))
347
365
  for stmt in node.body:
366
+ if not ctx().live:
367
+ break
348
368
  self.visit(stmt)
349
369
  ctx().branch_to_loop_header(header_ctx)
350
370
 
371
+ self.loop_head_ctxs.pop()
372
+ break_ctxs = self.break_ctxs.pop()
373
+
351
374
  set_ctx(else_ctx)
352
375
  for stmt in node.orelse:
376
+ if not ctx().live:
377
+ break
353
378
  self.visit(stmt)
354
379
  else_end_ctx = ctx()
355
380
 
356
- self.loop_head_ctxs.pop()
357
- break_ctxs = self.break_ctxs.pop()
358
381
  after_ctx = Context.meet([else_end_ctx, *break_ctxs])
359
382
  set_ctx(after_ctx)
360
383
 
@@ -365,27 +388,55 @@ class Visitor(ast.NodeVisitor):
365
388
  self.break_ctxs.append([])
366
389
  set_ctx(header_ctx)
367
390
  test = self.ensure_boolean_num(self.visit(node.test))
368
- if test._is_py_() and not test._as_py_():
369
- # The loop will never run, continue after evaluating the condition
370
- for stmt in node.orelse:
371
- self.visit(stmt)
372
- return
391
+ if test._is_py_():
392
+ if test._as_py_():
393
+ # The loop will run until a break / return
394
+ body_ctx = ctx().branch(None)
395
+ set_ctx(body_ctx)
396
+ for stmt in node.body:
397
+ if not ctx().live:
398
+ break
399
+ self.visit(stmt)
400
+ ctx().branch_to_loop_header(header_ctx)
401
+
402
+ self.loop_head_ctxs.pop()
403
+ break_ctxs = self.break_ctxs.pop()
404
+
405
+ # Skip the else block
406
+
407
+ after_ctx = Context.meet([ctx().into_dead(), *break_ctxs])
408
+ set_ctx(after_ctx)
409
+ return
410
+ else:
411
+ # The loop will never run, continue after evaluating the condition
412
+ self.loop_head_ctxs.pop()
413
+ self.break_ctxs.pop()
414
+ for stmt in node.orelse:
415
+ if not ctx().live:
416
+ break
417
+ self.visit(stmt)
418
+ return
373
419
  ctx().test = test.ir()
374
420
  body_ctx = ctx().branch(None)
375
421
  else_ctx = ctx().branch(0)
376
422
 
377
423
  set_ctx(body_ctx)
378
424
  for stmt in node.body:
425
+ if not ctx().live:
426
+ break
379
427
  self.visit(stmt)
380
428
  ctx().branch_to_loop_header(header_ctx)
381
429
 
430
+ self.loop_head_ctxs.pop()
431
+ break_ctxs = self.break_ctxs.pop()
432
+
382
433
  set_ctx(else_ctx)
383
434
  for stmt in node.orelse:
435
+ if not ctx().live:
436
+ break
384
437
  self.visit(stmt)
385
438
  else_end_ctx = ctx()
386
439
 
387
- self.loop_head_ctxs.pop()
388
- break_ctxs = self.break_ctxs.pop()
389
440
  after_ctx = Context.meet([else_end_ctx, *break_ctxs])
390
441
  set_ctx(after_ctx)
391
442
 
@@ -395,9 +446,13 @@ class Visitor(ast.NodeVisitor):
395
446
  if test._is_py_():
396
447
  if test._as_py_():
397
448
  for stmt in node.body:
449
+ if not ctx().live:
450
+ break
398
451
  self.visit(stmt)
399
452
  else:
400
453
  for stmt in node.orelse:
454
+ if not ctx().live:
455
+ break
401
456
  self.visit(stmt)
402
457
  return
403
458
 
@@ -408,11 +463,15 @@ class Visitor(ast.NodeVisitor):
408
463
 
409
464
  set_ctx(true_ctx)
410
465
  for stmt in node.body:
466
+ if not ctx().live:
467
+ break
411
468
  self.visit(stmt)
412
469
  true_end_ctx = ctx()
413
470
 
414
471
  set_ctx(false_ctx)
415
472
  for stmt in node.orelse:
473
+ if not ctx().live:
474
+ break
416
475
  self.visit(stmt)
417
476
  false_end_ctx = ctx()
418
477
 
@@ -439,6 +498,8 @@ class Visitor(ast.NodeVisitor):
439
498
  if guard._is_py_():
440
499
  if guard._as_py_():
441
500
  for stmt in case.body:
501
+ if not ctx().live:
502
+ break
442
503
  self.visit(stmt)
443
504
  end_ctxs.append(ctx())
444
505
  else:
@@ -450,6 +511,8 @@ class Visitor(ast.NodeVisitor):
450
511
  guard_false_ctx = ctx().branch(0)
451
512
  set_ctx(guard_true_ctx)
452
513
  for stmt in case.body:
514
+ if not ctx().live:
515
+ break
453
516
  self.visit(stmt)
454
517
  end_ctxs.append(ctx())
455
518
  false_ctx = Context.meet([false_ctx, guard_false_ctx])
@@ -610,7 +673,11 @@ class Visitor(ast.NodeVisitor):
610
673
  set_ctx(ctx().into_dead())
611
674
 
612
675
  def visit_Continue(self, node):
613
- ctx().branch_to_loop_header(self.loop_head_ctxs[-1])
676
+ loop_head = self.loop_head_ctxs[-1]
677
+ if isinstance(loop_head, list):
678
+ loop_head.append(ctx())
679
+ else:
680
+ ctx().branch_to_loop_header(loop_head)
614
681
  set_ctx(ctx().into_dead())
615
682
 
616
683
  def visit_BoolOp(self, node) -> Value:
@@ -959,7 +1026,11 @@ class Visitor(ast.NodeVisitor):
959
1026
  if isinstance(target, ConstantValue):
960
1027
  # Unwrap so we can access fields
961
1028
  target = target._as_py_()
962
- descriptor = type(target).__dict__.get(key)
1029
+ descriptor = None
1030
+ for cls in type.mro(type(target)):
1031
+ descriptor = cls.__dict__.get(key, None)
1032
+ if descriptor is not None:
1033
+ break
963
1034
  match descriptor:
964
1035
  case property(fget=getter):
965
1036
  return self.handle_call(node, getter, target)
sonolus/build/cli.py CHANGED
@@ -10,10 +10,11 @@ import sys
10
10
  from pathlib import Path
11
11
  from time import perf_counter
12
12
 
13
+ from sonolus.backend.optimize.optimize import FAST_PASSES, MINIMAL_PASSES, STANDARD_PASSES
13
14
  from sonolus.build.engine import package_engine
14
15
  from sonolus.build.level import package_level_data
15
16
  from sonolus.build.project import build_project_to_collection, get_project_schema
16
- from sonolus.script.project import Project
17
+ from sonolus.script.project import BuildConfig, Project
17
18
 
18
19
 
19
20
  def find_default_module() -> str | None:
@@ -63,29 +64,29 @@ def import_project(module_path: str) -> Project | None:
63
64
  return project
64
65
  except Exception as e:
65
66
  print(f"Error: Failed to import project: {e}")
66
- return None
67
+ raise e from None
67
68
 
68
69
 
69
- def build_project(project: Project, build_dir: Path):
70
+ def build_project(project: Project, build_dir: Path, config: BuildConfig):
70
71
  dist_dir = build_dir / "dist"
71
72
  levels_dir = dist_dir / "levels"
72
73
  shutil.rmtree(dist_dir, ignore_errors=True)
73
74
  dist_dir.mkdir(parents=True, exist_ok=True)
74
75
  levels_dir.mkdir(parents=True, exist_ok=True)
75
76
 
76
- package_engine(project.engine.data).write(dist_dir / "engine")
77
+ package_engine(project.engine.data, config).write(dist_dir / "engine")
77
78
 
78
79
  for level in project.levels:
79
80
  level_path = levels_dir / level.name
80
81
  level_path.write_bytes(package_level_data(level.data))
81
82
 
82
83
 
83
- def build_collection(project: Project, build_dir: Path):
84
+ def build_collection(project: Project, build_dir: Path, config: BuildConfig):
84
85
  site_dir = build_dir / "site"
85
86
  shutil.rmtree(site_dir, ignore_errors=True)
86
87
  site_dir.mkdir(parents=True, exist_ok=True)
87
88
 
88
- collection = build_project_to_collection(project)
89
+ collection = build_project_to_collection(project, config)
89
90
  collection.write(site_dir)
90
91
 
91
92
 
@@ -125,10 +126,55 @@ def run_server(base_dir: Path, port: int = 8000):
125
126
  httpd.shutdown()
126
127
 
127
128
 
129
+ def get_config(args: argparse.Namespace) -> BuildConfig:
130
+ if hasattr(args, "optimize_minimal") and args.optimize_minimal:
131
+ optimization_passes = MINIMAL_PASSES
132
+ elif hasattr(args, "optimize_fast") and args.optimize_fast:
133
+ optimization_passes = FAST_PASSES
134
+ elif hasattr(args, "optimize_standard") and args.optimize_standard:
135
+ optimization_passes = STANDARD_PASSES
136
+ else:
137
+ optimization_passes = FAST_PASSES if args.command == "dev" else STANDARD_PASSES
138
+
139
+ if any(hasattr(args, attr) and getattr(args, attr) for attr in ["play", "watch", "preview", "tutorial"]):
140
+ build_play = hasattr(args, "play") and args.play
141
+ build_watch = hasattr(args, "watch") and args.watch
142
+ build_preview = hasattr(args, "preview") and args.preview
143
+ build_tutorial = hasattr(args, "tutorial") and args.tutorial
144
+ else:
145
+ build_play = build_watch = build_preview = build_tutorial = True
146
+
147
+ return BuildConfig(
148
+ passes=optimization_passes,
149
+ build_play=build_play,
150
+ build_watch=build_watch,
151
+ build_preview=build_preview,
152
+ build_tutorial=build_tutorial,
153
+ )
154
+
155
+
128
156
  def main():
129
157
  parser = argparse.ArgumentParser(description="Sonolus project build and development tools")
130
158
  subparsers = parser.add_subparsers(dest="command", required=True)
131
159
 
160
+ def add_common_arguments(parser):
161
+ optimization_group = parser.add_mutually_exclusive_group()
162
+ optimization_group.add_argument(
163
+ "-o0", "--optimize-minimal", action="store_true", help="Use minimal optimization passes"
164
+ )
165
+ optimization_group.add_argument(
166
+ "-o1", "--optimize-fast", action="store_true", help="Use fast optimization passes"
167
+ )
168
+ optimization_group.add_argument(
169
+ "-o2", "--optimize-standard", action="store_true", help="Use standard optimization passes"
170
+ )
171
+
172
+ build_components = parser.add_argument_group("build components")
173
+ build_components.add_argument("--play", action="store_true", help="Build play component")
174
+ build_components.add_argument("--watch", action="store_true", help="Build watch component")
175
+ build_components.add_argument("--preview", action="store_true", help="Build preview component")
176
+ build_components.add_argument("--tutorial", action="store_true", help="Build tutorial component")
177
+
132
178
  build_parser = subparsers.add_parser("build")
133
179
  build_parser.add_argument(
134
180
  "module",
@@ -137,6 +183,7 @@ def main():
137
183
  help="Module path (e.g., 'module.name'). If omitted, will auto-detect if only one module exists.",
138
184
  )
139
185
  build_parser.add_argument("--build-dir", type=str, default="./build")
186
+ add_common_arguments(build_parser)
140
187
 
141
188
  dev_parser = subparsers.add_parser("dev")
142
189
  dev_parser.add_argument(
@@ -147,6 +194,7 @@ def main():
147
194
  )
148
195
  dev_parser.add_argument("--build-dir", type=str, default="./build")
149
196
  dev_parser.add_argument("--port", type=int, default=8000)
197
+ add_common_arguments(dev_parser)
150
198
 
151
199
  schema_parser = subparsers.add_parser("schema")
152
200
  schema_parser.add_argument(
@@ -161,7 +209,8 @@ def main():
161
209
  if not args.module:
162
210
  default_module = find_default_module()
163
211
  if default_module:
164
- print(f"Using auto-detected module: {default_module}")
212
+ if args.command != "schema":
213
+ print(f"Using auto-detected module: {default_module}")
165
214
  args.module = default_module
166
215
  else:
167
216
  parser.error("Module argument is required when multiple or no modules are found")
@@ -173,13 +222,15 @@ def main():
173
222
  if args.command == "build":
174
223
  build_dir = Path(args.build_dir)
175
224
  start_time = perf_counter()
176
- build_project(project, build_dir)
225
+ config = get_config(args)
226
+ build_project(project, build_dir, config)
177
227
  end_time = perf_counter()
178
228
  print(f"Project built successfully to '{build_dir.resolve()}' in {end_time - start_time:.2f}s")
179
229
  elif args.command == "dev":
180
230
  build_dir = Path(args.build_dir)
181
231
  start_time = perf_counter()
182
- build_collection(project, build_dir)
232
+ config = get_config(args)
233
+ build_collection(project, build_dir, config)
183
234
  end_time = perf_counter()
184
235
  print(f"Build finished in {end_time - start_time:.2f}s")
185
236
  run_server(build_dir / "site", port=args.port)