sonolus.py 0.3.1__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 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 hash((self.func, tuple(self.args)))
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("\n".join(format_engine_node(arg) for arg in node.args), " ")
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
- block.statements = [self.update_stmt(statement, mapping) for statement in block.statements]
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
- return IRSet(place=self.update_stmt(place, mapping), value=self.update_stmt(value, mapping))
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 [*block.statements, block.test]:
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
- result.setdefault(place, set()).update(live - {place})
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
@@ -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(f"{index}[{pre(fmt([f'#{index}', *(
95
- f"{dst} := phi({", ".join(f"{block_indexes.get(src_block, "<dead>")}: {src_place}"
96
- for src_block, src_place
97
- in sorted(phis.items(), key=lambda x: block_indexes.get(x[0])))})"
98
- for dst, phis in block.phis.items()
99
- ), *block.statements]))}]")
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 "default"]))}| {block_indexes[target]}"
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
- AdvancedDeadCodeElimination(),
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 = ast.parse(Path(source_file).read_text(encoding="utf-8"))
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
@@ -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
- **inspect.getclosurevars(fn).nonlocals,
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
- if type(op) is ast.Eq:
835
- result = Num._accept_(l_val is r_val)
836
- elif type(op) is ast.NotEq:
837
- result = Num._accept_(l_val is not r_val)
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=[ast.Starred(value=ast.Name(id="args", ctx=ast.Load()), ctx=ast.Load())],
1184
- keywords=[ast.keyword(value=ast.Name(id="kwargs", ctx=ast.Load()), arg=None)],
1185
- lineno=node.lineno,
1186
- col_offset=node.col_offset,
1187
- end_lineno=node.end_lineno,
1188
- end_col_offset=node.end_col_offset,
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 = None | bool | int | float | str | list[JsonValue] | dict[str, JsonValue]
35
+ type JsonValue = bool | int | float | str | list[JsonValue] | dict[str, JsonValue] | None
36
36
 
37
37
 
38
38
  @dataclass
@@ -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 data.
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`, except that it is not loaded from the level data.
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 such as `preprocess`, `update_sequential`, and `touch`.
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, *_args: T):
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__}({", ".join(str(self[i]) for i in range(self.size()))})"
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__}({", ".join(repr(self[i]) for i in range(self.size()))})"
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_()
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import random
4
- from abc import ABC, abstractmethod
5
- from collections.abc import Callable, Sequence
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](Sequence, ABC):
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."""
@@ -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 alloc, copy
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, alloc(Array[element_type, capacity]))
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, alloc(Array[_ArrayMapEntry[key_type, value_type], capacity]))
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."""