sonolus.py 0.1.9__py3-none-any.whl → 0.2.1__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/optimize/constant_evaluation.py +2 -2
- sonolus/backend/optimize/optimize.py +12 -4
- sonolus/backend/optimize/passes.py +2 -1
- sonolus/backend/place.py +95 -14
- sonolus/backend/visitor.py +51 -3
- sonolus/build/cli.py +60 -9
- sonolus/build/collection.py +68 -26
- sonolus/build/compile.py +85 -27
- sonolus/build/engine.py +166 -40
- sonolus/build/node.py +8 -1
- sonolus/build/project.py +30 -11
- sonolus/script/archetype.py +154 -32
- sonolus/script/array.py +14 -3
- sonolus/script/array_like.py +6 -2
- sonolus/script/containers.py +166 -0
- sonolus/script/debug.py +22 -4
- sonolus/script/effect.py +2 -2
- sonolus/script/engine.py +125 -17
- sonolus/script/internal/builtin_impls.py +21 -2
- sonolus/script/internal/constant.py +8 -4
- sonolus/script/internal/context.py +30 -25
- sonolus/script/internal/math_impls.py +2 -1
- sonolus/script/internal/transient.py +7 -3
- sonolus/script/internal/value.py +33 -11
- sonolus/script/interval.py +8 -3
- sonolus/script/iterator.py +17 -0
- sonolus/script/level.py +113 -10
- sonolus/script/metadata.py +32 -0
- sonolus/script/num.py +35 -15
- sonolus/script/options.py +23 -6
- sonolus/script/pointer.py +11 -1
- sonolus/script/project.py +41 -5
- sonolus/script/quad.py +55 -1
- sonolus/script/record.py +10 -5
- sonolus/script/runtime.py +78 -16
- sonolus/script/sprite.py +18 -1
- sonolus/script/text.py +9 -0
- sonolus/script/ui.py +20 -7
- sonolus/script/values.py +8 -5
- sonolus/script/vec.py +28 -0
- sonolus_py-0.2.1.dist-info/METADATA +10 -0
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.1.dist-info}/RECORD +46 -45
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.1.dist-info}/WHEEL +1 -1
- sonolus_py-0.1.9.dist-info/METADATA +0 -9
- /sonolus/script/{print.py → printing.py} +0 -0
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.1.dist-info}/entry_points.txt +0 -0
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.1.dist-info}/licenses/LICENSE +0 -0
sonolus/script/archetype.py
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
+
from abc import abstractmethod
|
|
4
5
|
from collections.abc import Callable
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from enum import Enum, StrEnum
|
|
7
8
|
from types import FunctionType
|
|
8
9
|
from typing import Annotated, Any, ClassVar, Self, TypedDict, get_origin
|
|
9
10
|
|
|
10
|
-
from sonolus.backend.ir import IRConst, IRInstr
|
|
11
|
+
from sonolus.backend.ir import IRConst, IRExpr, IRInstr, IRPureInstr, IRStmt
|
|
11
12
|
from sonolus.backend.mode import Mode
|
|
12
13
|
from sonolus.backend.ops import Op
|
|
13
|
-
from sonolus.backend.place import BlockPlace
|
|
14
14
|
from sonolus.script.bucket import Bucket, Judgment
|
|
15
15
|
from sonolus.script.internal.callbacks import PLAY_CALLBACKS, PREVIEW_CALLBACKS, WATCH_ARCHETYPE_CALLBACKS, CallbackInfo
|
|
16
16
|
from sonolus.script.internal.context import ctx
|
|
@@ -19,9 +19,9 @@ from sonolus.script.internal.generic import validate_concrete_type
|
|
|
19
19
|
from sonolus.script.internal.impl import meta_fn, validate_value
|
|
20
20
|
from sonolus.script.internal.introspection import get_field_specifiers
|
|
21
21
|
from sonolus.script.internal.native import native_call
|
|
22
|
-
from sonolus.script.internal.value import Value
|
|
22
|
+
from sonolus.script.internal.value import BackingValue, DataValue, Value
|
|
23
23
|
from sonolus.script.num import Num
|
|
24
|
-
from sonolus.script.pointer import _deref
|
|
24
|
+
from sonolus.script.pointer import _backing_deref, _deref
|
|
25
25
|
from sonolus.script.record import Record
|
|
26
26
|
from sonolus.script.values import zeros
|
|
27
27
|
|
|
@@ -43,6 +43,17 @@ class _ArchetypeFieldInfo:
|
|
|
43
43
|
storage: _StorageType
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
class _ExportBackingValue(BackingValue):
|
|
47
|
+
def __init__(self, index: IRExpr):
|
|
48
|
+
self.index = index
|
|
49
|
+
|
|
50
|
+
def read(self) -> IRExpr:
|
|
51
|
+
raise NotImplementedError("Exported fields are write-only")
|
|
52
|
+
|
|
53
|
+
def write(self, value: IRExpr) -> IRStmt:
|
|
54
|
+
return IRInstr(Op.ExportValue, [self.index, value])
|
|
55
|
+
|
|
56
|
+
|
|
46
57
|
class _ArchetypeField(SonolusDescriptor):
|
|
47
58
|
def __init__(self, name: str, data_name: str, storage: _StorageType, offset: int, type_: type[Value]):
|
|
48
59
|
self.name = name
|
|
@@ -69,7 +80,20 @@ class _ArchetypeField(SonolusDescriptor):
|
|
|
69
80
|
case _ArchetypeLevelData(values=values):
|
|
70
81
|
result = values[self.name]
|
|
71
82
|
case _StorageType.EXPORTED:
|
|
72
|
-
|
|
83
|
+
match instance._data_:
|
|
84
|
+
case _ArchetypeSelfData():
|
|
85
|
+
|
|
86
|
+
def backing_source(i: IRExpr):
|
|
87
|
+
return _ExportBackingValue(IRPureInstr(Op.Add, [i, IRConst(self.offset)]))
|
|
88
|
+
|
|
89
|
+
result = _backing_deref(
|
|
90
|
+
backing_source,
|
|
91
|
+
self.type,
|
|
92
|
+
)
|
|
93
|
+
case _ArchetypeReferenceData():
|
|
94
|
+
raise RuntimeError("Exported fields of other entities are not accessible")
|
|
95
|
+
case _ArchetypeLevelData():
|
|
96
|
+
raise RuntimeError("Exported fields are not available in level data")
|
|
73
97
|
case _StorageType.MEMORY:
|
|
74
98
|
match instance._data_:
|
|
75
99
|
case _ArchetypeSelfData():
|
|
@@ -177,6 +201,23 @@ def imported(*, name: str | None = None) -> Any:
|
|
|
177
201
|
return _ArchetypeFieldInfo(name, _StorageType.IMPORTED)
|
|
178
202
|
|
|
179
203
|
|
|
204
|
+
def entity_data() -> Any:
|
|
205
|
+
"""Declare a field as entity data.
|
|
206
|
+
|
|
207
|
+
Entity data is accessible from other entities, but may only be updated in the `preprocess` callback
|
|
208
|
+
and is read-only in other callbacks.
|
|
209
|
+
|
|
210
|
+
It functions like `imported`, except that it is not loaded from the level data.
|
|
211
|
+
|
|
212
|
+
Usage:
|
|
213
|
+
```
|
|
214
|
+
class MyArchetype(PlayArchetype):
|
|
215
|
+
field: int = entity_data()
|
|
216
|
+
```
|
|
217
|
+
"""
|
|
218
|
+
return _ArchetypeFieldInfo(None, _StorageType.IMPORTED)
|
|
219
|
+
|
|
220
|
+
|
|
180
221
|
def exported(*, name: str | None = None) -> Any:
|
|
181
222
|
"""Declare a field as exported.
|
|
182
223
|
|
|
@@ -418,7 +459,11 @@ class _BaseArchetype:
|
|
|
418
459
|
bound.apply_defaults()
|
|
419
460
|
data = []
|
|
420
461
|
for field in cls._memory_fields_.values():
|
|
421
|
-
data.extend(
|
|
462
|
+
data.extend(
|
|
463
|
+
field.type._accept_(
|
|
464
|
+
bound.arguments[field.name] if field.name in bound.arguments else zeros(field.type)
|
|
465
|
+
)._to_list_()
|
|
466
|
+
)
|
|
422
467
|
native_call(Op.Spawn, archetype_id, *(Num(x) for x in data))
|
|
423
468
|
|
|
424
469
|
@classmethod
|
|
@@ -446,9 +491,7 @@ class _BaseArchetype:
|
|
|
446
491
|
raise TypeError("Cannot directly subclass Archetype, use the Archetype subclass for your mode")
|
|
447
492
|
cls._default_callbacks_ = {getattr(cls, cb_info.py_name) for cb_info in cls._supported_callbacks_.values()}
|
|
448
493
|
return
|
|
449
|
-
if getattr(
|
|
450
|
-
raise TypeError("Cannot subclass Archetypes")
|
|
451
|
-
if cls.name is None:
|
|
494
|
+
if cls.name is None or cls.name in {getattr(mro_entry, "name", None) for mro_entry in cls.mro()[1:]}:
|
|
452
495
|
cls.name = cls.__name__
|
|
453
496
|
cls._callbacks_ = []
|
|
454
497
|
for name in cls._supported_callbacks_:
|
|
@@ -463,17 +506,32 @@ class _BaseArchetype:
|
|
|
463
506
|
if cls._field_init_done:
|
|
464
507
|
return
|
|
465
508
|
cls._field_init_done = True
|
|
509
|
+
for mro_entry in cls.mro()[1:]:
|
|
510
|
+
if hasattr(mro_entry, "_field_init_done"):
|
|
511
|
+
mro_entry._init_fields()
|
|
466
512
|
field_specifiers = get_field_specifiers(
|
|
467
513
|
cls, skip={"name", "is_scored", "_callbacks_", "_field_init_done"}
|
|
468
514
|
).items()
|
|
469
|
-
cls
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
515
|
+
if not hasattr(cls, "_imported_fields_"):
|
|
516
|
+
cls._imported_fields_ = {}
|
|
517
|
+
else:
|
|
518
|
+
cls._imported_fields_ = {**cls._imported_fields_}
|
|
519
|
+
if not hasattr(cls, "_exported_fields_"):
|
|
520
|
+
cls._exported_fields_ = {}
|
|
521
|
+
else:
|
|
522
|
+
cls._exported_fields_ = {**cls._exported_fields_}
|
|
523
|
+
if not hasattr(cls, "_memory_fields_"):
|
|
524
|
+
cls._memory_fields_ = {}
|
|
525
|
+
else:
|
|
526
|
+
cls._memory_fields_ = {**cls._memory_fields_}
|
|
527
|
+
if not hasattr(cls, "_shared_memory_fields_"):
|
|
528
|
+
cls._shared_memory_fields_ = {}
|
|
529
|
+
else:
|
|
530
|
+
cls._shared_memory_fields_ = {**cls._shared_memory_fields_}
|
|
531
|
+
imported_offset = sum(field.type._size_() for field in cls._imported_fields_.values())
|
|
532
|
+
exported_offset = sum(field.type._size_() for field in cls._exported_fields_.values())
|
|
533
|
+
memory_offset = sum(field.type._size_() for field in cls._memory_fields_.values())
|
|
534
|
+
shared_memory_offset = sum(field.type._size_() for field in cls._shared_memory_fields_.values())
|
|
477
535
|
for name, value in field_specifiers:
|
|
478
536
|
if value is ClassVar or get_origin(value) is ClassVar:
|
|
479
537
|
continue
|
|
@@ -504,24 +562,32 @@ class _BaseArchetype:
|
|
|
504
562
|
name, field_info.name or name, field_info.storage, imported_offset, field_type
|
|
505
563
|
)
|
|
506
564
|
imported_offset += field_type._size_()
|
|
565
|
+
if imported_offset > _ENTITY_DATA_SIZE:
|
|
566
|
+
raise ValueError("Imported fields exceed entity data size")
|
|
507
567
|
setattr(cls, name, cls._imported_fields_[name])
|
|
508
568
|
case _StorageType.EXPORTED:
|
|
509
569
|
cls._exported_fields_[name] = _ArchetypeField(
|
|
510
570
|
name, field_info.name or name, field_info.storage, exported_offset, field_type
|
|
511
571
|
)
|
|
512
572
|
exported_offset += field_type._size_()
|
|
573
|
+
if exported_offset > _ENTITY_DATA_SIZE:
|
|
574
|
+
raise ValueError("Exported fields exceed entity data size")
|
|
513
575
|
setattr(cls, name, cls._exported_fields_[name])
|
|
514
576
|
case _StorageType.MEMORY:
|
|
515
577
|
cls._memory_fields_[name] = _ArchetypeField(
|
|
516
578
|
name, field_info.name or name, field_info.storage, memory_offset, field_type
|
|
517
579
|
)
|
|
518
580
|
memory_offset += field_type._size_()
|
|
581
|
+
if memory_offset > _ENTITY_MEMORY_SIZE:
|
|
582
|
+
raise ValueError("Memory fields exceed entity memory size")
|
|
519
583
|
setattr(cls, name, cls._memory_fields_[name])
|
|
520
584
|
case _StorageType.SHARED:
|
|
521
585
|
cls._shared_memory_fields_[name] = _ArchetypeField(
|
|
522
586
|
name, field_info.name or name, field_info.storage, shared_memory_offset, field_type
|
|
523
587
|
)
|
|
524
588
|
shared_memory_offset += field_type._size_()
|
|
589
|
+
if shared_memory_offset > _ENTITY_SHARED_MEMORY_SIZE:
|
|
590
|
+
raise ValueError("Shared memory fields exceed entity shared memory size")
|
|
525
591
|
setattr(cls, name, cls._shared_memory_fields_[name])
|
|
526
592
|
cls._imported_keys_ = {
|
|
527
593
|
name: i
|
|
@@ -541,6 +607,35 @@ class _BaseArchetype:
|
|
|
541
607
|
cls._spawn_signature_ = inspect.Signature(
|
|
542
608
|
[inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD) for name in cls._memory_fields_]
|
|
543
609
|
)
|
|
610
|
+
cls._post_init_fields()
|
|
611
|
+
|
|
612
|
+
@property
|
|
613
|
+
@abstractmethod
|
|
614
|
+
def index(self) -> int:
|
|
615
|
+
"""The index of this entity."""
|
|
616
|
+
raise NotImplementedError
|
|
617
|
+
|
|
618
|
+
@meta_fn
|
|
619
|
+
def ref(self) -> EntityRef[Self]:
|
|
620
|
+
"""Get a reference to this entity.
|
|
621
|
+
|
|
622
|
+
Valid both in level data and in callbacks.
|
|
623
|
+
"""
|
|
624
|
+
match self._data_:
|
|
625
|
+
case _ArchetypeSelfData():
|
|
626
|
+
return EntityRef[type(self)](index=self.index)
|
|
627
|
+
case _ArchetypeReferenceData(index=index):
|
|
628
|
+
return EntityRef[type(self)](index=index)
|
|
629
|
+
case _ArchetypeLevelData():
|
|
630
|
+
result = EntityRef[type(self)](index=-1)
|
|
631
|
+
result._ref_ = self
|
|
632
|
+
return result
|
|
633
|
+
case _:
|
|
634
|
+
raise RuntimeError("Invalid entity data")
|
|
635
|
+
|
|
636
|
+
@classmethod
|
|
637
|
+
def _post_init_fields(cls):
|
|
638
|
+
pass
|
|
544
639
|
|
|
545
640
|
|
|
546
641
|
class PlayArchetype(_BaseArchetype):
|
|
@@ -682,6 +777,7 @@ class PlayArchetype(_BaseArchetype):
|
|
|
682
777
|
return self._info.state == 2
|
|
683
778
|
|
|
684
779
|
@property
|
|
780
|
+
@meta_fn
|
|
685
781
|
def life(self) -> ArchetypeLife:
|
|
686
782
|
"""How this entity contributes to life."""
|
|
687
783
|
if not ctx():
|
|
@@ -693,6 +789,7 @@ class PlayArchetype(_BaseArchetype):
|
|
|
693
789
|
raise RuntimeError("Life is not available in level data")
|
|
694
790
|
|
|
695
791
|
@property
|
|
792
|
+
@meta_fn
|
|
696
793
|
def result(self) -> PlayEntityInput:
|
|
697
794
|
"""The result of this entity.
|
|
698
795
|
|
|
@@ -706,17 +803,6 @@ class PlayArchetype(_BaseArchetype):
|
|
|
706
803
|
case _:
|
|
707
804
|
raise RuntimeError("Result is only accessible from the entity itself")
|
|
708
805
|
|
|
709
|
-
def ref(self):
|
|
710
|
-
"""Get a reference to this entity for creating level data.
|
|
711
|
-
|
|
712
|
-
Not valid elsewhere.
|
|
713
|
-
"""
|
|
714
|
-
if not isinstance(self._data_, _ArchetypeLevelData):
|
|
715
|
-
raise RuntimeError("Entity is not level data")
|
|
716
|
-
result = EntityRef[type(self)](index=-1)
|
|
717
|
-
result._ref_ = self
|
|
718
|
-
return result
|
|
719
|
-
|
|
720
806
|
|
|
721
807
|
class WatchArchetype(_BaseArchetype):
|
|
722
808
|
"""Base class for watch mode archetypes.
|
|
@@ -801,6 +887,7 @@ class WatchArchetype(_BaseArchetype):
|
|
|
801
887
|
return self._info.state == 1
|
|
802
888
|
|
|
803
889
|
@property
|
|
890
|
+
@meta_fn
|
|
804
891
|
def life(self) -> ArchetypeLife:
|
|
805
892
|
"""How this entity contributes to life."""
|
|
806
893
|
if not ctx():
|
|
@@ -812,6 +899,7 @@ class WatchArchetype(_BaseArchetype):
|
|
|
812
899
|
raise RuntimeError("Life is not available in level data")
|
|
813
900
|
|
|
814
901
|
@property
|
|
902
|
+
@meta_fn
|
|
815
903
|
def result(self) -> WatchEntityInput:
|
|
816
904
|
"""The result of this entity.
|
|
817
905
|
|
|
@@ -825,6 +913,11 @@ class WatchArchetype(_BaseArchetype):
|
|
|
825
913
|
case _:
|
|
826
914
|
raise RuntimeError("Result is only accessible from the entity itself")
|
|
827
915
|
|
|
916
|
+
@classmethod
|
|
917
|
+
def _post_init_fields(cls):
|
|
918
|
+
if cls._exported_fields_:
|
|
919
|
+
raise RuntimeError("Watch archetypes cannot have exported fields")
|
|
920
|
+
|
|
828
921
|
|
|
829
922
|
class PreviewArchetype(_BaseArchetype):
|
|
830
923
|
"""Base class for preview mode archetypes.
|
|
@@ -857,6 +950,7 @@ class PreviewArchetype(_BaseArchetype):
|
|
|
857
950
|
"""
|
|
858
951
|
|
|
859
952
|
@property
|
|
953
|
+
@meta_fn
|
|
860
954
|
def _info(self) -> PreviewEntityInfo:
|
|
861
955
|
if not ctx():
|
|
862
956
|
raise RuntimeError("Calling info is only allowed within a callback")
|
|
@@ -873,6 +967,11 @@ class PreviewArchetype(_BaseArchetype):
|
|
|
873
967
|
"""The index of this entity."""
|
|
874
968
|
return self._info.index
|
|
875
969
|
|
|
970
|
+
@classmethod
|
|
971
|
+
def _post_init_fields(cls):
|
|
972
|
+
if cls._exported_fields_:
|
|
973
|
+
raise RuntimeError("Preview archetypes cannot have exported fields")
|
|
974
|
+
|
|
876
975
|
|
|
877
976
|
@meta_fn
|
|
878
977
|
def entity_info_at(index: Num) -> PlayEntityInfo | WatchEntityInfo | PreviewEntityInfo:
|
|
@@ -974,24 +1073,38 @@ class WatchEntityInput(Record):
|
|
|
974
1073
|
|
|
975
1074
|
|
|
976
1075
|
class EntityRef[A: _BaseArchetype](Record):
|
|
977
|
-
"""Reference to another entity.
|
|
1076
|
+
"""Reference to another entity.
|
|
1077
|
+
|
|
1078
|
+
May be used with `Any` to reference an unknown archetype.
|
|
1079
|
+
|
|
1080
|
+
Usage:
|
|
1081
|
+
```
|
|
1082
|
+
class MyArchetype(PlayArchetype):
|
|
1083
|
+
ref_1: EntityRef[OtherArchetype] = imported()
|
|
1084
|
+
ref_2: EntityRef[Any] = imported()
|
|
1085
|
+
```
|
|
1086
|
+
"""
|
|
978
1087
|
|
|
979
1088
|
index: int
|
|
980
1089
|
|
|
981
1090
|
@classmethod
|
|
982
1091
|
def archetype(cls) -> type[A]:
|
|
1092
|
+
"""Get the archetype type."""
|
|
983
1093
|
return cls.type_var_value(A)
|
|
984
1094
|
|
|
985
1095
|
def with_archetype(self, archetype: type[A]) -> EntityRef[A]:
|
|
1096
|
+
"""Return a new reference with the given archetype type."""
|
|
986
1097
|
return EntityRef[archetype](index=self.index)
|
|
987
1098
|
|
|
988
1099
|
def get(self) -> A:
|
|
1100
|
+
"""Get the entity."""
|
|
989
1101
|
return self.archetype().at(self.index)
|
|
990
1102
|
|
|
991
|
-
def
|
|
1103
|
+
def archetype_matches(self) -> bool:
|
|
1104
|
+
"""Check if entity at the index is precisely of the archetype."""
|
|
992
1105
|
return self.index >= 0 and self.archetype().is_at(self.index)
|
|
993
1106
|
|
|
994
|
-
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[
|
|
1107
|
+
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
|
|
995
1108
|
ref = getattr(self, "_ref_", None)
|
|
996
1109
|
if ref is None:
|
|
997
1110
|
return [self.index]
|
|
@@ -1000,9 +1113,18 @@ class EntityRef[A: _BaseArchetype](Record):
|
|
|
1000
1113
|
raise KeyError("Reference to entity not in level data")
|
|
1001
1114
|
return [level_refs[ref]]
|
|
1002
1115
|
|
|
1116
|
+
def _copy_from_(self, value: Self):
|
|
1117
|
+
super()._copy_from_(value)
|
|
1118
|
+
if hasattr(value, "_ref_"):
|
|
1119
|
+
self._ref_ = value._ref_
|
|
1120
|
+
|
|
1003
1121
|
@classmethod
|
|
1004
1122
|
def _accepts_(cls, value: Any) -> bool:
|
|
1005
|
-
return
|
|
1123
|
+
return (
|
|
1124
|
+
super()._accepts_(value)
|
|
1125
|
+
or (cls._type_args_ and cls.archetype() is Any and isinstance(value, EntityRef))
|
|
1126
|
+
or (issubclass(type(value), EntityRef) and issubclass(value.archetype(), cls.archetype()))
|
|
1127
|
+
)
|
|
1006
1128
|
|
|
1007
1129
|
@classmethod
|
|
1008
1130
|
def _accept_(cls, value: Any) -> Self:
|
sonolus/script/array.py
CHANGED
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
from collections.abc import Iterable
|
|
5
5
|
from typing import Any, Self, final
|
|
6
6
|
|
|
7
|
+
from sonolus.backend.ir import IRConst, IRSet
|
|
7
8
|
from sonolus.backend.place import BlockPlace
|
|
8
9
|
from sonolus.script.array_like import ArrayLike
|
|
9
10
|
from sonolus.script.debug import assert_unreachable
|
|
@@ -11,7 +12,7 @@ from sonolus.script.internal.context import ctx
|
|
|
11
12
|
from sonolus.script.internal.error import InternalError
|
|
12
13
|
from sonolus.script.internal.generic import GenericValue
|
|
13
14
|
from sonolus.script.internal.impl import meta_fn, validate_value
|
|
14
|
-
from sonolus.script.internal.value import Value
|
|
15
|
+
from sonolus.script.internal.value import DataValue, Value
|
|
15
16
|
from sonolus.script.num import Num
|
|
16
17
|
|
|
17
18
|
|
|
@@ -107,11 +108,11 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
|
|
|
107
108
|
return self
|
|
108
109
|
|
|
109
110
|
@classmethod
|
|
110
|
-
def _from_list_(cls, values: Iterable[
|
|
111
|
+
def _from_list_(cls, values: Iterable[DataValue]) -> Self:
|
|
111
112
|
iterator = iter(values)
|
|
112
113
|
return cls(*(cls.element_type()._from_list_(iterator) for _ in range(cls.size())))
|
|
113
114
|
|
|
114
|
-
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[
|
|
115
|
+
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
|
|
115
116
|
match self._value:
|
|
116
117
|
case list():
|
|
117
118
|
return [entry for value in self._value for entry in value._to_list_(level_refs)]
|
|
@@ -159,6 +160,16 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
|
|
|
159
160
|
else:
|
|
160
161
|
return cls._with_value([cls.element_type()._alloc_() for _ in range(cls.size())])
|
|
161
162
|
|
|
163
|
+
@classmethod
|
|
164
|
+
def _zero_(cls) -> Self:
|
|
165
|
+
if ctx():
|
|
166
|
+
place = ctx().alloc(size=cls._size_())
|
|
167
|
+
result: Self = cls._from_place_(place)
|
|
168
|
+
ctx().add_statements(*[IRSet(place.add_offset(i), IRConst(0)) for i in range(cls._size_())])
|
|
169
|
+
return result
|
|
170
|
+
else:
|
|
171
|
+
return cls._with_value([cls.element_type()._zero_() for _ in range(cls.size())])
|
|
172
|
+
|
|
162
173
|
def __len__(self):
|
|
163
174
|
return self.size()
|
|
164
175
|
|
sonolus/script/array_like.py
CHANGED
|
@@ -158,10 +158,14 @@ class ArrayLike[T](Sequence, ABC):
|
|
|
158
158
|
return min_index
|
|
159
159
|
|
|
160
160
|
def _max_(self, key: Callable[T, Any] | None = None) -> T:
|
|
161
|
-
|
|
161
|
+
index = self.index_of_max(key=key)
|
|
162
|
+
assert index != -1
|
|
163
|
+
return self[index]
|
|
162
164
|
|
|
163
165
|
def _min_(self, key: Callable[T, Any] | None = None) -> T:
|
|
164
|
-
|
|
166
|
+
index = self.index_of_min(key=key)
|
|
167
|
+
assert index != -1
|
|
168
|
+
return self[index]
|
|
165
169
|
|
|
166
170
|
def swap(self, i: Num, j: Num, /):
|
|
167
171
|
"""Swap the values at the given indices.
|
sonolus/script/containers.py
CHANGED
|
@@ -1,13 +1,44 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from sonolus.backend.visitor import compile_and_call
|
|
3
4
|
from sonolus.script.array import Array
|
|
4
5
|
from sonolus.script.array_like import ArrayLike
|
|
5
6
|
from sonolus.script.debug import error
|
|
7
|
+
from sonolus.script.internal.context import ctx
|
|
8
|
+
from sonolus.script.internal.impl import meta_fn
|
|
6
9
|
from sonolus.script.iterator import SonolusIterator
|
|
10
|
+
from sonolus.script.num import Num
|
|
11
|
+
from sonolus.script.pointer import _deref
|
|
7
12
|
from sonolus.script.record import Record
|
|
8
13
|
from sonolus.script.values import alloc, copy
|
|
9
14
|
|
|
10
15
|
|
|
16
|
+
class Box[T](Record):
|
|
17
|
+
"""A box that contains a value.
|
|
18
|
+
|
|
19
|
+
This can be helpful for generic code that can handle both Num and non-Num types.
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
```python
|
|
23
|
+
Box[T](value: T)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
```python
|
|
28
|
+
box = Box(1)
|
|
29
|
+
box = Box[int](2)
|
|
30
|
+
|
|
31
|
+
x: T = ...
|
|
32
|
+
y: T = ...
|
|
33
|
+
box = Box(x)
|
|
34
|
+
box.value = y # Works regardless of whether x is a Num or not
|
|
35
|
+
```
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
value: T
|
|
39
|
+
"""The value contained in the box."""
|
|
40
|
+
|
|
41
|
+
|
|
11
42
|
class Pair[T, U](Record):
|
|
12
43
|
"""A generic pair of values.
|
|
13
44
|
|
|
@@ -129,6 +160,17 @@ class VarArray[T, Capacity](Record, ArrayLike[T]):
|
|
|
129
160
|
self._array[self._size] = value
|
|
130
161
|
self._size += 1
|
|
131
162
|
|
|
163
|
+
def append_unchecked(self, value: T):
|
|
164
|
+
"""Append the given value to the end of the array without checking the capacity.
|
|
165
|
+
|
|
166
|
+
Use with caution as this may cause hard to debug issues if the array is full.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
value: The value to append.
|
|
170
|
+
"""
|
|
171
|
+
self._array[self._size] = value
|
|
172
|
+
self._size += 1
|
|
173
|
+
|
|
132
174
|
def extend(self, values: ArrayLike[T]):
|
|
133
175
|
"""Appends copies of the values in the given array to the end of the array.
|
|
134
176
|
|
|
@@ -258,6 +300,130 @@ class VarArray[T, Capacity](Record, ArrayLike[T]):
|
|
|
258
300
|
raise TypeError("unhashable type: 'VarArray'")
|
|
259
301
|
|
|
260
302
|
|
|
303
|
+
class ArrayPointer[T](Record, ArrayLike[T]):
|
|
304
|
+
"""An array defined by a size and pointer to the first element.
|
|
305
|
+
|
|
306
|
+
This is intended to be created internally and improper use may result in hard to debug issues.
|
|
307
|
+
|
|
308
|
+
Usage:
|
|
309
|
+
```python
|
|
310
|
+
ArrayPointer[T](size: int, block: int, offset: int)
|
|
311
|
+
```
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
size: int
|
|
315
|
+
block: int
|
|
316
|
+
offset: int
|
|
317
|
+
|
|
318
|
+
def __len__(self) -> int:
|
|
319
|
+
"""Return the number of elements in the array."""
|
|
320
|
+
return self.size
|
|
321
|
+
|
|
322
|
+
@classmethod
|
|
323
|
+
def element_type(cls) -> type[T]:
|
|
324
|
+
"""Return the type of the elements in the array."""
|
|
325
|
+
return cls.type_var_value(T)
|
|
326
|
+
|
|
327
|
+
def _check_index(self, index: int):
|
|
328
|
+
assert 0 <= index < self.size
|
|
329
|
+
|
|
330
|
+
@meta_fn
|
|
331
|
+
def _get_item(self, item: int) -> T:
|
|
332
|
+
if not ctx():
|
|
333
|
+
raise TypeError("ArrayPointer values cannot be accessed outside of a context")
|
|
334
|
+
return _deref(
|
|
335
|
+
self.block,
|
|
336
|
+
self.offset + Num._accept_(item) * Num._accept_(self.element_type()._size_()),
|
|
337
|
+
self.element_type(),
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
@meta_fn
|
|
341
|
+
def __getitem__(self, item: int) -> T:
|
|
342
|
+
compile_and_call(self._check_index, item)
|
|
343
|
+
return self._get_item(item)._get_()
|
|
344
|
+
|
|
345
|
+
@meta_fn
|
|
346
|
+
def __setitem__(self, key: int, value: T):
|
|
347
|
+
compile_and_call(self._check_index, key)
|
|
348
|
+
dst = self._get_item(key)
|
|
349
|
+
if self.element_type()._is_value_type_():
|
|
350
|
+
dst._set_(value)
|
|
351
|
+
else:
|
|
352
|
+
dst._copy_from__(value)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class ArraySet[T, Capacity](Record):
|
|
356
|
+
"""A set implemented as an array with a fixed maximum capacity.
|
|
357
|
+
|
|
358
|
+
Usage:
|
|
359
|
+
```python
|
|
360
|
+
ArraySet[T, Capacity].new() # Create a new empty set
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Examples:
|
|
364
|
+
```python
|
|
365
|
+
s = ArraySet[int, 10].new()
|
|
366
|
+
s.add(1)
|
|
367
|
+
s.add(2)
|
|
368
|
+
assert 1 in s
|
|
369
|
+
assert 3 not in s
|
|
370
|
+
s.remove(1)
|
|
371
|
+
assert 1 not in s
|
|
372
|
+
```
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
_values: VarArray[T, Capacity]
|
|
376
|
+
|
|
377
|
+
@classmethod
|
|
378
|
+
def new(cls):
|
|
379
|
+
"""Create a new empty set."""
|
|
380
|
+
element_type = cls.type_var_value(T)
|
|
381
|
+
capacity = cls.type_var_value(Capacity)
|
|
382
|
+
return cls(VarArray[element_type, capacity].new())
|
|
383
|
+
|
|
384
|
+
def __len__(self):
|
|
385
|
+
"""Return the number of elements in the set."""
|
|
386
|
+
return len(self._values)
|
|
387
|
+
|
|
388
|
+
def __contains__(self, value):
|
|
389
|
+
"""Return whether the given value is present in the set."""
|
|
390
|
+
return value in self._values
|
|
391
|
+
|
|
392
|
+
def __iter__(self):
|
|
393
|
+
"""Return an iterator over the values in the set."""
|
|
394
|
+
return self._values.__iter__()
|
|
395
|
+
|
|
396
|
+
def add(self, value: T) -> bool:
|
|
397
|
+
"""Add a copy of the given value to the set.
|
|
398
|
+
|
|
399
|
+
This has no effect and returns False if the value is already present or if the set is full.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
value: The value to add.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
True if the value was added, False otherwise.
|
|
406
|
+
"""
|
|
407
|
+
return self._values.set_add(value)
|
|
408
|
+
|
|
409
|
+
def remove(self, value: T) -> bool:
|
|
410
|
+
"""Remove the given value from the set.
|
|
411
|
+
|
|
412
|
+
This has no effect and returns False if the value is not present.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
value: The value to remove.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
True if the value was removed, False otherwise.
|
|
419
|
+
"""
|
|
420
|
+
return self._values.set_remove(value)
|
|
421
|
+
|
|
422
|
+
def clear(self):
|
|
423
|
+
"""Clear the set, removing all elements."""
|
|
424
|
+
self._values.clear()
|
|
425
|
+
|
|
426
|
+
|
|
261
427
|
class _ArrayMapEntry[K, V](Record):
|
|
262
428
|
key: K
|
|
263
429
|
value: V
|
sonolus/script/debug.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from collections.abc import Callable
|
|
1
|
+
from collections.abc import Callable, Sequence
|
|
2
2
|
from contextvars import ContextVar
|
|
3
3
|
from typing import Any, Never
|
|
4
4
|
|
|
@@ -16,8 +16,13 @@ debug_log_callback = ContextVar[Callable[[Num], None]]("debug_log_callback")
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
@meta_fn
|
|
19
|
-
def error(message: str | None = None) ->
|
|
20
|
-
|
|
19
|
+
def error(message: str | None = None) -> Never:
|
|
20
|
+
"""Raise an error.
|
|
21
|
+
|
|
22
|
+
This function is used to raise an error during runtime.
|
|
23
|
+
When this happens, the game will pause in debug mode. The current callback will also immediately return 0.
|
|
24
|
+
"""
|
|
25
|
+
message = validate_value(message)._as_py_() if message is not None else "Error"
|
|
21
26
|
if not isinstance(message, str):
|
|
22
27
|
raise ValueError("Expected a string")
|
|
23
28
|
if ctx():
|
|
@@ -28,6 +33,19 @@ def error(message: str | None = None) -> None:
|
|
|
28
33
|
raise RuntimeError(message)
|
|
29
34
|
|
|
30
35
|
|
|
36
|
+
@meta_fn
|
|
37
|
+
def static_error(message: str | None = None) -> Never:
|
|
38
|
+
"""Raise a static error.
|
|
39
|
+
|
|
40
|
+
This function is used to raise an error during compile-time if the compiler cannot guarantee that
|
|
41
|
+
this function will not be called during runtime.
|
|
42
|
+
"""
|
|
43
|
+
message = validate_value(message)._as_py_() if message is not None else "Error"
|
|
44
|
+
if not isinstance(message, str):
|
|
45
|
+
raise ValueError("Expected a string")
|
|
46
|
+
raise RuntimeError(message)
|
|
47
|
+
|
|
48
|
+
|
|
31
49
|
@meta_fn
|
|
32
50
|
def debug_log(value: Num):
|
|
33
51
|
"""Log a value in debug mode."""
|
|
@@ -75,7 +93,7 @@ def terminate():
|
|
|
75
93
|
raise RuntimeError("Terminated")
|
|
76
94
|
|
|
77
95
|
|
|
78
|
-
def visualize_cfg(fn: Callable[[], Any], passes:
|
|
96
|
+
def visualize_cfg(fn: Callable[[], Any], passes: Sequence[CompilerPass] | None = None) -> str:
|
|
79
97
|
from sonolus.build.compile import callback_to_cfg
|
|
80
98
|
|
|
81
99
|
if passes is None:
|
sonolus/script/effect.py
CHANGED
|
@@ -25,7 +25,7 @@ class Effect(Record):
|
|
|
25
25
|
"""Return whether the effect clip is available."""
|
|
26
26
|
return _has_effect_clip(self.id)
|
|
27
27
|
|
|
28
|
-
def play(self, distance: float) -> None:
|
|
28
|
+
def play(self, distance: float = 0) -> None:
|
|
29
29
|
"""Play the effect clip.
|
|
30
30
|
|
|
31
31
|
If the clip was already played within the specified distance, it will be skipped.
|
|
@@ -35,7 +35,7 @@ class Effect(Record):
|
|
|
35
35
|
"""
|
|
36
36
|
_play(self.id, distance)
|
|
37
37
|
|
|
38
|
-
def schedule(self, time: float, distance: float) -> None:
|
|
38
|
+
def schedule(self, time: float, distance: float = 0) -> None:
|
|
39
39
|
"""Schedule the effect clip to play at a specific time.
|
|
40
40
|
|
|
41
41
|
This is not suitable for real-time effects such as responses to user input. Use `play` instead.
|