sonolus.py 0.3.0__py3-none-any.whl → 0.3.2__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.
- sonolus/backend/node.py +13 -5
- sonolus/backend/optimize/allocate.py +41 -4
- sonolus/backend/optimize/flow.py +24 -7
- sonolus/backend/optimize/optimize.py +2 -9
- sonolus/backend/utils.py +6 -1
- sonolus/backend/visitor.py +47 -14
- sonolus/build/cli.py +6 -1
- sonolus/build/engine.py +1 -1
- sonolus/script/archetype.py +27 -17
- sonolus/script/array.py +15 -5
- sonolus/script/array_like.py +5 -3
- sonolus/script/containers.py +3 -3
- sonolus/script/debug.py +66 -8
- sonolus/script/globals.py +17 -0
- sonolus/script/internal/builtin_impls.py +2 -3
- sonolus/script/internal/context.py +50 -0
- sonolus/script/internal/simulation_context.py +131 -0
- sonolus/script/internal/tuple_impl.py +15 -10
- sonolus/script/iterator.py +3 -2
- sonolus/script/num.py +13 -3
- sonolus/script/options.py +24 -1
- sonolus/script/quad.py +2 -0
- sonolus/script/record.py +22 -3
- sonolus/script/runtime.py +383 -0
- sonolus/script/stream.py +149 -17
- sonolus/script/transform.py +289 -0
- sonolus/script/values.py +9 -3
- {sonolus_py-0.3.0.dist-info → sonolus_py-0.3.2.dist-info}/METADATA +1 -1
- {sonolus_py-0.3.0.dist-info → sonolus_py-0.3.2.dist-info}/RECORD +32 -31
- {sonolus_py-0.3.0.dist-info → sonolus_py-0.3.2.dist-info}/WHEEL +0 -0
- {sonolus_py-0.3.0.dist-info → sonolus_py-0.3.2.dist-info}/entry_points.txt +0 -0
- {sonolus_py-0.3.0.dist-info → sonolus_py-0.3.2.dist-info}/licenses/LICENSE +0 -0
sonolus/backend/node.py
CHANGED
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
import textwrap
|
|
2
|
-
from dataclasses import dataclass
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
3
|
|
|
4
4
|
from sonolus.backend.ops import Op
|
|
5
5
|
|
|
6
6
|
type EngineNode = ConstantNode | FunctionNode
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
@dataclass
|
|
9
|
+
@dataclass(slots=True)
|
|
10
10
|
class ConstantNode:
|
|
11
11
|
value: float
|
|
12
|
+
_hash: int = field(init=False, repr=False)
|
|
13
|
+
|
|
14
|
+
def __post_init__(self):
|
|
15
|
+
self._hash = hash(self.value)
|
|
12
16
|
|
|
13
17
|
def __hash__(self):
|
|
14
18
|
return hash(self.value)
|
|
15
19
|
|
|
16
20
|
|
|
17
|
-
@dataclass
|
|
21
|
+
@dataclass(slots=True)
|
|
18
22
|
class FunctionNode:
|
|
19
23
|
func: Op
|
|
20
24
|
args: list[EngineNode]
|
|
25
|
+
_hash: int = field(init=False, repr=False)
|
|
26
|
+
|
|
27
|
+
def __post_init__(self):
|
|
28
|
+
self._hash = hash((self.func, tuple(self.args)))
|
|
21
29
|
|
|
22
30
|
def __hash__(self):
|
|
23
|
-
return
|
|
31
|
+
return self._hash
|
|
24
32
|
|
|
25
33
|
|
|
26
34
|
def format_engine_node(node: EngineNode) -> str:
|
|
@@ -34,7 +42,7 @@ def format_engine_node(node: EngineNode) -> str:
|
|
|
34
42
|
return f"{node.func.name}({format_engine_node(node.args[0])})"
|
|
35
43
|
case _:
|
|
36
44
|
return f"{node.func.name}(\n{
|
|
37
|
-
textwrap.indent(
|
|
45
|
+
textwrap.indent('\n'.join(format_engine_node(arg) for arg in node.args), ' ')
|
|
38
46
|
}\n)"
|
|
39
47
|
else:
|
|
40
48
|
raise ValueError(f"Invalid engine node: {node}")
|
|
@@ -65,7 +65,8 @@ class Allocate(CompilerPass):
|
|
|
65
65
|
def run(self, entry: BasicBlock):
|
|
66
66
|
mapping = self.get_mapping(entry)
|
|
67
67
|
for block in traverse_cfg_preorder(entry):
|
|
68
|
-
|
|
68
|
+
updated_statements = [self.update_stmt(statement, mapping) for statement in block.statements]
|
|
69
|
+
block.statements = [stmt for stmt in updated_statements if stmt is not None]
|
|
69
70
|
block.test = self.update_stmt(block.test, mapping)
|
|
70
71
|
return entry
|
|
71
72
|
|
|
@@ -80,7 +81,19 @@ class Allocate(CompilerPass):
|
|
|
80
81
|
case IRGet(place=place):
|
|
81
82
|
return IRGet(place=self.update_stmt(place, mapping))
|
|
82
83
|
case IRSet(place=place, value=value):
|
|
83
|
-
|
|
84
|
+
# Do some dead code elimination here which is pretty much free since we already have liveness analysis,
|
|
85
|
+
# and prevents an error from the dead block place being missing from the mapping.
|
|
86
|
+
live = get_live(stmt)
|
|
87
|
+
is_live = not (
|
|
88
|
+
(isinstance(place, BlockPlace) and isinstance(place.block, TempBlock) and place.block not in live)
|
|
89
|
+
or (isinstance(value, IRGet) and place == value.place)
|
|
90
|
+
)
|
|
91
|
+
if is_live:
|
|
92
|
+
return IRSet(place=self.update_stmt(place, mapping), value=self.update_stmt(value, mapping))
|
|
93
|
+
elif isinstance(value, IRInstr) and value.op.side_effects:
|
|
94
|
+
return self.update_stmt(value, mapping)
|
|
95
|
+
else:
|
|
96
|
+
return None
|
|
84
97
|
case BlockPlace(block=block, index=index, offset=offset):
|
|
85
98
|
if isinstance(block, TempBlock):
|
|
86
99
|
if block.size == 0:
|
|
@@ -119,8 +132,32 @@ class Allocate(CompilerPass):
|
|
|
119
132
|
def get_interference(self, entry: BasicBlock) -> dict[TempBlock, set[TempBlock]]:
|
|
120
133
|
result = {}
|
|
121
134
|
for block in traverse_cfg_preorder(entry):
|
|
122
|
-
for stmt in
|
|
135
|
+
for stmt in block.statements:
|
|
136
|
+
if not isinstance(stmt, IRSet):
|
|
137
|
+
continue
|
|
123
138
|
live = {p for p in get_live(stmt) if isinstance(p, TempBlock) and p.size > 0}
|
|
124
139
|
for place in live:
|
|
125
|
-
|
|
140
|
+
if place not in result:
|
|
141
|
+
result[place] = set(live)
|
|
142
|
+
else:
|
|
143
|
+
result[place].update(live)
|
|
126
144
|
return result
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class AllocateFast(Allocate):
|
|
148
|
+
"""A bit faster than Allocate but a bit less optimal."""
|
|
149
|
+
|
|
150
|
+
def get_mapping(self, entry: BasicBlock) -> dict[TempBlock, int]:
|
|
151
|
+
interference = self.get_interference(entry)
|
|
152
|
+
offsets: dict[TempBlock, int] = dict.fromkeys(interference, 0)
|
|
153
|
+
end_offsets: dict[TempBlock, int] = dict.fromkeys(interference, 0)
|
|
154
|
+
|
|
155
|
+
for block, others in interference.items():
|
|
156
|
+
size = block.size
|
|
157
|
+
offset = max((end_offsets[other] for other in others), default=0)
|
|
158
|
+
if offset + size > TEMP_SIZE:
|
|
159
|
+
raise ValueError("Temporary memory limit exceeded")
|
|
160
|
+
offsets[block] = offset
|
|
161
|
+
end_offsets[block] = offset + size
|
|
162
|
+
|
|
163
|
+
return offsets
|
sonolus/backend/optimize/flow.py
CHANGED
|
@@ -91,12 +91,29 @@ def cfg_to_mermaid(entry: BasicBlock):
|
|
|
91
91
|
|
|
92
92
|
lines = ["Entry([Entry]) --> 0"]
|
|
93
93
|
for block, index in block_indexes.items():
|
|
94
|
-
lines.append(
|
|
95
|
-
f"{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
lines.append(
|
|
95
|
+
f"{index}[{
|
|
96
|
+
pre(
|
|
97
|
+
fmt(
|
|
98
|
+
[
|
|
99
|
+
f'#{index}',
|
|
100
|
+
*(
|
|
101
|
+
f'{dst} := phi({
|
|
102
|
+
", ".join(
|
|
103
|
+
f"{block_indexes.get(src_block, '<dead>')}: {src_place}"
|
|
104
|
+
for src_block, src_place in sorted(
|
|
105
|
+
phis.items(), key=lambda x: block_indexes.get(x[0])
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
})'
|
|
109
|
+
for dst, phis in block.phis.items()
|
|
110
|
+
),
|
|
111
|
+
*block.statements,
|
|
112
|
+
]
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
}]"
|
|
116
|
+
)
|
|
100
117
|
|
|
101
118
|
outgoing = {edge.cond: edge.dst for edge in block.outgoing}
|
|
102
119
|
match outgoing:
|
|
@@ -114,7 +131,7 @@ def cfg_to_mermaid(entry: BasicBlock):
|
|
|
114
131
|
lines.append(f"{index} --> {index}_")
|
|
115
132
|
for cond, target in tgt.items():
|
|
116
133
|
lines.append(
|
|
117
|
-
f"{index}_ --> |{pre(fmt([cond if cond is not None else
|
|
134
|
+
f"{index}_ --> |{pre(fmt([cond if cond is not None else 'default']))}| {block_indexes[target]}"
|
|
118
135
|
)
|
|
119
136
|
lines.append("Exit([Exit])")
|
|
120
137
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from sonolus.backend.optimize.allocate import Allocate, AllocateBasic
|
|
1
|
+
from sonolus.backend.optimize.allocate import Allocate, AllocateBasic, AllocateFast
|
|
2
2
|
from sonolus.backend.optimize.constant_evaluation import SparseConditionalConstantPropagation
|
|
3
3
|
from sonolus.backend.optimize.copy_coalesce import CopyCoalesce
|
|
4
4
|
from sonolus.backend.optimize.dead_code import (
|
|
@@ -6,9 +6,7 @@ from sonolus.backend.optimize.dead_code import (
|
|
|
6
6
|
DeadCodeElimination,
|
|
7
7
|
UnreachableCodeElimination,
|
|
8
8
|
)
|
|
9
|
-
from sonolus.backend.optimize.flow import BasicBlock
|
|
10
9
|
from sonolus.backend.optimize.inlining import InlineVars
|
|
11
|
-
from sonolus.backend.optimize.passes import run_passes
|
|
12
10
|
from sonolus.backend.optimize.simplify import CoalesceFlow, NormalizeSwitch, RewriteToSwitch
|
|
13
11
|
from sonolus.backend.optimize.ssa import FromSSA, ToSSA
|
|
14
12
|
|
|
@@ -21,9 +19,8 @@ MINIMAL_PASSES = (
|
|
|
21
19
|
FAST_PASSES = (
|
|
22
20
|
CoalesceFlow(),
|
|
23
21
|
UnreachableCodeElimination(),
|
|
24
|
-
|
|
22
|
+
AllocateFast(), # Does dead code elimination too, so no need for a separate pass
|
|
25
23
|
CoalesceFlow(),
|
|
26
|
-
Allocate(),
|
|
27
24
|
)
|
|
28
25
|
|
|
29
26
|
STANDARD_PASSES = (
|
|
@@ -46,7 +43,3 @@ STANDARD_PASSES = (
|
|
|
46
43
|
NormalizeSwitch(),
|
|
47
44
|
Allocate(),
|
|
48
45
|
)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def optimize_and_allocate(cfg: BasicBlock):
|
|
52
|
-
return run_passes(cfg, STANDARD_PASSES)
|
sonolus/backend/utils.py
CHANGED
|
@@ -11,10 +11,15 @@ def get_function(fn: Callable) -> tuple[str, ast.FunctionDef]:
|
|
|
11
11
|
# This preserves both line number and column number in the returned node
|
|
12
12
|
source_file = inspect.getsourcefile(fn)
|
|
13
13
|
_, start_line = inspect.getsourcelines(fn)
|
|
14
|
-
base_tree =
|
|
14
|
+
base_tree = get_tree_from_file(source_file)
|
|
15
15
|
return source_file, find_function(base_tree, start_line)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
@cache
|
|
19
|
+
def get_tree_from_file(file: str | Path) -> ast.Module:
|
|
20
|
+
return ast.parse(Path(file).read_text(encoding="utf-8"))
|
|
21
|
+
|
|
22
|
+
|
|
18
23
|
class FindFunction(ast.NodeVisitor):
|
|
19
24
|
def __init__(self, line):
|
|
20
25
|
self.line = line
|
sonolus/backend/visitor.py
CHANGED
|
@@ -4,6 +4,7 @@ import builtins
|
|
|
4
4
|
import functools
|
|
5
5
|
import inspect
|
|
6
6
|
from collections.abc import Callable, Sequence
|
|
7
|
+
from inspect import ismethod
|
|
7
8
|
from types import FunctionType, MethodType, MethodWrapperType
|
|
8
9
|
from typing import Any, Never, Self
|
|
9
10
|
|
|
@@ -54,10 +55,22 @@ def eval_fn(fn: Callable, /, *args, **kwargs):
|
|
|
54
55
|
source_file, node = get_function(fn)
|
|
55
56
|
bound_args = inspect.signature(fn).bind(*args, **kwargs)
|
|
56
57
|
bound_args.apply_defaults()
|
|
58
|
+
if ismethod(fn):
|
|
59
|
+
code = fn.__func__.__code__
|
|
60
|
+
closure = fn.__func__.__closure__
|
|
61
|
+
else:
|
|
62
|
+
code = fn.__code__
|
|
63
|
+
closure = fn.__closure__
|
|
64
|
+
if closure is None:
|
|
65
|
+
nonlocal_vars = {}
|
|
66
|
+
else:
|
|
67
|
+
nonlocal_vars = {
|
|
68
|
+
var: cell.cell_contents for var, cell in zip(code.co_freevars, closure, strict=True) if cell is not None
|
|
69
|
+
}
|
|
57
70
|
global_vars = {
|
|
58
71
|
**builtins.__dict__,
|
|
59
72
|
**fn.__globals__,
|
|
60
|
-
**
|
|
73
|
+
**nonlocal_vars,
|
|
61
74
|
}
|
|
62
75
|
return Visitor(source_file, bound_args, global_vars).run(node)
|
|
63
76
|
|
|
@@ -728,6 +741,10 @@ class Visitor(ast.NodeVisitor):
|
|
|
728
741
|
if isinstance(node.op, ast.Not):
|
|
729
742
|
return self.ensure_boolean_num(operand).not_()
|
|
730
743
|
op = unary_ops[type(node.op)]
|
|
744
|
+
if operand._is_py_():
|
|
745
|
+
operand_py = operand._as_py_()
|
|
746
|
+
if isinstance(operand_py, type) and hasattr(type(operand_py), op):
|
|
747
|
+
return self.handle_call(node, getattr(type(operand_py), op), operand_py)
|
|
731
748
|
if hasattr(operand, op):
|
|
732
749
|
return self.handle_call(node, getattr(operand, op))
|
|
733
750
|
raise TypeError(f"bad operand type for unary {op_to_symbol[type(node.op)]}: '{type(operand).__name__}'")
|
|
@@ -831,10 +848,11 @@ class Visitor(ast.NodeVisitor):
|
|
|
831
848
|
):
|
|
832
849
|
result = self.handle_call(node, getattr(r_val, rcomp_ops[type(op)]), l_val)
|
|
833
850
|
if result is None or self.is_not_implemented(result):
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
851
|
+
# Can't defer to the default object.__eq__ or similar since reference equality (is) is not reliable
|
|
852
|
+
if type(op) is ast.Eq and type(l_val) is not type(r_val):
|
|
853
|
+
return Num._accept_(False)
|
|
854
|
+
elif type(op) is ast.NotEq and type(l_val) is not type(r_val):
|
|
855
|
+
return Num._accept_(True)
|
|
838
856
|
else:
|
|
839
857
|
raise TypeError(
|
|
840
858
|
f"'{op_to_symbol[type(op)]}' not supported between instances of '{type(l_val).__name__}' and "
|
|
@@ -1177,18 +1195,33 @@ class Visitor(ast.NodeVisitor):
|
|
|
1177
1195
|
self, node: ast.stmt | ast.expr, fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs
|
|
1178
1196
|
) -> R:
|
|
1179
1197
|
"""Executes the given function at the given node for a better traceback."""
|
|
1198
|
+
location_args = {
|
|
1199
|
+
"lineno": node.lineno,
|
|
1200
|
+
"col_offset": node.col_offset,
|
|
1201
|
+
"end_lineno": node.end_lineno,
|
|
1202
|
+
"end_col_offset": node.end_col_offset,
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1180
1205
|
expr = ast.Expression(
|
|
1181
1206
|
body=ast.Call(
|
|
1182
|
-
func=ast.Name(id="fn", ctx=ast.Load()),
|
|
1183
|
-
args=[
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1207
|
+
func=ast.Name(id="fn", ctx=ast.Load(), **location_args),
|
|
1208
|
+
args=[
|
|
1209
|
+
ast.Starred(
|
|
1210
|
+
value=ast.Name(id="args", ctx=ast.Load(), **location_args),
|
|
1211
|
+
ctx=ast.Load(),
|
|
1212
|
+
**location_args,
|
|
1213
|
+
)
|
|
1214
|
+
],
|
|
1215
|
+
keywords=[
|
|
1216
|
+
ast.keyword(
|
|
1217
|
+
value=ast.Name(id="kwargs", ctx=ast.Load(), **location_args),
|
|
1218
|
+
arg=None,
|
|
1219
|
+
**location_args,
|
|
1220
|
+
)
|
|
1221
|
+
],
|
|
1222
|
+
**location_args,
|
|
1223
|
+
)
|
|
1190
1224
|
)
|
|
1191
|
-
expr = ast.fix_missing_locations(expr)
|
|
1192
1225
|
return eval(
|
|
1193
1226
|
compile(expr, filename=self.source_file, mode="eval"),
|
|
1194
1227
|
{"fn": fn, "args": args, "kwargs": kwargs, "_filter_traceback_": True},
|
sonolus/build/cli.py
CHANGED
|
@@ -11,7 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
from time import perf_counter
|
|
12
12
|
|
|
13
13
|
from sonolus.backend.optimize.optimize import FAST_PASSES, MINIMAL_PASSES, STANDARD_PASSES
|
|
14
|
-
from sonolus.build.engine import package_engine
|
|
14
|
+
from sonolus.build.engine import no_gil, package_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
17
|
from sonolus.script.project import BuildConfig, Project
|
|
@@ -215,6 +215,11 @@ def main():
|
|
|
215
215
|
else:
|
|
216
216
|
parser.error("Module argument is required when multiple or no modules are found")
|
|
217
217
|
|
|
218
|
+
if no_gil():
|
|
219
|
+
print("Multithreading is enabled")
|
|
220
|
+
if hasattr(sys, "_jit") and sys._jit.is_enabled():
|
|
221
|
+
print("Python JIT is enabled")
|
|
222
|
+
|
|
218
223
|
project = import_project(args.module)
|
|
219
224
|
if project is None:
|
|
220
225
|
sys.exit(1)
|
sonolus/build/engine.py
CHANGED
|
@@ -32,7 +32,7 @@ from sonolus.script.project import BuildConfig
|
|
|
32
32
|
from sonolus.script.sprite import Skin
|
|
33
33
|
from sonolus.script.ui import UiConfig
|
|
34
34
|
|
|
35
|
-
type JsonValue =
|
|
35
|
+
type JsonValue = bool | int | float | str | list[JsonValue] | dict[str, JsonValue] | None
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
@dataclass
|
sonolus/script/archetype.py
CHANGED
|
@@ -185,14 +185,14 @@ class _ArchetypeField(SonolusDescriptor):
|
|
|
185
185
|
def imported(*, name: str | None = None) -> Any:
|
|
186
186
|
"""Declare a field as imported.
|
|
187
187
|
|
|
188
|
-
Imported fields may be loaded from the level
|
|
188
|
+
Imported fields may be loaded from the level.
|
|
189
189
|
|
|
190
190
|
In watch mode, data may also be loaded from a corresponding exported field in play mode.
|
|
191
191
|
|
|
192
192
|
Imported fields may only be updated in the `preprocess` callback, and are read-only in other callbacks.
|
|
193
193
|
|
|
194
194
|
Usage:
|
|
195
|
-
```
|
|
195
|
+
```python
|
|
196
196
|
class MyArchetype(PlayArchetype):
|
|
197
197
|
field: int = imported()
|
|
198
198
|
field_with_explicit_name: int = imported(name="field_name")
|
|
@@ -207,10 +207,10 @@ def entity_data() -> Any:
|
|
|
207
207
|
Entity data is accessible from other entities, but may only be updated in the `preprocess` callback
|
|
208
208
|
and is read-only in other callbacks.
|
|
209
209
|
|
|
210
|
-
It functions like `imported
|
|
210
|
+
It functions like `imported` and shares the same underlying storage, except that it is not loaded from a level.
|
|
211
211
|
|
|
212
212
|
Usage:
|
|
213
|
-
```
|
|
213
|
+
```python
|
|
214
214
|
class MyArchetype(PlayArchetype):
|
|
215
215
|
field: int = entity_data()
|
|
216
216
|
```
|
|
@@ -226,7 +226,7 @@ def exported(*, name: str | None = None) -> Any:
|
|
|
226
226
|
Exported fields are write-only.
|
|
227
227
|
|
|
228
228
|
Usage:
|
|
229
|
-
```
|
|
229
|
+
```python
|
|
230
230
|
class MyArchetype(PlayArchetype):
|
|
231
231
|
field: int = exported()
|
|
232
232
|
field_with_explicit_name: int = exported(name="#FIELD")
|
|
@@ -238,12 +238,13 @@ def exported(*, name: str | None = None) -> Any:
|
|
|
238
238
|
def entity_memory() -> Any:
|
|
239
239
|
"""Declare a field as entity memory.
|
|
240
240
|
|
|
241
|
-
Entity memory is private to the entity and is not accessible from other entities.
|
|
241
|
+
Entity memory is private to the entity and is not accessible from other entities. It may be read or updated in any
|
|
242
|
+
callback associated with the entity.
|
|
242
243
|
|
|
243
244
|
Entity memory fields may also be set when an entity is spawned using the `spawn()` method.
|
|
244
245
|
|
|
245
246
|
Usage:
|
|
246
|
-
```
|
|
247
|
+
```python
|
|
247
248
|
class MyArchetype(PlayArchetype):
|
|
248
249
|
field: int = entity_memory()
|
|
249
250
|
|
|
@@ -257,10 +258,11 @@ def shared_memory() -> Any:
|
|
|
257
258
|
|
|
258
259
|
Shared memory is accessible from other entities.
|
|
259
260
|
|
|
260
|
-
Shared memory may only be updated by sequential callbacks
|
|
261
|
+
Shared memory may be read in any callback, but may only be updated by sequential callbacks
|
|
262
|
+
(`preprocess`, `update_sequential`, and `touch`).
|
|
261
263
|
|
|
262
264
|
Usage:
|
|
263
|
-
```
|
|
265
|
+
```python
|
|
264
266
|
class MyArchetype(PlayArchetype):
|
|
265
267
|
field: int = shared_memory()
|
|
266
268
|
```
|
|
@@ -280,7 +282,7 @@ class StandardImport:
|
|
|
280
282
|
"""Standard import annotations for Archetype fields.
|
|
281
283
|
|
|
282
284
|
Usage:
|
|
283
|
-
```
|
|
285
|
+
```python
|
|
284
286
|
class MyArchetype(WatchArchetype):
|
|
285
287
|
judgment: StandardImport.JUDGMENT
|
|
286
288
|
```
|
|
@@ -313,7 +315,7 @@ def callback[T: Callable](*, order: int = 0) -> Callable[[T], T]:
|
|
|
313
315
|
Callbacks are execute from lowest to highest order. By default, callbacks have an order of 0.
|
|
314
316
|
|
|
315
317
|
Usage:
|
|
316
|
-
```
|
|
318
|
+
```python
|
|
317
319
|
class MyArchetype(PlayArchetype):
|
|
318
320
|
@callback(order=1)
|
|
319
321
|
def update_sequential(self):
|
|
@@ -360,6 +362,8 @@ class ArchetypeSchema(TypedDict):
|
|
|
360
362
|
class _BaseArchetype:
|
|
361
363
|
_is_comptime_value_ = True
|
|
362
364
|
|
|
365
|
+
_removable_prefix: ClassVar[str] = ""
|
|
366
|
+
|
|
363
367
|
_supported_callbacks_: ClassVar[dict[str, CallbackInfo]]
|
|
364
368
|
_default_callbacks_: ClassVar[set[Callable]]
|
|
365
369
|
|
|
@@ -440,7 +444,7 @@ class _BaseArchetype:
|
|
|
440
444
|
"""Spawn an entity of this archetype, injecting the given values into entity memory.
|
|
441
445
|
|
|
442
446
|
Usage:
|
|
443
|
-
```
|
|
447
|
+
```python
|
|
444
448
|
class MyArchetype(PlayArchetype):
|
|
445
449
|
field: int = entity_memory()
|
|
446
450
|
|
|
@@ -492,7 +496,7 @@ class _BaseArchetype:
|
|
|
492
496
|
cls._default_callbacks_ = {getattr(cls, cb_info.py_name) for cb_info in cls._supported_callbacks_.values()}
|
|
493
497
|
return
|
|
494
498
|
if cls.name is None or cls.name in {getattr(mro_entry, "name", None) for mro_entry in cls.mro()[1:]}:
|
|
495
|
-
cls.name = cls.__name__
|
|
499
|
+
cls.name = cls.__name__.removeprefix(cls._removable_prefix)
|
|
496
500
|
cls._callbacks_ = []
|
|
497
501
|
for name in cls._supported_callbacks_:
|
|
498
502
|
cb = getattr(cls, name)
|
|
@@ -642,7 +646,7 @@ class PlayArchetype(_BaseArchetype):
|
|
|
642
646
|
"""Base class for play mode archetypes.
|
|
643
647
|
|
|
644
648
|
Usage:
|
|
645
|
-
```
|
|
649
|
+
```python
|
|
646
650
|
class MyArchetype(PlayArchetype):
|
|
647
651
|
# Set to True if the entity is a note and contributes to combo and score
|
|
648
652
|
# Default is False
|
|
@@ -659,6 +663,8 @@ class PlayArchetype(_BaseArchetype):
|
|
|
659
663
|
```
|
|
660
664
|
"""
|
|
661
665
|
|
|
666
|
+
_removable_prefix: ClassVar[str] = "Play"
|
|
667
|
+
|
|
662
668
|
_supported_callbacks_ = PLAY_CALLBACKS
|
|
663
669
|
|
|
664
670
|
is_scored: ClassVar[bool] = False
|
|
@@ -808,7 +814,7 @@ class WatchArchetype(_BaseArchetype):
|
|
|
808
814
|
"""Base class for watch mode archetypes.
|
|
809
815
|
|
|
810
816
|
Usage:
|
|
811
|
-
```
|
|
817
|
+
```python
|
|
812
818
|
class MyArchetype(WatchArchetype):
|
|
813
819
|
imported_field: int = imported()
|
|
814
820
|
entity_memory_field: int = entity_memory()
|
|
@@ -820,6 +826,8 @@ class WatchArchetype(_BaseArchetype):
|
|
|
820
826
|
```
|
|
821
827
|
"""
|
|
822
828
|
|
|
829
|
+
_removable_prefix: ClassVar[str] = "Watch"
|
|
830
|
+
|
|
823
831
|
_supported_callbacks_ = WATCH_ARCHETYPE_CALLBACKS
|
|
824
832
|
|
|
825
833
|
def preprocess(self):
|
|
@@ -923,7 +931,7 @@ class PreviewArchetype(_BaseArchetype):
|
|
|
923
931
|
"""Base class for preview mode archetypes.
|
|
924
932
|
|
|
925
933
|
Usage:
|
|
926
|
-
```
|
|
934
|
+
```python
|
|
927
935
|
class MyArchetype(PreviewArchetype):
|
|
928
936
|
imported_field: int = imported()
|
|
929
937
|
entity_memory_field: int = entity_memory()
|
|
@@ -935,6 +943,8 @@ class PreviewArchetype(_BaseArchetype):
|
|
|
935
943
|
```
|
|
936
944
|
"""
|
|
937
945
|
|
|
946
|
+
_removable_prefix: ClassVar[str] = "Preview"
|
|
947
|
+
|
|
938
948
|
_supported_callbacks_ = PREVIEW_CALLBACKS
|
|
939
949
|
|
|
940
950
|
def preprocess(self):
|
|
@@ -1078,7 +1088,7 @@ class EntityRef[A: _BaseArchetype](Record):
|
|
|
1078
1088
|
May be used with `Any` to reference an unknown archetype.
|
|
1079
1089
|
|
|
1080
1090
|
Usage:
|
|
1081
|
-
```
|
|
1091
|
+
```python
|
|
1082
1092
|
class MyArchetype(PlayArchetype):
|
|
1083
1093
|
ref_1: EntityRef[OtherArchetype] = imported()
|
|
1084
1094
|
ref_2: EntityRef[Any] = imported()
|
sonolus/script/array.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# ruff: noqa: A005
|
|
2
1
|
from __future__ import annotations
|
|
3
2
|
|
|
4
3
|
from collections.abc import Iterable
|
|
@@ -16,14 +15,21 @@ from sonolus.script.internal.value import BackingSource, DataValue, Value
|
|
|
16
15
|
from sonolus.script.num import Num
|
|
17
16
|
|
|
18
17
|
|
|
18
|
+
class ArrayMeta(type):
|
|
19
|
+
@meta_fn
|
|
20
|
+
def __pos__[T](cls: type[T]) -> T:
|
|
21
|
+
return cls._zero_()
|
|
22
|
+
|
|
23
|
+
|
|
19
24
|
@final
|
|
20
|
-
class Array[T, Size](GenericValue, ArrayLike[T]):
|
|
25
|
+
class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
|
|
21
26
|
"""A fixed size array of values.
|
|
22
27
|
|
|
23
28
|
Usage:
|
|
24
29
|
```python
|
|
25
30
|
array_1 = Array(1, 2, 3)
|
|
26
31
|
array_2 = Array[int, 0]()
|
|
32
|
+
array_3 = +Array[int, 3] # Create a zero-initialized array
|
|
27
33
|
```
|
|
28
34
|
"""
|
|
29
35
|
|
|
@@ -66,7 +72,7 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
|
|
|
66
72
|
else:
|
|
67
73
|
return parameterized_cls._with_value([value._copy_() for value in values])
|
|
68
74
|
|
|
69
|
-
def __init__(self, *
|
|
75
|
+
def __init__(self, *args: T):
|
|
70
76
|
super().__init__()
|
|
71
77
|
|
|
72
78
|
@classmethod
|
|
@@ -289,10 +295,14 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
|
|
|
289
295
|
if isinstance(self._value, BlockPlace) or callable(self._value):
|
|
290
296
|
return f"{type(self).__name__}({self._value}...)"
|
|
291
297
|
else:
|
|
292
|
-
return f"{type(self).__name__}({
|
|
298
|
+
return f"{type(self).__name__}({', '.join(str(self[i]) for i in range(self.size()))})"
|
|
293
299
|
|
|
294
300
|
def __repr__(self):
|
|
295
301
|
if isinstance(self._value, BlockPlace) or callable(self._value):
|
|
296
302
|
return f"{type(self).__name__}({self._value}...)"
|
|
297
303
|
else:
|
|
298
|
-
return f"{type(self).__name__}({
|
|
304
|
+
return f"{type(self).__name__}({', '.join(repr(self[i]) for i in range(self.size()))})"
|
|
305
|
+
|
|
306
|
+
def __pos__(self) -> Self:
|
|
307
|
+
"""Return a copy of the array."""
|
|
308
|
+
return self._copy_()
|
sonolus/script/array_like.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import random
|
|
4
|
-
from abc import
|
|
5
|
-
from collections.abc import Callable
|
|
4
|
+
from abc import abstractmethod
|
|
5
|
+
from collections.abc import Callable
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
from sonolus.script.iterator import SonolusIterator
|
|
@@ -13,7 +13,7 @@ from sonolus.script.values import copy
|
|
|
13
13
|
# Note: we don't use Range in this file because Range itself inherits from ArrayLike
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
class ArrayLike[T]
|
|
16
|
+
class ArrayLike[T]:
|
|
17
17
|
"""Mixin for array-like objects.
|
|
18
18
|
|
|
19
19
|
Inheritors must implement `__len__`, `__getitem__`, and `__setitem__`.
|
|
@@ -32,6 +32,8 @@ class ArrayLike[T](Sequence, ABC):
|
|
|
32
32
|
```
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
+
_allow_instance_check_ = True
|
|
36
|
+
|
|
35
37
|
@abstractmethod
|
|
36
38
|
def __len__(self) -> int:
|
|
37
39
|
"""Return the length of the array."""
|
sonolus/script/containers.py
CHANGED
|
@@ -10,7 +10,7 @@ from sonolus.script.iterator import SonolusIterator
|
|
|
10
10
|
from sonolus.script.num import Num
|
|
11
11
|
from sonolus.script.pointer import _deref
|
|
12
12
|
from sonolus.script.record import Record
|
|
13
|
-
from sonolus.script.values import
|
|
13
|
+
from sonolus.script.values import copy, zeros
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class Box[T](Record):
|
|
@@ -104,7 +104,7 @@ class VarArray[T, Capacity](Record, ArrayLike[T]):
|
|
|
104
104
|
"""Create a new empty array."""
|
|
105
105
|
element_type = cls.type_var_value(T)
|
|
106
106
|
capacity = cls.type_var_value(Capacity)
|
|
107
|
-
return cls(0,
|
|
107
|
+
return cls(0, zeros(Array[element_type, capacity]))
|
|
108
108
|
|
|
109
109
|
def __len__(self) -> int:
|
|
110
110
|
"""Return the number of elements in the array."""
|
|
@@ -457,7 +457,7 @@ class ArrayMap[K, V, Capacity](Record):
|
|
|
457
457
|
key_type = cls.type_var_value(K)
|
|
458
458
|
value_type = cls.type_var_value(V)
|
|
459
459
|
capacity = cls.type_var_value(Capacity)
|
|
460
|
-
return cls(0,
|
|
460
|
+
return cls(0, zeros(Array[_ArrayMapEntry[key_type, value_type], capacity]))
|
|
461
461
|
|
|
462
462
|
def __len__(self) -> int:
|
|
463
463
|
"""Return the number of key-value pairs in the map."""
|