sonolus.py 0.1.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 (75) hide show
  1. sonolus/__init__.py +0 -0
  2. sonolus/backend/__init__.py +0 -0
  3. sonolus/backend/allocate.py +51 -0
  4. sonolus/backend/blocks.py +756 -0
  5. sonolus/backend/excepthook.py +37 -0
  6. sonolus/backend/finalize.py +69 -0
  7. sonolus/backend/flow.py +92 -0
  8. sonolus/backend/interpret.py +333 -0
  9. sonolus/backend/ir.py +89 -0
  10. sonolus/backend/mode.py +24 -0
  11. sonolus/backend/node.py +40 -0
  12. sonolus/backend/ops.py +197 -0
  13. sonolus/backend/optimize.py +9 -0
  14. sonolus/backend/passes.py +6 -0
  15. sonolus/backend/place.py +90 -0
  16. sonolus/backend/simplify.py +30 -0
  17. sonolus/backend/utils.py +48 -0
  18. sonolus/backend/visitor.py +880 -0
  19. sonolus/build/__init__.py +0 -0
  20. sonolus/build/cli.py +170 -0
  21. sonolus/build/collection.py +293 -0
  22. sonolus/build/compile.py +90 -0
  23. sonolus/build/defaults.py +32 -0
  24. sonolus/build/engine.py +149 -0
  25. sonolus/build/level.py +23 -0
  26. sonolus/build/node.py +43 -0
  27. sonolus/build/project.py +94 -0
  28. sonolus/py.typed +0 -0
  29. sonolus/script/__init__.py +0 -0
  30. sonolus/script/archetype.py +651 -0
  31. sonolus/script/array.py +241 -0
  32. sonolus/script/bucket.py +192 -0
  33. sonolus/script/callbacks.py +105 -0
  34. sonolus/script/comptime.py +146 -0
  35. sonolus/script/containers.py +247 -0
  36. sonolus/script/debug.py +70 -0
  37. sonolus/script/effect.py +132 -0
  38. sonolus/script/engine.py +101 -0
  39. sonolus/script/globals.py +234 -0
  40. sonolus/script/graphics.py +141 -0
  41. sonolus/script/icon.py +73 -0
  42. sonolus/script/internal/__init__.py +5 -0
  43. sonolus/script/internal/builtin_impls.py +144 -0
  44. sonolus/script/internal/context.py +365 -0
  45. sonolus/script/internal/descriptor.py +17 -0
  46. sonolus/script/internal/error.py +15 -0
  47. sonolus/script/internal/generic.py +197 -0
  48. sonolus/script/internal/impl.py +69 -0
  49. sonolus/script/internal/introspection.py +14 -0
  50. sonolus/script/internal/native.py +38 -0
  51. sonolus/script/internal/value.py +144 -0
  52. sonolus/script/interval.py +98 -0
  53. sonolus/script/iterator.py +211 -0
  54. sonolus/script/level.py +52 -0
  55. sonolus/script/math.py +92 -0
  56. sonolus/script/num.py +382 -0
  57. sonolus/script/options.py +194 -0
  58. sonolus/script/particle.py +158 -0
  59. sonolus/script/pointer.py +30 -0
  60. sonolus/script/project.py +17 -0
  61. sonolus/script/range.py +58 -0
  62. sonolus/script/record.py +293 -0
  63. sonolus/script/runtime.py +526 -0
  64. sonolus/script/sprite.py +332 -0
  65. sonolus/script/text.py +404 -0
  66. sonolus/script/timing.py +42 -0
  67. sonolus/script/transform.py +118 -0
  68. sonolus/script/ui.py +160 -0
  69. sonolus/script/values.py +43 -0
  70. sonolus/script/vec.py +48 -0
  71. sonolus_py-0.1.0.dist-info/METADATA +10 -0
  72. sonolus_py-0.1.0.dist-info/RECORD +75 -0
  73. sonolus_py-0.1.0.dist-info/WHEEL +4 -0
  74. sonolus_py-0.1.0.dist-info/entry_points.txt +2 -0
  75. sonolus_py-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,37 @@
1
+ """Exception hook to filter out compiler internal frames from tracebacks."""
2
+
3
+ import sys
4
+ from types import TracebackType
5
+
6
+
7
+ def should_filter_traceback(tb: TracebackType | None) -> bool:
8
+ return tb is not None and (
9
+ tb.tb_frame.f_globals.get("_filter_traceback_", False) or should_filter_traceback(tb.tb_next)
10
+ )
11
+
12
+
13
+ def is_compiler_internal(tb: TracebackType):
14
+ return tb.tb_frame.f_locals.get("_compiler_internal_", False) or tb.tb_frame.f_globals.get(
15
+ "_compiler_internal_", False
16
+ )
17
+
18
+
19
+ def filter_traceback(tb: TracebackType | None) -> TracebackType | None:
20
+ if tb is None:
21
+ return None
22
+ if is_compiler_internal(tb):
23
+ return filter_traceback(tb.tb_next)
24
+ tb.tb_next = filter_traceback(tb.tb_next)
25
+ return tb
26
+
27
+
28
+ def excepthook(exc, value, tb):
29
+ import traceback
30
+
31
+ if should_filter_traceback(tb):
32
+ tb = filter_traceback(tb)
33
+ traceback.print_exception(exc, value, tb)
34
+
35
+
36
+ def install_excepthook():
37
+ sys.excepthook = excepthook
@@ -0,0 +1,69 @@
1
+ from sonolus.backend.flow import BasicBlock, traverse_cfg_preorder
2
+ from sonolus.backend.ir import IRConst, IRGet, IRInstr, IRPureInstr, IRSet
3
+ from sonolus.backend.node import ConstantNode, EngineNode, FunctionNode
4
+ from sonolus.backend.ops import Op
5
+ from sonolus.backend.place import BlockPlace
6
+
7
+
8
+ def cfg_to_engine_node(entry: BasicBlock):
9
+ block_indexes = {block: i for i, block in enumerate(traverse_cfg_preorder(entry))}
10
+ block_statements = []
11
+ for block in block_indexes:
12
+ statements = []
13
+ statements.extend(ir_to_engine_node(stmt) for stmt in block.statements)
14
+ outgoing = {edge.cond: edge.dst for edge in block.outgoing}
15
+ match outgoing:
16
+ case {**other} if not other:
17
+ statements.append(ConstantNode(value=len(block_indexes)))
18
+ case {None: target, **other} if not other:
19
+ statements.append(ConstantNode(value=block_indexes[target]))
20
+ case {0: f_branch, None: t_branch, **other} if not other:
21
+ statements.append(
22
+ FunctionNode(
23
+ func=Op.If,
24
+ args=[
25
+ ir_to_engine_node(block.test),
26
+ ConstantNode(value=block_indexes[t_branch]),
27
+ ConstantNode(value=block_indexes[f_branch]),
28
+ ],
29
+ )
30
+ )
31
+ case dict() as tgt:
32
+ args = [ir_to_engine_node(block.test)]
33
+ default = len(block_indexes)
34
+ for cond, target in tgt.items():
35
+ if cond is None:
36
+ default = block_indexes[target]
37
+ args.append(ConstantNode(value=cond))
38
+ args.append(ConstantNode(value=block_indexes[target]))
39
+ args.append(ConstantNode(value=default))
40
+ statements.append(FunctionNode(Op.SwitchWithDefault, args))
41
+ block_statements.append(FunctionNode(Op.Execute, statements))
42
+ block_statements.append(ConstantNode(value=0))
43
+ return FunctionNode(Op.Block, [FunctionNode(Op.JumpLoop, block_statements)])
44
+
45
+
46
+ def ir_to_engine_node(stmt) -> EngineNode:
47
+ match stmt:
48
+ case int() | float():
49
+ return ConstantNode(value=float(stmt))
50
+ case IRConst(value=value):
51
+ return ConstantNode(value=value)
52
+ case IRPureInstr(op=op, args=args) | IRInstr(op=op, args=args):
53
+ return FunctionNode(func=op, args=[ir_to_engine_node(arg) for arg in args])
54
+ case IRGet(place=place):
55
+ return ir_to_engine_node(place)
56
+ case BlockPlace() as place:
57
+ if place.offset == 0:
58
+ index = ir_to_engine_node(place.index)
59
+ elif place.index == 0:
60
+ index = ConstantNode(value=place.offset)
61
+ else:
62
+ index = FunctionNode(
63
+ func=Op.Add, args=[ir_to_engine_node(place.index), ConstantNode(value=place.offset)]
64
+ )
65
+ return FunctionNode(func=Op.Get, args=[ir_to_engine_node(place.block), index])
66
+ case IRSet(place=place, value=value):
67
+ return FunctionNode(func=Op.Set, args=[*ir_to_engine_node(place).args, ir_to_engine_node(value)])
68
+ case _:
69
+ raise TypeError(f"Unsupported IR statement: {stmt}")
@@ -0,0 +1,92 @@
1
+ import textwrap
2
+ from collections import deque
3
+ from collections.abc import Iterator
4
+
5
+ from sonolus.backend.ir import IRConst, IRExpr, IRStmt
6
+
7
+
8
+ class FlowEdge:
9
+ src: "BasicBlock"
10
+ dst: "BasicBlock"
11
+ cond: IRExpr | None
12
+
13
+ def __init__(self, src: "BasicBlock", dst: "BasicBlock", cond: IRExpr | None = None):
14
+ self.src = src
15
+ self.dst = dst
16
+ self.cond = cond
17
+
18
+
19
+ class BasicBlock:
20
+ statements: list[IRStmt]
21
+ test: IRExpr
22
+ incoming: set[FlowEdge]
23
+ outgoing: set[FlowEdge]
24
+
25
+ def __init__(
26
+ self,
27
+ statements: list[IRStmt] | None = None,
28
+ test: IRExpr | None = None,
29
+ incoming: set[FlowEdge] | None = None,
30
+ outgoing: set[FlowEdge] | None = None,
31
+ ):
32
+ self.statements = statements or []
33
+ self.test = test or IRConst(1)
34
+ self.incoming = incoming or set()
35
+ self.outgoing = outgoing or set()
36
+
37
+ def connect_to(self, other: "BasicBlock", cond: IRExpr | None = None):
38
+ edge = FlowEdge(self, other, cond)
39
+ self.outgoing.add(edge)
40
+ other.incoming.add(edge)
41
+
42
+
43
+ def traverse_cfg_preorder(block: BasicBlock) -> Iterator[BasicBlock]:
44
+ visited = set()
45
+ queue = deque([block])
46
+ while queue:
47
+ block = queue.popleft()
48
+ if block in visited:
49
+ continue
50
+ visited.add(block)
51
+ yield block
52
+ for edge in sorted(block.outgoing, key=lambda e: (e.cond is None, e.cond)):
53
+ queue.append(edge.dst)
54
+
55
+
56
+ def cfg_to_mermaid(entry: BasicBlock):
57
+ def pre(s: str):
58
+ return "\"<pre style='text-align: left;'>" + s.replace("\n", "<br/>") + '</pre>"'
59
+
60
+ def fmt(nodes):
61
+ if nodes:
62
+ return "\n".join(str(n) for n in nodes)
63
+ else:
64
+ return "{}"
65
+
66
+ block_indexes = {block: i for i, block in enumerate(traverse_cfg_preorder(entry))}
67
+
68
+ lines = ["Entry([Entry]) --> 0"]
69
+ for block in traverse_cfg_preorder(entry):
70
+ index = block_indexes[block]
71
+ lines.append(f"{index}[{pre(fmt([f'#{index}', *block.statements]))}]")
72
+
73
+ outgoing = {edge.cond: edge.dst for edge in block.outgoing}
74
+ match outgoing:
75
+ case {**other} if not other:
76
+ lines.append(f"{index} --> Exit")
77
+ case {None: target, **other} if not other:
78
+ lines.append(f"{index} --> {block_indexes[target]}")
79
+ case {0: f_branch, None: t_branch, **other} if not other:
80
+ lines.append(f"{index}_{{{pre(fmt([block.test]))}}}")
81
+ lines.append(f"{index} --> {index}_")
82
+ lines.append(f"{index}_ --> |true| {block_indexes[t_branch]}")
83
+ lines.append(f"{index}_ --> |false| {block_indexes[f_branch]}")
84
+ case dict() as tgt:
85
+ lines.append(f"{index}_{{{{{pre(fmt([block.test]))}}}}}")
86
+ lines.append(f"{index} --> {index}_")
87
+ for cond, target in tgt.items():
88
+ lines.append(f"{index}_ --> |{pre(fmt([cond or "default"]))}| {block_indexes[target]}")
89
+ lines.append("Exit([Exit])")
90
+
91
+ body = textwrap.indent("\n".join(lines), " ")
92
+ return f"graph\n{body}"
@@ -0,0 +1,333 @@
1
+ import math
2
+ import operator
3
+ import random
4
+
5
+ from sonolus.backend.node import ConstantNode, EngineNode
6
+ from sonolus.backend.ops import Op
7
+
8
+
9
+ class BreakException(Exception): # noqa: N818
10
+ n: int
11
+ value: float
12
+
13
+ def __init__(self, n: int, value: float):
14
+ self.n = n
15
+ self.value = value
16
+
17
+
18
+ class Interpreter:
19
+ blocks: dict[int, list[int]]
20
+ log: list[float]
21
+
22
+ def __init__(self):
23
+ self.blocks = {}
24
+ self.log = []
25
+
26
+ def run(self, node: EngineNode) -> float:
27
+ if isinstance(node, ConstantNode):
28
+ return node.value
29
+ func = node.func
30
+ args = node.args
31
+ match func:
32
+ case Op.Execute:
33
+ result = 0.0
34
+ for arg in args:
35
+ result = self.run(arg)
36
+ return result
37
+ case Op.Execute0:
38
+ for arg in args:
39
+ self.run(arg)
40
+ return 0.0
41
+ case Op.If:
42
+ test, t_branch, f_branch = args
43
+ if self.run(test) != 0.0:
44
+ return self.run(t_branch)
45
+ else:
46
+ return self.run(f_branch)
47
+ case Op.Switch:
48
+ test, *branches = args
49
+ test_result = self.run(test)
50
+ for i in range(0, len(branches), 2):
51
+ case, branch = branches[i], branches[i + 1]
52
+ if test_result == self.run(case):
53
+ return self.run(branch)
54
+ return 0.0
55
+ case Op.SwitchWithDefault:
56
+ test, *branches, default = args
57
+ test_result = self.run(test)
58
+ for i in range(0, len(branches), 2):
59
+ case, branch = branches[i], branches[i + 1]
60
+ if test_result == self.run(case):
61
+ return self.run(branch)
62
+ return self.run(default)
63
+ case Op.SwitchInteger:
64
+ test, *branches = args
65
+ test_result = int(self.run(test))
66
+ if 0 <= test_result < len(branches):
67
+ return self.run(branches[test_result])
68
+ else:
69
+ return 0.0
70
+ case Op.SwitchIntegerWithDefault:
71
+ test, *branches, default = args
72
+ test_result = int(self.run(test))
73
+ if 0 <= test_result < len(branches):
74
+ return self.run(branches[test_result])
75
+ else:
76
+ return self.run(default)
77
+ case Op.While:
78
+ test, body = args
79
+ while self.run(test) != 0.0:
80
+ self.run(body)
81
+ return 0.0
82
+ case Op.DoWhile:
83
+ body, test = args
84
+ while True:
85
+ self.run(body)
86
+ if self.run(test) == 0.0:
87
+ break
88
+ return 0.0
89
+ case Op.And:
90
+ result = 0.0
91
+ for arg in args:
92
+ result = self.run(arg)
93
+ if result == 0.0:
94
+ break
95
+ return result
96
+ case Op.Or:
97
+ result = 0.0
98
+ for arg in args:
99
+ result = self.run(arg)
100
+ if result != 0.0:
101
+ break
102
+ return result
103
+ case Op.JumpLoop:
104
+ index = 0
105
+ while 0 <= index < len(args):
106
+ if index == len(args) - 1:
107
+ return self.run(args[index])
108
+ index = int(self.run(args[index]))
109
+ return 0.0
110
+ case Op.Block:
111
+ try:
112
+ return self.run(args[0])
113
+ except BreakException as e:
114
+ if e.n > 1:
115
+ e.n -= 1
116
+ raise e from None
117
+ return e.value
118
+ case Op.Break:
119
+ raise BreakException(self.ensure_int(self.run(args[0])), self.run(args[1]))
120
+
121
+ case Op.Abs:
122
+ return abs(self.run(args[0]))
123
+ case Op.Add:
124
+ return self.reduce_args(args, operator.add)
125
+ case Op.Arccos:
126
+ return math.acos(self.run(args[0]))
127
+ case Op.Arcsin:
128
+ return math.asin(self.run(args[0]))
129
+ case Op.Arctan:
130
+ return math.atan(self.run(args[0]))
131
+ case Op.Arctan2:
132
+ return math.atan2(self.run(args[0]), self.run(args[1]))
133
+ case Op.Ceil:
134
+ return math.ceil(self.run(args[0]))
135
+ case Op.Clamp:
136
+ x, a, b = (self.run(arg) for arg in args)
137
+ return max(a, min(b, x))
138
+ case Op.Copy:
139
+ src_id, src_index, dst_id, dst_index, count = (self.ensure_int(self.run(arg)) for arg in args)
140
+ assert count >= 0, "Count must be non-negative"
141
+ values = [self.get(src_id, src_index + i) for i in range(count)]
142
+ for i, value in enumerate(values):
143
+ self.set(dst_id, dst_index + i, value)
144
+ return 0.0
145
+ case Op.Cos:
146
+ return math.cos(self.run(args[0]))
147
+ case Op.Cosh:
148
+ return math.cosh(self.run(args[0]))
149
+ case Op.DebugLog:
150
+ value = self.run(args[0])
151
+ self.log.append(value)
152
+ return 0.0
153
+ case Op.DebugPause:
154
+ return 0.0
155
+ case Op.Degree:
156
+ return math.degrees(self.run(args[0]))
157
+ case Op.Divide:
158
+ return self.reduce_args(args, operator.truediv)
159
+ case Op.Equal:
160
+ return 1.0 if self.run(args[0]) == self.run(args[1]) else 0.0
161
+ case Op.Floor:
162
+ return math.floor(self.run(args[0]))
163
+ case Op.Frac:
164
+ result = self.run(args[0]) % 1
165
+ return result if result >= 0 else result + 1
166
+ case Op.Get:
167
+ block, index = (self.ensure_int(self.run(arg)) for arg in args)
168
+ return self.get(block, index)
169
+ case Op.GetPointed:
170
+ block, index, offset = (self.ensure_int(self.run(arg)) for arg in args)
171
+ return self.get(self.get(block, index), self.get(block, index + 1) + offset)
172
+ case Op.GetShifted:
173
+ block, offset, index, stride = (self.ensure_int(self.run(arg)) for arg in args)
174
+ return self.get(block, offset + index * stride)
175
+ case Op.Greater:
176
+ return 1.0 if self.run(args[0]) > self.run(args[1]) else 0.0
177
+ case Op.GreaterOr:
178
+ return 1.0 if self.run(args[0]) >= self.run(args[1]) else 0.0
179
+ case Op.IncrementPost:
180
+ block, index = (self.ensure_int(self.run(arg)) for arg in args)
181
+ value = self.get(block, index)
182
+ self.set(block, index, value + 1)
183
+ return value
184
+ case Op.IncrementPostPointed:
185
+ block, index, offset = (self.ensure_int(self.run(arg)) for arg in args)
186
+ deref_block = self.get(block, index)
187
+ deref_index = self.get(block, index + 1) + offset
188
+ value = self.get(deref_block, deref_index)
189
+ self.set(deref_block, deref_index, value + 1)
190
+ return value
191
+ case Op.IncrementPostShifted:
192
+ block, offset, index, stride = (self.ensure_int(self.run(arg)) for arg in args)
193
+ value = self.get(block, offset + index * stride)
194
+ self.set(block, offset + index * stride, value + 1)
195
+ return value
196
+ case Op.IncrementPre:
197
+ block, index = (self.ensure_int(self.run(arg)) for arg in args)
198
+ value = self.get(block, index) + 1
199
+ self.set(block, index, value)
200
+ return value
201
+ case Op.IncrementPrePointed:
202
+ block, index, offset = (self.ensure_int(self.run(arg)) for arg in args)
203
+ deref_block = self.get(block, index)
204
+ deref_index = self.get(block, index + 1) + offset
205
+ value = self.get(deref_block, deref_index) + 1
206
+ self.set(deref_block, deref_index, value)
207
+ return value
208
+ case Op.IncrementPreShifted:
209
+ block, offset, index, stride = (self.ensure_int(self.run(arg)) for arg in args)
210
+ value = self.get(block, offset + index * stride) + 1
211
+ self.set(block, offset + index * stride, value)
212
+ return value
213
+ case Op.Lerp:
214
+ x, y, s = (self.run(arg) for arg in args)
215
+ return x + (y - x) * s
216
+ case Op.LerpClamped:
217
+ x, y, s = (self.run(arg) for arg in args)
218
+ return x + (y - x) * max(0, min(1, s))
219
+ case Op.Less:
220
+ return 1.0 if self.run(args[0]) < self.run(args[1]) else 0.0
221
+ case Op.LessOr:
222
+ return 1.0 if self.run(args[0]) <= self.run(args[1]) else 0.0
223
+ case Op.Log:
224
+ return math.log(self.run(args[0]))
225
+ case Op.Max:
226
+ return max(self.run(args[0]), self.run(args[1]))
227
+ case Op.Min:
228
+ return min(self.run(args[0]), self.run(args[1]))
229
+ case Op.Mod:
230
+ return self.reduce_args(args, operator.mod)
231
+ case Op.Multiply:
232
+ return self.reduce_args(args, operator.mul)
233
+ case Op.Negate:
234
+ return -self.run(args[0])
235
+ case Op.Not:
236
+ return 1.0 if self.run(args[0]) == 0.0 else 0.0
237
+ case Op.NotEqual:
238
+ return 1.0 if self.run(args[0]) != self.run(args[1]) else 0.0
239
+ case Op.Power:
240
+ return self.reduce_args(args, operator.pow)
241
+ case Op.Radian:
242
+ return math.radians(self.run(args[0]))
243
+ case Op.Random:
244
+ lo, hi = (self.run(arg) for arg in args)
245
+ return lo + (hi - lo) * random.random()
246
+ case Op.RandomInteger:
247
+ lo, hi = (self.ensure_int(self.run(arg)) for arg in args)
248
+ return random.randint(lo, hi)
249
+ case Op.Rem:
250
+ return self.reduce_args(args, math.remainder)
251
+ case Op.Remap:
252
+ from_min, from_max, to_min, to_max, value = (self.run(arg) for arg in args)
253
+ return to_min + (to_max - to_min) * (value - from_min) / (from_max - from_min)
254
+ case Op.RemapClamped:
255
+ from_min, from_max, to_min, to_max, value = (self.run(arg) for arg in args)
256
+ return to_min + (to_max - to_min) * max(0, min(1, (value - from_min) / (from_max - from_min)))
257
+ case Op.Round:
258
+ return round(self.run(args[0]))
259
+ case Op.Set:
260
+ block, index, value = (self.run(arg) for arg in args)
261
+ block, index = self.ensure_int(block), self.ensure_int(index)
262
+ return self.set(block, index, value)
263
+ case Op.SetPointed:
264
+ block, index, offset, value = (self.run(arg) for arg in args)
265
+ block, index, offset = self.ensure_int(block), self.ensure_int(index), self.ensure_int(offset)
266
+ return self.set(self.get(block, index), self.get(block, index + 1) + offset, value)
267
+ case Op.SetShifted:
268
+ block, offset, index, stride, value = (self.run(arg) for arg in args)
269
+ block, offset, index, stride = (
270
+ self.ensure_int(block),
271
+ self.ensure_int(offset),
272
+ self.ensure_int(index),
273
+ self.ensure_int(stride),
274
+ )
275
+
276
+ return self.set(block, offset + index * stride, value)
277
+ case Op.Sign:
278
+ return math.copysign(1, self.run(args[0]))
279
+ case Op.Sin:
280
+ return math.sin(self.run(args[0]))
281
+ case Op.Sinh:
282
+ return math.sinh(self.run(args[0]))
283
+ case Op.Subtract:
284
+ return self.reduce_args(args, operator.sub)
285
+ case Op.Tan:
286
+ return math.tan(self.run(args[0]))
287
+ case Op.Tanh:
288
+ return math.tanh(self.run(args[0]))
289
+ case Op.Trunc:
290
+ return math.trunc(self.run(args[0]))
291
+ case Op.Unlerp:
292
+ lo, hi, value = (self.run(arg) for arg in args)
293
+ return (value - lo) / (hi - lo)
294
+ case Op.UnlerpClamped:
295
+ lo, hi, value = (self.run(arg) for arg in args)
296
+ return max(0, min(1, (value - lo) / (hi - lo)))
297
+ case _:
298
+ raise NotImplementedError(f"Unsupported operation: {func}")
299
+
300
+ def reduce_args(self, args: list[EngineNode], operator) -> float:
301
+ if not args:
302
+ return 0.0
303
+ acc, *rest = (self.run(arg) for arg in args)
304
+ for arg in rest:
305
+ acc = operator(acc, arg)
306
+ return acc
307
+
308
+ def get(self, block: float, index: float) -> float:
309
+ block = self.ensure_int(block)
310
+ index = self.ensure_int(index)
311
+ assert index >= 0, "Index must be non-negative"
312
+ assert index <= 65535, "Index is too large"
313
+ if block not in self.blocks:
314
+ self.blocks[block] = []
315
+ if len(self.blocks[block]) <= index:
316
+ self.blocks[block] += [-1.0] * (index - len(self.blocks[block]) + 1)
317
+ return self.blocks[block][index]
318
+
319
+ def set(self, block: float, index: float, value: float):
320
+ block = self.ensure_int(block)
321
+ index = self.ensure_int(index)
322
+ assert index >= 0, "Index must be non-negative"
323
+ assert index <= 65535, "Index is too large"
324
+ if block not in self.blocks:
325
+ self.blocks[block] = []
326
+ if len(self.blocks[block]) <= index:
327
+ self.blocks[block] += [-1.0] * (index - len(self.blocks[block]) + 1)
328
+ self.blocks[block][index] = value
329
+ return value
330
+
331
+ def ensure_int(self, value: float) -> int:
332
+ assert value == int(value), "Value must be an integer"
333
+ return int(value)
sonolus/backend/ir.py ADDED
@@ -0,0 +1,89 @@
1
+ from sonolus.backend.ops import Op
2
+ from sonolus.backend.place import BlockPlace, Place, SSAPlace
3
+
4
+ type IRExpr = IRConst | IRPureInstr | IRGet
5
+ type IRStmt = IRExpr | IRInstr | IRSet
6
+
7
+
8
+ class IRConst:
9
+ value: float
10
+
11
+ def __init__(self, value: float):
12
+ self.value = value
13
+
14
+ def __repr__(self):
15
+ return f"IRConst({self.value!r})"
16
+
17
+ def __str__(self):
18
+ return f"{self.value}"
19
+
20
+ def __eq__(self, other):
21
+ return isinstance(other, IRConst) and self.value == other.value
22
+
23
+ def __hash__(self):
24
+ return hash(self.value)
25
+
26
+
27
+ class IRPureInstr:
28
+ op: Op
29
+ args: list[IRExpr]
30
+
31
+ def __init__(self, op: Op, args: list[IRExpr]):
32
+ assert op.pure
33
+ self.op = op
34
+ self.args = args
35
+
36
+ def __repr__(self):
37
+ return f"IRPureInstr({self.op!r}, {self.args!r})"
38
+
39
+ def __str__(self):
40
+ return f"{self.op.name}({', '.join(map(str, self.args))})"
41
+
42
+
43
+ class IRInstr:
44
+ op: Op
45
+ args: list[IRExpr]
46
+
47
+ def __init__(self, op: Op, args: list[IRExpr]):
48
+ self.op = op
49
+ self.args = args
50
+
51
+ def __repr__(self):
52
+ return f"IRInstr({self.op!r}, {self.args!r})"
53
+
54
+ def __str__(self):
55
+ return f"{self.op.name}({', '.join(map(str, self.args))})"
56
+
57
+
58
+ class IRGet:
59
+ place: Place
60
+
61
+ def __init__(self, place: Place):
62
+ self.place = place
63
+
64
+ def __repr__(self):
65
+ return f"IRGet({self.place!r})"
66
+
67
+ def __str__(self):
68
+ return f"{self.place}"
69
+
70
+
71
+ class IRSet:
72
+ place: Place
73
+ value: IRStmt
74
+
75
+ def __init__(self, place: Place, value: IRStmt):
76
+ self.place = place
77
+ self.value = value
78
+
79
+ def __repr__(self):
80
+ return f"IRSet({self.place!r}, {self.value!r})"
81
+
82
+ def __str__(self):
83
+ match self.place:
84
+ case BlockPlace():
85
+ return f"{self.place} <- {self.value}"
86
+ case SSAPlace():
87
+ return f"{self.place} := {self.value}"
88
+ case _:
89
+ raise TypeError(f"Invalid place: {self.place}")
@@ -0,0 +1,24 @@
1
+ from enum import Enum
2
+ from functools import cached_property
3
+
4
+ from sonolus.backend.blocks import Block, PlayBlock, PreviewBlock, TutorialBlock, WatchBlock
5
+
6
+
7
+ class Mode(Enum):
8
+ blocks: type[Block]
9
+
10
+ Play = (PlayBlock,)
11
+ Watch = (WatchBlock,)
12
+ Preview = (PreviewBlock,)
13
+ Tutorial = (TutorialBlock,)
14
+
15
+ def __init__(self, blocks: type[Block]):
16
+ self.blocks = blocks
17
+
18
+ @cached_property
19
+ def callbacks(self) -> frozenset[str]:
20
+ cbs = set()
21
+ for block in self.blocks:
22
+ cbs.update(block.readable)
23
+ cbs.update(block.writable)
24
+ return frozenset(cbs)
@@ -0,0 +1,40 @@
1
+ import textwrap
2
+ from dataclasses import dataclass
3
+
4
+ from sonolus.backend.ops import Op
5
+
6
+ type EngineNode = ConstantNode | FunctionNode
7
+
8
+
9
+ @dataclass
10
+ class ConstantNode:
11
+ value: float
12
+
13
+ def __hash__(self):
14
+ return hash(self.value)
15
+
16
+
17
+ @dataclass
18
+ class FunctionNode:
19
+ func: Op
20
+ args: list[EngineNode]
21
+
22
+ def __hash__(self):
23
+ return hash((self.func, tuple(self.args)))
24
+
25
+
26
+ def format_engine_node(node: EngineNode) -> str:
27
+ if isinstance(node, ConstantNode):
28
+ return str(node.value)
29
+ elif isinstance(node, FunctionNode):
30
+ match len(node.args):
31
+ case 0:
32
+ return f"{node.func.name}()"
33
+ case 1:
34
+ return f"{node.func.name}({format_engine_node(node.args[0])})"
35
+ case _:
36
+ return f"{node.func.name}(\n{
37
+ textwrap.indent("\n".join(format_engine_node(arg) for arg in node.args), " ")
38
+ }\n)"
39
+ else:
40
+ raise ValueError(f"Invalid engine node: {node}")