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

Files changed (77) hide show
  1. sonolus/backend/finalize.py +18 -10
  2. sonolus/backend/interpret.py +7 -7
  3. sonolus/backend/ir.py +24 -0
  4. sonolus/backend/optimize/__init__.py +0 -0
  5. sonolus/backend/{allocate.py → optimize/allocate.py} +4 -3
  6. sonolus/backend/{constant_evaluation.py → optimize/constant_evaluation.py} +7 -7
  7. sonolus/backend/{coalesce.py → optimize/copy_coalesce.py} +3 -3
  8. sonolus/backend/optimize/dead_code.py +185 -0
  9. sonolus/backend/{dominance.py → optimize/dominance.py} +2 -17
  10. sonolus/backend/{flow.py → optimize/flow.py} +6 -5
  11. sonolus/backend/{inlining.py → optimize/inlining.py} +4 -17
  12. sonolus/backend/{liveness.py → optimize/liveness.py} +69 -65
  13. sonolus/backend/optimize/optimize.py +44 -0
  14. sonolus/backend/{passes.py → optimize/passes.py} +1 -1
  15. sonolus/backend/optimize/simplify.py +191 -0
  16. sonolus/backend/{ssa.py → optimize/ssa.py} +31 -18
  17. sonolus/backend/place.py +17 -25
  18. sonolus/backend/utils.py +10 -0
  19. sonolus/backend/visitor.py +360 -101
  20. sonolus/build/compile.py +8 -8
  21. sonolus/build/engine.py +10 -5
  22. sonolus/script/archetype.py +419 -137
  23. sonolus/script/array.py +25 -8
  24. sonolus/script/array_like.py +297 -0
  25. sonolus/script/bucket.py +73 -11
  26. sonolus/script/containers.py +234 -51
  27. sonolus/script/debug.py +8 -8
  28. sonolus/script/easing.py +147 -105
  29. sonolus/script/effect.py +60 -0
  30. sonolus/script/engine.py +71 -4
  31. sonolus/script/globals.py +66 -32
  32. sonolus/script/instruction.py +79 -25
  33. sonolus/script/internal/builtin_impls.py +138 -27
  34. sonolus/script/internal/constant.py +139 -0
  35. sonolus/script/internal/context.py +14 -5
  36. sonolus/script/internal/dict_impl.py +65 -0
  37. sonolus/script/internal/generic.py +6 -9
  38. sonolus/script/internal/impl.py +38 -13
  39. sonolus/script/internal/introspection.py +5 -2
  40. sonolus/script/{math.py → internal/math_impls.py} +28 -28
  41. sonolus/script/internal/native.py +3 -3
  42. sonolus/script/internal/random.py +67 -0
  43. sonolus/script/internal/range.py +81 -0
  44. sonolus/script/internal/transient.py +51 -0
  45. sonolus/script/internal/tuple_impl.py +113 -0
  46. sonolus/script/interval.py +234 -16
  47. sonolus/script/iterator.py +120 -167
  48. sonolus/script/level.py +24 -0
  49. sonolus/script/num.py +79 -47
  50. sonolus/script/options.py +78 -12
  51. sonolus/script/particle.py +37 -4
  52. sonolus/script/pointer.py +4 -4
  53. sonolus/script/print.py +22 -1
  54. sonolus/script/project.py +8 -0
  55. sonolus/script/{graphics.py → quad.py} +75 -12
  56. sonolus/script/record.py +44 -13
  57. sonolus/script/runtime.py +50 -1
  58. sonolus/script/sprite.py +197 -112
  59. sonolus/script/text.py +2 -0
  60. sonolus/script/timing.py +72 -0
  61. sonolus/script/transform.py +296 -66
  62. sonolus/script/ui.py +134 -78
  63. sonolus/script/values.py +6 -13
  64. sonolus/script/vec.py +118 -3
  65. {sonolus_py-0.1.4.dist-info → sonolus_py-0.1.5.dist-info}/METADATA +1 -1
  66. sonolus_py-0.1.5.dist-info/RECORD +89 -0
  67. sonolus/backend/dead_code.py +0 -80
  68. sonolus/backend/optimize.py +0 -37
  69. sonolus/backend/simplify.py +0 -47
  70. sonolus/script/comptime.py +0 -160
  71. sonolus/script/random.py +0 -14
  72. sonolus/script/range.py +0 -58
  73. sonolus_py-0.1.4.dist-info/RECORD +0 -84
  74. /sonolus/script/{callbacks.py → internal/callbacks.py} +0 -0
  75. {sonolus_py-0.1.4.dist-info → sonolus_py-0.1.5.dist-info}/WHEEL +0 -0
  76. {sonolus_py-0.1.4.dist-info → sonolus_py-0.1.5.dist-info}/entry_points.txt +0 -0
  77. {sonolus_py-0.1.4.dist-info → sonolus_py-0.1.5.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,8 @@
1
1
  from collections import deque
2
2
 
3
- from sonolus.backend.flow import BasicBlock, traverse_cfg_preorder
4
3
  from sonolus.backend.ir import IRConst, IRGet, IRInstr, IRPureInstr, IRSet, IRStmt
5
- from sonolus.backend.passes import CompilerPass
4
+ from sonolus.backend.optimize.flow import BasicBlock, traverse_cfg_preorder
5
+ from sonolus.backend.optimize.passes import CompilerPass
6
6
  from sonolus.backend.place import BlockPlace, SSAPlace, TempBlock
7
7
 
8
8
  type HasLiveness = SSAPlace | TempBlock
@@ -15,9 +15,26 @@ class LivenessAnalysis(CompilerPass):
15
15
  def run(self, entry: BasicBlock) -> BasicBlock:
16
16
  self.preprocess(entry)
17
17
  self.process(entry)
18
- self.process_arrays(entry)
19
18
  return entry
20
19
 
20
+ def preprocess(self, entry: BasicBlock):
21
+ for block in traverse_cfg_preorder(entry):
22
+ block.live_out = None
23
+ block.live_in = set()
24
+ block.live_phi_targets = set()
25
+ block.array_defs_in = set()
26
+ block.array_defs_out = None
27
+ for statement in block.statements:
28
+ statement.live = set()
29
+ statement.visited = False
30
+ statement.uses = self.get_uses(statement, set())
31
+ statement.defs = self.get_defs(statement)
32
+ statement.is_array_init = False # True if this may be the first assignment to an array
33
+ statement.array_defs = self.get_array_defs(statement)
34
+ block.test.live = set()
35
+ block.test.uses = self.get_uses(block.test, set())
36
+ self.preprocess_arrays(entry)
37
+
21
38
  def process(self, entry: BasicBlock):
22
39
  queue = deque(self.get_exits(entry))
23
40
  if not queue:
@@ -27,101 +44,88 @@ class LivenessAnalysis(CompilerPass):
27
44
  updated_blocks = self.process_block(block)
28
45
  queue.extend(updated_blocks)
29
46
 
30
- def process_arrays(self, entry: BasicBlock):
31
- # With arrays, we can't assume that an assignment will render previous assignments dead.
32
- # Before this function is run, arrays are treated as live at a statement as long as they are read from
33
- # at some future point.
34
- # This function will mark arrays as dead if they could not have been assigned to yet.
35
- queue = deque([entry])
47
+ def preprocess_arrays(self, entry: BasicBlock):
48
+ queue = {entry}
49
+ visited = set()
36
50
  while queue:
37
- block = queue.popleft()
38
- if block.live_arrays_in is None:
39
- block.live_arrays_in = set()
40
- live_arrays_in = block.live_arrays_in.copy()
51
+ block = queue.pop()
52
+ array_defs = block.array_defs_in.copy()
53
+ is_first_visit = block not in visited
54
+ visited.add(block)
41
55
  for statement in block.statements:
42
- live_arrays_in.update(self.get_array_defs(statement))
43
- updated_blocks = []
44
- for edge in block.outgoing:
45
- if edge.dst.live_arrays_in is None:
46
- prev_size = -1
47
- edge.dst.live_arrays_in = set()
56
+ if statement.array_defs - array_defs:
57
+ statement.is_array_init = True
58
+ array_defs.update(statement.array_defs)
48
59
  else:
49
- prev_size = len(edge.dst.live_arrays_in)
50
- edge.dst.live_arrays_in.update(live_arrays_in)
51
- if len(edge.dst.live_arrays_in) != prev_size:
52
- updated_blocks.append(edge.dst)
53
- queue.extend(updated_blocks)
54
-
55
- for block in traverse_cfg_preorder(entry):
56
- live_arrays_in = block.live_arrays_in
57
- for statement in block.statements:
58
- if not self.can_skip(statement, statement.live):
59
- live_arrays_in.update(self.get_array_defs(statement))
60
- statement.live = {
61
- place
62
- for place in statement.live
63
- if not (isinstance(place, TempBlock) and place.size != 1 and place not in live_arrays_in)
64
- }
65
-
66
- def preprocess(self, entry: BasicBlock):
67
- for block in traverse_cfg_preorder(entry):
68
- block.live_out = None
69
- block.live_in = None
70
- block.live_phi_targets = None
71
- block.live_arrays_in = None
60
+ statement.is_array_init = False
61
+ if is_first_visit or array_defs != block.array_defs_out:
62
+ block.array_defs_out = array_defs
63
+ for edge in block.outgoing:
64
+ queue.add(edge.dst)
65
+ edge.dst.array_defs_in.update(array_defs)
72
66
 
73
67
  def process_block(self, block: BasicBlock) -> list[BasicBlock]:
74
68
  if block.live_out is None:
75
69
  block.live_out = set()
76
- live: set[HasLiveness] = block.live_out.copy()
77
- block.test.live = live.copy()
78
- live.update(self.get_uses(block.test))
70
+ live: set[HasLiveness] = {
71
+ place
72
+ for place in block.live_out
73
+ if not (isinstance(place, TempBlock) and place.size > 1 and place not in block.array_defs_out)
74
+ }
75
+ block.test.live.update(live)
76
+ live.update(block.test.uses)
79
77
  for statement in reversed(block.statements):
80
- statement.live = live.copy()
78
+ statement.live.update(live)
81
79
  if self.can_skip(statement, live):
82
80
  continue
83
- live.difference_update(self.get_defs(statement))
84
- live.update(self.get_uses(statement))
81
+ live.difference_update(statement.defs)
82
+ if statement.is_array_init:
83
+ live.difference_update(statement.array_defs)
84
+ live.update(statement.uses)
85
+ prev_sizes_by_block = {
86
+ edge.src: len(edge.src.live_out) if edge.src.live_out is not None else -1 for edge in block.incoming
87
+ }
85
88
  live_phi_targets = set()
86
89
  for target, args in block.phis.items():
87
90
  if target not in live:
88
91
  continue
89
- live.difference_update({target})
90
- live.update(args.values())
92
+ live.remove(target)
93
+ for src_block, arg in args.items():
94
+ if src_block.live_out is None:
95
+ src_block.live_out = set()
96
+ src_block.live_out.add(arg)
91
97
  live_phi_targets.add(target)
92
- block.live_in = live.copy()
98
+ block.live_in = live
93
99
  block.live_phi_targets = live_phi_targets
94
100
  updated_blocks = []
95
101
  for edge in block.incoming:
96
102
  if edge.src.live_out is None:
97
- prev_size = -1
98
103
  edge.src.live_out = set()
99
- else:
100
- prev_size = len(edge.src.live_out)
101
104
  edge.src.live_out.update(live)
102
- if len(edge.src.live_out) != prev_size:
105
+ if len(edge.src.live_out) != prev_sizes_by_block[edge.src]:
103
106
  updated_blocks.append(edge.src)
104
107
  return updated_blocks
105
108
 
106
- def get_uses(self, stmt: IRStmt | BlockPlace | SSAPlace | TempBlock | int) -> set[HasLiveness]:
107
- uses = set()
109
+ def get_uses(
110
+ self, stmt: IRStmt | BlockPlace | SSAPlace | TempBlock | int, uses: set[HasLiveness]
111
+ ) -> set[HasLiveness]:
108
112
  match stmt:
109
113
  case IRPureInstr(op=_, args=args) | IRInstr(op=_, args=args):
110
114
  for arg in args:
111
- uses.update(self.get_uses(arg))
115
+ self.get_uses(arg, uses)
112
116
  case IRGet(place=place):
113
- uses.update(self.get_uses(place))
117
+ self.get_uses(place, uses)
114
118
  case IRSet(place=place, value=value):
115
119
  if isinstance(place, BlockPlace):
116
120
  if not isinstance(place.block, TempBlock):
117
- uses.update(self.get_uses(place.block))
118
- uses.update(self.get_uses(place.index))
119
- uses.update(self.get_uses(value))
121
+ self.get_uses(place.block, uses)
122
+ self.get_uses(place.index, uses)
123
+ self.get_uses(value, uses)
120
124
  case IRConst() | int():
121
125
  pass
122
126
  case BlockPlace(block=block, index=index, offset=_):
123
- uses.update(self.get_uses(block))
124
- uses.update(self.get_uses(index))
127
+ self.get_uses(block, uses)
128
+ self.get_uses(index, uses)
125
129
  case SSAPlace() | TempBlock():
126
130
  uses.add(stmt)
127
131
  case _:
@@ -151,7 +155,7 @@ class LivenessAnalysis(CompilerPass):
151
155
  case IRSet(place=_, value=value):
152
156
  if isinstance(value, IRInstr) and value.op.side_effects:
153
157
  return False
154
- defs = self.get_defs(stmt) | self.get_array_defs(stmt)
158
+ defs = stmt.defs | stmt.array_defs
155
159
  return defs and not (defs & live)
156
160
  return False
157
161
 
@@ -0,0 +1,44 @@
1
+ from sonolus.backend.optimize.allocate import Allocate, AllocateBasic
2
+ from sonolus.backend.optimize.constant_evaluation import SparseConditionalConstantPropagation
3
+ from sonolus.backend.optimize.copy_coalesce import CopyCoalesce
4
+ from sonolus.backend.optimize.dead_code import (
5
+ AdvancedDeadCodeElimination,
6
+ DeadCodeElimination,
7
+ UnreachableCodeElimination,
8
+ )
9
+ from sonolus.backend.optimize.flow import BasicBlock
10
+ from sonolus.backend.optimize.inlining import InlineVars
11
+ from sonolus.backend.optimize.passes import run_passes
12
+ from sonolus.backend.optimize.simplify import CoalesceFlow, NormalizeSwitch, RewriteToSwitch
13
+ from sonolus.backend.optimize.ssa import FromSSA, ToSSA
14
+
15
+ MINIMAL_PASSES = [
16
+ CoalesceFlow(),
17
+ UnreachableCodeElimination(),
18
+ AllocateBasic(),
19
+ ]
20
+
21
+ STANDARD_PASSES = [
22
+ CoalesceFlow(),
23
+ UnreachableCodeElimination(),
24
+ DeadCodeElimination(),
25
+ ToSSA(),
26
+ SparseConditionalConstantPropagation(),
27
+ UnreachableCodeElimination(),
28
+ DeadCodeElimination(),
29
+ CoalesceFlow(),
30
+ InlineVars(),
31
+ DeadCodeElimination(),
32
+ RewriteToSwitch(),
33
+ FromSSA(),
34
+ CoalesceFlow(),
35
+ CopyCoalesce(),
36
+ AdvancedDeadCodeElimination(),
37
+ CoalesceFlow(),
38
+ NormalizeSwitch(),
39
+ Allocate(),
40
+ ]
41
+
42
+
43
+ def optimize_and_allocate(cfg: BasicBlock):
44
+ return run_passes(cfg, STANDARD_PASSES)
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from abc import ABC, abstractmethod
4
4
  from collections import deque
5
5
 
6
- from sonolus.backend.flow import BasicBlock
6
+ from sonolus.backend.optimize.flow import BasicBlock
7
7
 
8
8
 
9
9
  class CompilerPass(ABC):
@@ -0,0 +1,191 @@
1
+ from sonolus.backend.ir import IRConst, IRGet, IRPureInstr, IRSet
2
+ from sonolus.backend.ops import Op
3
+ from sonolus.backend.optimize.flow import BasicBlock, traverse_cfg_preorder
4
+ from sonolus.backend.optimize.passes import CompilerPass
5
+
6
+
7
+ class CoalesceFlow(CompilerPass):
8
+ def run(self, entry: BasicBlock) -> BasicBlock:
9
+ queue = [entry]
10
+ processed = set()
11
+ while queue:
12
+ block = queue.pop()
13
+ if block in processed:
14
+ continue
15
+ processed.add(block)
16
+ for edge in block.outgoing:
17
+ while True:
18
+ dst = edge.dst
19
+ if dst.phis or dst.statements or len(dst.outgoing) != 1 or dst is block:
20
+ break
21
+ next_dst = next(iter(dst.outgoing)).dst
22
+ if next_dst.phis:
23
+ break
24
+ dst.incoming.remove(edge)
25
+ if not dst.incoming:
26
+ for dst_edge in dst.outgoing:
27
+ dst_edge.dst.incoming.remove(dst_edge)
28
+ processed.add(dst)
29
+ edge.dst = next_dst
30
+ next_dst.incoming.add(edge)
31
+ if dst is edge.dst:
32
+ break
33
+ default_edge = next((edge for edge in block.outgoing if edge.cond is None), None)
34
+ if default_edge is not None:
35
+ for edge in [*block.outgoing]:
36
+ if edge is default_edge:
37
+ continue
38
+ if edge.dst is default_edge.dst:
39
+ block.outgoing.remove(edge)
40
+ edge.dst.incoming.remove(edge)
41
+ if len(block.outgoing) != 1:
42
+ queue.extend(edge.dst for edge in block.outgoing)
43
+ continue
44
+ next_block = next(iter(block.outgoing)).dst
45
+ if len(next_block.incoming) != 1:
46
+ queue.append(next_block)
47
+ if not block.statements and not block.phis and not next_block.phis:
48
+ for edge in block.incoming:
49
+ edge.dst = next_block
50
+ next_block.incoming.add(edge)
51
+ for edge in block.outgoing: # There should be exactly one
52
+ next_block.incoming.remove(edge)
53
+ if block is entry:
54
+ entry = next_block
55
+ continue
56
+ for p, args in next_block.phis.items():
57
+ if block not in args:
58
+ continue
59
+ block.statements.append(IRSet(p, IRGet(args[block])))
60
+ block.statements.extend(next_block.statements)
61
+ block.test = next_block.test
62
+ block.outgoing = next_block.outgoing
63
+ for edge in block.outgoing:
64
+ edge.src = block
65
+ dst = edge.dst
66
+ for args in dst.phis.values():
67
+ if next_block in args:
68
+ args[block] = args.pop(next_block)
69
+ processed.add(next_block)
70
+ queue.extend(edge.dst for edge in block.outgoing)
71
+ processed.remove(block)
72
+ queue.append(block)
73
+ return entry
74
+
75
+
76
+ class RewriteToSwitch(CompilerPass):
77
+ """Rewrite if-else chains to switch statements.
78
+
79
+ Note that this needs inlining (and dead code elimination) to be run first to really do anything useful.
80
+ """
81
+
82
+ def run(self, entry: BasicBlock) -> BasicBlock:
83
+ self.ifs_to_switch(entry)
84
+ self.combine_blocks(entry)
85
+ self.remove_unreachable(entry)
86
+ return entry
87
+
88
+ def ifs_to_switch(self, entry: BasicBlock):
89
+ for block in traverse_cfg_preorder(entry):
90
+ if len(block.outgoing) != 2 or {edge.cond for edge in block.outgoing} != {None, 0}:
91
+ continue
92
+ test = block.test
93
+ if not isinstance(test, IRPureInstr) or test.op != Op.Equal:
94
+ continue
95
+ assert len(test.args) == 2
96
+ if isinstance(test.args[0], IRConst):
97
+ const, other = test.args
98
+ elif isinstance(test.args[1], IRConst):
99
+ other, const = test.args
100
+ else:
101
+ continue
102
+ block.test = other
103
+ for edge in block.outgoing:
104
+ if edge.cond is None:
105
+ edge.cond = const.value
106
+ else:
107
+ edge.cond = None
108
+
109
+ def combine_blocks(self, entry: BasicBlock):
110
+ queue = [entry]
111
+ processed = set()
112
+ while queue:
113
+ block = queue.pop()
114
+ if block in processed:
115
+ continue
116
+ processed.add(block)
117
+ queue.extend(edge.dst for edge in block.outgoing)
118
+
119
+ default_edge = next((edge for edge in block.outgoing if edge.cond is None), None)
120
+ if default_edge is None:
121
+ continue
122
+
123
+ next_block = default_edge.dst
124
+ if (
125
+ len(next_block.incoming) > 1
126
+ or next_block.statements
127
+ or next_block.phis
128
+ or block.test != next_block.test
129
+ or block is next_block
130
+ or next_block is entry
131
+ ):
132
+ continue
133
+
134
+ outgoing_by_cond = {edge.cond: edge for edge in block.outgoing}
135
+ assert len(outgoing_by_cond) == len(block.outgoing)
136
+ outgoing_by_cond.pop(None)
137
+ for edge in next_block.outgoing:
138
+ if edge.cond in outgoing_by_cond:
139
+ # This edge is unreachable since an equivalent edge would have been taken
140
+ edge.dst.incoming.remove(edge)
141
+ continue
142
+ outgoing_by_cond[edge.cond] = edge
143
+ edge.src = block
144
+ for args in edge.dst.phis.values():
145
+ if next_block in args:
146
+ args[block] = args.pop(next_block)
147
+ block.outgoing = set(outgoing_by_cond.values())
148
+ processed.add(next_block)
149
+ queue.append(block)
150
+ processed.remove(block)
151
+
152
+ def remove_unreachable(self, entry: BasicBlock):
153
+ reachable = {*traverse_cfg_preorder(entry)}
154
+ for block in traverse_cfg_preorder(entry):
155
+ block.incoming = {edge for edge in block.incoming if edge.src in reachable}
156
+ block.outgoing = {edge for edge in block.outgoing if edge.dst in reachable}
157
+
158
+
159
+ class NormalizeSwitch(CompilerPass):
160
+ """Normalize branches like cond -> case a, case a + b, case a + 2b to ((cond - a) / b) -> case 0, case 1, case 2."""
161
+
162
+ def run(self, entry: BasicBlock) -> BasicBlock:
163
+ for block in traverse_cfg_preorder(entry):
164
+ cases = {edge.cond for edge in block.outgoing}
165
+ if len(cases) <= 2:
166
+ continue
167
+ assert None in cases, "Non-terminal blocks should always have a default edge"
168
+ cases.remove(None)
169
+ offset, stride = self.get_offset_stride(cases)
170
+ if offset is None or (offset == 0 and stride == 1):
171
+ continue
172
+ for edge in block.outgoing:
173
+ if edge.cond is None:
174
+ continue
175
+ edge.cond = (edge.cond - offset) // stride
176
+ if offset != 0:
177
+ block.test = IRPureInstr(Op.Subtract, [block.test, IRConst(offset)])
178
+ if stride != 1:
179
+ block.test = IRPureInstr(Op.Divide, [block.test, IRConst(stride)])
180
+ return entry
181
+
182
+ def get_offset_stride(self, cases: set[int]) -> tuple[int | None, int | None]:
183
+ cases = sorted(cases)
184
+ offset = cases[0]
185
+ stride = cases[1] - offset
186
+ if int(offset) != offset or int(stride) != stride:
187
+ return None, None
188
+ for i, case in enumerate(cases[2:], 2):
189
+ if case != offset + i * stride:
190
+ return None, None
191
+ return offset, stride
@@ -1,7 +1,7 @@
1
- from sonolus.backend.dominance import DominanceFrontiers, get_df, get_dom_children
2
- from sonolus.backend.flow import BasicBlock, FlowEdge, traverse_cfg_preorder
3
1
  from sonolus.backend.ir import IRConst, IRGet, IRInstr, IRPureInstr, IRSet, IRStmt
4
- from sonolus.backend.passes import CompilerPass
2
+ from sonolus.backend.optimize.dominance import DominanceFrontiers, get_df, get_dom_children
3
+ from sonolus.backend.optimize.flow import BasicBlock, FlowEdge, traverse_cfg_preorder
4
+ from sonolus.backend.optimize.passes import CompilerPass
5
5
  from sonolus.backend.place import BlockPlace, SSAPlace, TempBlock
6
6
 
7
7
 
@@ -23,13 +23,14 @@ class ToSSA(CompilerPass):
23
23
  ssa_places: dict[TempBlock, list[SSAPlace]],
24
24
  used: dict[str, int],
25
25
  ):
26
- original_ssa_place_lens = {var: len(ssa_places[var]) for var in defs}
26
+ to_pop = []
27
27
  for var, args in [*block.phis.items()]:
28
28
  if isinstance(var, SSAPlace):
29
29
  continue
30
30
  ssa_places[var].append(self.get_new_ssa_place(var.name, used))
31
+ to_pop.append(var)
31
32
  block.phis[ssa_places[var][-1]] = args
32
- block.statements = [self.rename_stmt(stmt, ssa_places, used) for stmt in block.statements]
33
+ block.statements = [self.rename_stmt(stmt, ssa_places, used, to_pop) for stmt in block.statements]
33
34
  for edge in block.outgoing:
34
35
  dst = edge.dst
35
36
  for var, args in dst.phis.items():
@@ -37,35 +38,47 @@ class ToSSA(CompilerPass):
37
38
  continue
38
39
  if ssa_places[var]:
39
40
  args[block] = ssa_places[var][-1]
40
- block.test = self.rename_stmt(block.test, ssa_places, used)
41
+ block.test = self.rename_stmt(block.test, ssa_places, used, to_pop)
41
42
  for dom_child in get_dom_children(block):
42
43
  self.rename(dom_child, defs, ssa_places, used)
43
- for var, length in original_ssa_place_lens.items():
44
- ssa_places[var] = ssa_places[var][:length]
44
+ for var in to_pop:
45
+ ssa_places[var].pop()
45
46
 
46
47
  def remove_placeholder_phis(self, entry: BasicBlock):
47
48
  for block in traverse_cfg_preorder(entry):
48
49
  block.phis = {var: args for var, args in block.phis.items() if isinstance(var, SSAPlace)}
49
50
 
50
- def rename_stmt(self, stmt: IRStmt, ssa_places: dict[TempBlock, list[SSAPlace]], used: dict[str, int]):
51
+ def rename_stmt(
52
+ self, stmt: IRStmt, ssa_places: dict[TempBlock, list[SSAPlace]], used: dict[str, int], to_pop: list[SSAPlace]
53
+ ):
51
54
  match stmt:
52
55
  case IRConst():
53
56
  return stmt
54
57
  case IRPureInstr(op=op, args=args):
55
- return IRPureInstr(op=op, args=[self.rename_stmt(arg, ssa_places, used) for arg in args])
58
+ return IRPureInstr(op=op, args=[self.rename_stmt(arg, ssa_places, used, to_pop) for arg in args])
56
59
  case IRInstr(op=op, args=args):
57
- return IRInstr(op=op, args=[self.rename_stmt(arg, ssa_places, used) for arg in args])
60
+ return IRInstr(op=op, args=[self.rename_stmt(arg, ssa_places, used, to_pop) for arg in args])
58
61
  case IRGet(place=place):
59
- return IRGet(place=self.rename_stmt(place, ssa_places, used))
62
+ return IRGet(place=self.rename_stmt(place, ssa_places, used, to_pop))
60
63
  case IRSet(place=place, value=value):
61
- value = self.rename_stmt(value, ssa_places, used)
64
+ value = self.rename_stmt(value, ssa_places, used, to_pop)
62
65
  if isinstance(place, BlockPlace) and isinstance(place.block, TempBlock) and place.block.size == 1:
63
66
  ssa_places[place.block].append(self.get_new_ssa_place(place.block.name, used))
64
- place = self.rename_stmt(place, ssa_places, used)
67
+ to_pop.append(place.block)
68
+ place = self.rename_stmt(place, ssa_places, used, to_pop)
65
69
  return IRSet(place=place, value=value)
66
70
  case SSAPlace():
67
71
  return stmt
68
72
  case TempBlock() if stmt.size == 1:
73
+ if stmt not in ssa_places or not ssa_places[stmt]:
74
+ # This is an access to a definitely undefined variable
75
+ # But it might not be reachable in reality, so we should allow it
76
+ # Maybe there should be an error if this still happens after optimization,
77
+ # but recovering the location of the error in the original code is hard.
78
+ # This can happen in places like matching a VarArray[Num, 1] which was just created.
79
+ # IR generation won't immediately fold a check that size > 0 to false, so here we
80
+ # might see an access to uninitialized memory even though it's not reachable in reality.
81
+ return SSAPlace("err", 0)
69
82
  return ssa_places[stmt][-1]
70
83
  case TempBlock():
71
84
  return stmt
@@ -73,11 +86,11 @@ class ToSSA(CompilerPass):
73
86
  return stmt
74
87
  case BlockPlace(block=block, index=index, offset=offset):
75
88
  if isinstance(block, TempBlock) and block.size == 1:
76
- return self.rename_stmt(block, ssa_places, used)
89
+ return self.rename_stmt(block, ssa_places, used, to_pop)
77
90
  return BlockPlace(
78
- block=self.rename_stmt(block, ssa_places, used),
79
- index=self.rename_stmt(index, ssa_places, used),
80
- offset=self.rename_stmt(offset, ssa_places, used),
91
+ block=self.rename_stmt(block, ssa_places, used, to_pop),
92
+ index=self.rename_stmt(index, ssa_places, used, to_pop),
93
+ offset=self.rename_stmt(offset, ssa_places, used, to_pop),
81
94
  )
82
95
  case _:
83
96
  raise TypeError(f"Unexpected statement: {stmt}")
sonolus/backend/place.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from collections.abc import Iterator
2
- from typing import Self
2
+ from typing import NamedTuple, Self
3
3
 
4
4
  from sonolus.backend.blocks import Block
5
5
 
@@ -8,13 +8,9 @@ type BlockValue = Block | int | TempBlock | Place
8
8
  type IndexValue = int | Place
9
9
 
10
10
 
11
- class TempBlock:
11
+ class TempBlock(NamedTuple):
12
12
  name: str
13
- size: int
14
-
15
- def __init__(self, name: str, size: int = 1):
16
- self.name = name
17
- self.size = size
13
+ size: int = 1
18
14
 
19
15
  def __repr__(self):
20
16
  return f"TempBlock(name={self.name!r}, size={self.size!r})"
@@ -33,19 +29,14 @@ class TempBlock:
33
29
  return isinstance(other, TempBlock) and self.name == other.name and self.size == other.size
34
30
 
35
31
  def __hash__(self):
36
- return hash((self.name, self.size))
32
+ return hash(self.name) # Typically will be unique by name alone
37
33
 
38
34
 
39
- class BlockPlace:
35
+ class BlockPlace(NamedTuple):
40
36
  block: BlockValue
41
- index: IndexValue
37
+ index: IndexValue = 0
42
38
  offset: int = 0
43
39
 
44
- def __init__(self, block: BlockValue, index: IndexValue = 0, offset: int = 0):
45
- self.block = block
46
- self.index = index
47
- self.offset = offset
48
-
49
40
  def __repr__(self):
50
41
  return f"BlockPlace(block={self.block!r}, index={self.index!r}, offset={self.offset!r})"
51
42
 
@@ -59,24 +50,25 @@ class BlockPlace:
59
50
  else:
60
51
  return f"{self.block}[{self.index} + {self.offset}]"
61
52
 
53
+ def add_offset(self, offset: int) -> Self:
54
+ return BlockPlace(self.block, self.index, self.offset + offset)
55
+
62
56
  def __eq__(self, other):
63
- return isinstance(other, BlockPlace) and self.block == other.block and self.index == other.index
57
+ return (
58
+ isinstance(other, BlockPlace)
59
+ and self.block == other.block
60
+ and self.index == other.index
61
+ and self.offset == other.offset
62
+ )
64
63
 
65
64
  def __hash__(self):
66
- return hash((self.block, self.index))
65
+ return hash((self.block, self.index, self.offset))
67
66
 
68
- def add_offset(self, offset: int) -> Self:
69
- return BlockPlace(self.block, self.index, self.offset + offset)
70
67
 
71
-
72
- class SSAPlace:
68
+ class SSAPlace(NamedTuple):
73
69
  name: str
74
70
  num: int
75
71
 
76
- def __init__(self, name: str, num: int):
77
- self.name = name
78
- self.num = num
79
-
80
72
  def __repr__(self):
81
73
  return f"SSAPlace(name={self.name!r}, num={self.num!r})"
82
74
 
sonolus/backend/utils.py CHANGED
@@ -2,9 +2,11 @@
2
2
  import ast
3
3
  import inspect
4
4
  from collections.abc import Callable
5
+ from functools import cache
5
6
  from pathlib import Path
6
7
 
7
8
 
9
+ @cache
8
10
  def get_function(fn: Callable) -> tuple[str, ast.FunctionDef]:
9
11
  # This preserves both line number and column number in the returned node
10
12
  source_file = inspect.getsourcefile(fn)
@@ -26,6 +28,14 @@ class FindFunction(ast.NodeVisitor):
26
28
  else:
27
29
  self.generic_visit(node)
28
30
 
31
+ def visit_Lambda(self, node: ast.Lambda):
32
+ if node.lineno == self.line:
33
+ if self.node is not None:
34
+ raise ValueError("Multiple functions defined on the same line are not supported")
35
+ self.node = node
36
+ else:
37
+ self.generic_visit(node)
38
+
29
39
 
30
40
  def find_function(tree: ast.Module, line: int):
31
41
  visitor = FindFunction(line)