Types¶
Sonolus.py has 3 core types: Num
, Array
, and Record
. representing numeric values, fixed-size arrays,
and custom data structures, respectively. Arrays and records can be nested within each other to create complex data
structures.
Additionally, Sonolus.py supports the built-in types tuple
, dict
, str
, classes and functions, and
the constants None
, Ellipsis
, and NotImplemented
.
Num¶
Num
is the numeric and boolean type in Sonolus.py. It is interchangeable with int
, float
, and bool
.
Sonolus.py will treat any of these types as Num
, but it's recommended to use what's appropriate for clarity.
The Sonolus app uses 32-bit floating-point numbers for all numeric values, so precision may be lower compared to Python when running on Sonolus.
Infinity, NaN, and values outside the range of 32-bit floating-point numbers are not supported.
You can import Num
from sonolus.script.num
:
from sonolus.script.num import Num
Declaration¶
Nums can be declared using standard Python syntax.
a = 1
b = 2.5
c = True
Operations¶
Nums support most of the standard Python operations:
- Comparison operators:
==
,!=
,<
,>
,<=
,>=
- Arithmetic operators:
+
,-
,*
,/
,//
,%
,**
- Unary operators:
+
,-
Note
Floating point precision may be lower when running on Sonolus compared to Python. Care should be taken when performing precision-sensitive operations.
Nums are the only supported type for boolean operations and control flow conditions.
As a condition, any nonzero value is considered true, and 0
is considered false.
- Logical operators:
and
,or
,not
- Ternary expressions:
... if <condition> else ...
- If statements:
if <condition>:
,elif <condition>:
- While loops:
while <condition>:
- Case guards:
case ... if <condition>:
Instance Checks¶
Since Num
is interchangeable with int
, float
, and bool
, only Num
is supported for type checks.
x = ...
# Ok:
isinstance(x, Num)
match x:
case Num(value):
...
# Not ok:
isinstance(x, int)
isinstance(x, float)
isinstance(x, bool)
match x:
case int(value):
...
case float(value):
...
case bool(value):
...
Conversion¶
Calling int
, float
, or bool
is only supported for an argument of type Num
.
Details:
int
: Equivalent tomath.trunc
.float
: Validates that the value is aNum
and returns it as is.bool
: Validates that the value is aNum
and returns1
forTrue
and0
forFalse
.
Array¶
Array[T, Size]
stores a fixed number of elements of the same type.
It has two type parameters:
T
: The type of the elements.Size
: The number of elements.
You can import Array
from sonolus.script.array
:
from sonolus.script.array import Array
Declaration¶
Arrays can be created using its constructor or the unary +
operator.
a1 = Array[int, 3](1, 2, 3)
a2 = Array[int, 0]()
a3 = +Array[int, 3] # Create a zero-initialized array
If at least one element is provided, the element type and size can be inferred:
a3 = Array(1, 2, 3)
Since Array
takes type parameters, it is considered a generic type. A version of Array
with type parameters provided
is considered a concrete type.
Array # The Generic Array type
Array[int, 3] # A concrete Array type
The element type of an array must be concrete (not generic) and the size must be a non-negative compile-time constant integer:
# Ok
a4 = Array[Array[int, 3], 2](Array(1, 2, 3), Array(4, 5, 6))
# Not ok:
a5 = Array[int, 0.5]() # The size must be a non-negative integer
a6 = Array[Array, 2](Array(1, 2, 3), Array(4, 5, 6)) # The element type must be concrete (not generic)
Copies are made of any values provided to the constructor:
pair = Pair(1, 2)
a = Array[Pair, 1](pair)
assert a[0] == Pair(1, 2)
pair.x = 3
assert a[0] == Pair(1, 2) # The value in the array is independent of the original value
Operations¶
An array can be copied with the unary +
operator, which creates a new array with the same elements:
a = Array(1, 2, 3)
b = +a
assert b == Array(1, 2, 3)
The value of an array can be copied from another array using the copy from operator (@=
)1:
source_array = Array(1, 2, 3)
destination_array = Array(0, 0, 0)
destination_array @= source_array
assert destination_array == Array(1, 2, 3)
Arrays can be compared for equality and inequality:
assert Array(1, 2, 3) == Array(1, 2, 3)
assert Array(1, 2, 3) != Array(4, 5, 6)
Elements can be accessed by index:
a = Array(1, 2, 3)
assert a[0] == 1
assert a[1] == 2
assert a[2] == 3
Elements can be updated by index, copying the given value into the corresponding element of the array:
a = Array(1, 2, 3)
a[0] = 4
assert a == Array(4, 2, 3)
Warning
If a value in an array is not a Num
, updating it will copy the given value into the corresponding element
of the array. However, that element remains independent of the original value, which may lead to unexpected
results when updating either value.
pair = Pair(1, 2)
a = Array(Pair(0, 0))
a[0] = pair # or equivalently: a[0] @= pair
assert a[0] == Pair(1, 2)
pair.x = 3
assert a[0] == Pair(1, 2) # The value in the array is independent of the original value
For clarity, it's recommended to use the copy from operator (@=
) when updating elements that are known to be
an array or record.
a[0] @= pair
The length of an array can be accessed using the len()
function:
assert len(Array(1, 2, 3)) == 3
Arrays can be iterated over using a for loop:
a = Array(1, 2, 3)
for element in a:
debug_log(element)
Other functionality:
Array
inherits from ArrayLike
and supports all of its methods.
Instance Checks¶
Any array is considered an instance of the generic Array
type.
a = Array(1, 2, 3)
assert isinstance(a, Array)
Only an array with the exact element type and size is considered an instance of a concrete Array[T, Size]
type.
a = Array(1, 2, 3)
assert isinstance(a, Array[int, 3])
assert not isinstance(a, Array[int, 2])
assert not isinstance(a, Array[Pair, 3])
Enums¶
There is limited support for enums containing Num
values. Methods on enums are not supported.
When used as a type, any enum class is treated as Num
and no enforcement is done on the values.
class MyEnum(IntEnum):
A = 1
B = 2
a = Array[MyEnum, 2](MyEnum.A, MyEnum.B)
b = Array[MyEnum, 2](1, 2)
Record¶
Record
is the base class for user-defined types in Sonolus.py. It functions similarly to dataclasses.
You can import Record
from sonolus.script.record
:
from sonolus.script.record import Record
Declaration¶
A record can be defined by inheriting from Record
and defining zero or more fields as class attributes:
class MyPair(Record):
first: int
second: int
Fields must be annotated by Num
(or equivalently int
, float
, or bool
),
a concrete array type, or a concrete record type.
# Not ok:
class MyRecord(Record):
array: Array # Array is not concrete since it has unspecified type parameters
A Record
subclass cannot be further subclassed.
# Not ok:
class MyPairSubclass(MyPair):
third: int
Instantiation¶
A constructor is automatically generated for the Record
class and the unary +
operator can also be used to create a zero-initialized record.
pair_1 = MyPair(1, 2)
pair_2 = MyPair(first=1, second=2)
pair_3 = +MyPair # Create a zero-initialized record
Generics¶
Record
supports generics. If at least one type parameter is provided in the class definition, a generic
record type is created.
class MyGenericPair[T, U](Record):
first: T
second: U
class ContainsArray[T, Size](Record):
array: Array[T, Size]
Generic type parameters can be specified explicitly when instantiating a generic or inferred from the provided values:
pair_1 = MyGenericPair[int, int](1, 2)
pair_2 = MyGenericPair(1, 2)
The value of a type parameter can be accessed via the type_var_value()
classmethod.
class MyGenericRecord[T](Record):
value: T
def my_type(self) -> type:
return self.type_var_value(T)
assert MyGenericRecord(1).my_type() == Num
Operations¶
A record can be copied with the unary +
operator, which creates a new record with the same field values:
pair = MyPair(1, 2)
copy_pair = +pair
assert copy_pair == MyPair(1, 2)
The value of a record can be copied from another record using the copy from operator (@=
)1:
source_record = MyPair(1, 2)
destination_record = MyPair(0, 0)
destination_record @= source_record
assert destination_record == MyPair(1, 2)
Records can be compared for equality and inequality:
assert MyPair(1, 2) == MyPair(1, 2)
assert MyPair(1, 2) != MyPair(3, 4)
Dunder methods can be implemented to define custom behavior for records:
class MyAddablePair(Record):
first: int
second: int
def __add__(self, other: MyAddablePair) -> MyAddablePair:
return MyAddablePair(self.first + other.first, self.second + other.second)
If a dunder method has an in-place variant and the in-place method is not explicitly implemented
(e.g. __iadd__
is the in-place variant of __add__
), Record
will automatically generate one that
modifies the instance in place:
pair = MyAddablePair(1, 2)
reference = pair
pair += MyAddablePair(3, 4)
assert pair == reference == MyAddablePair(4, 6) # The instance is modified in place
Regular methods, properties, classmethods, and staticmethods can also be defined in a Record
subclass.
class MyRecord(Record):
def my_method(self):
...
@property
def my_property(self):
...
@property.setter
def my_property(self, value):
...
@classmethod
def my_classmethod(cls):
...
@staticmethod
def my_staticmethod():
...
Fields can be accessed and updated using the dot operator:
pair = MyPair(1, 2)
assert pair.first == 1
assert pair.second == 2
pair.first = 3
assert pair == MyPair(3, 2)
Warning
If a value in a record is not a Num
, updating it will copy the given value into the corresponding field
of the record. However, that field remains independent of the original value.
array = Array(1, 2, 3)
record = MyRecord(array)
record.array = Array(4, 5, 6) # or equivalently: record.array @= Array(4, 5, 6)
assert record.array == Array(4, 5, 6)
array[0] = 7
assert record.array == Array(4, 5, 6) # The value in the record is independent of the original
For clarity, it's recommended to use the copy from operator (@=
) when updating fields that are known to be
an array or record.
record.array @= array
Instance Checks¶
Any record is considered an instance of the generic Record
type:
pair = MyPair(1, 2)
assert isinstance(pair, Record)
If a record is generic, any instance of it is considered an instance of the generic type:
pair = MyGenericPair[int, int](1, 2)
assert isinstance(pair, MyGenericPair)
Only an instance of a record with the exact field types is considered an instance of a concrete Record
type:
pair = MyPair(1, 2)
assert isinstance(pair, MyPair[int, int])
assert not isinstance(pair, MyPair[int, Array[int, 2]])
Transient Types¶
In addition to the core types, the following transient types are available. There are some restrictions on how they can be used:
- They cannot be used as type arguments:
# Not ok: Array[str, 3]
- They cannot be used as a field types:
# Not ok: class MyRecord(Record): field: str # Not ok: class MyArchetype(PlayArchetype): field: str = imported()
tuple¶
The built-in tuple
type can be declared and destructured as usual:
t = (1, (2, 3))
a, (b, c) = t
Tuples may be indexed, but the given index must be a compile-time constant:
t = (1, 2, 3)
# Ok
debug_log(t[0])
# Not ok:
debug_log(t[random_integer(0, 2)])
They may also be created as an *args argument to a function and unpacked as an argument to a function:
def f1(a, b, c):
return a + b + c
def f2(*args):
return f1(*args)
Iterating over a tuple is also supported, but they are expanded at compile time, so iterating over large tuples may significantly increase the size of the compiled engine and slow down compilation:
t = (1, 2, 3)
for x in t:
debug_log(x)
dict¶
Dicts can be created by the **kwargs syntax and unpacked as arguments to a function:
def f1(a, b):
return a + b
def f2(**kwargs):
return f1(**kwargs)
str¶
Strings can be created and compared for equality and inequality:
s1 = 'abc'
s2 = 'def'
assert s1 == 'abc'
assert s1 != s2
Special Constants¶
The built-in None
, Ellipsis
, and NotImplemented
constants are supported.
None
is the only supported right-side operand for the is
and is not
operators.
a = None
b = 1
# Ok
a is None
b is not None
# Not ok:
b is b
Other types¶
Classes themselves are considered instances of type
. They may be used as arguments to functions, but annotating
a record field as type
or declaring an array with element type type
is not supported.
Functions or methods may be used as arguments to functions, but annotating a record field or setting
an array element type to Callable
is not supported.
Storing Instances of Transient Types in Records¶
Warning
The following is advanced usage and is unnecessary for most use cases.
While transient types cannot be used as type parameters or as a Record field's type, it is possible to store them in a generic record in a field annotated by a type parameter. Type arguments must not be explicitly provided when doing so. If multiple fields are annotated by the same type parameter, all such fields may be required to hold the exact same value in some cases.
For example, a version of the filter
function can be implemented as follows (see Iterables
for more information on iterators):
class _FilteringIterator[T, Fn](Record, SonolusIterator):
fn: Fn
iterator: T
def has_next(self) -> bool:
while self.iterator.has_next():
if self.fn(self.iterator.get()):
return True
self.iterator.advance()
return False
def get(self) -> Any:
return self.iterator.get()
def advance(self):
self.iterator.advance()
def my_filter[T, Fn](iterable: T, fn: Fn) -> T:
return _FilteringIterator(fn, iterable.__iter__())