Skip to content

Overview

Sonolus.py is a Python library for creating Sonolus engines. This page provides an overview of the key functionality available in the library. For detailed information, see the Concepts and Reference sections.

Language

Sonolus.py compiles Python code into Sonolus nodes. It supports a subset of Python including most syntax and a portion of the standard library. Additionally, Sonolus.py provides its own library of types and functions that are designed for use in Sonolus engines.

Syntax

Most Python syntax is supported, but there are a few limitations. The primary restrictions are:

  • Destructuring assignment with the * operator is unsupported.
  • Sequence (list and array) match patterns with the * operator are unsupported.
  • Mapping (dict) match patterns are unsupported.
  • Within functions, import statements are unsupported.
  • The global and nonlocal keywords are unsupported.
  • Exception related statements (try, except, finally, raise) are unsupported.

Compile Time Evaluation

Sonolus.py will evaluate some expressions at compile time such as basic arithmetic operations on constants, boolean logical operations (and, or, not) on constants, and type checks (isinstance, issubclass).

In control flow constructs like if and match, Sonolus.py may determine some branches to be unreachable at compile and eliminate them without evaluating them. This allows code like the following to compile successfully:

a = 1
if isinstance(a, Vec2):
    # This branch is eliminated at compile time.
    # If it were not, compilation would fail because `a` has no attribute `x`.
    debug_log(a.x)
else:
    debug_log(a)

Variables

Numeric (int, float, bool) variables are fully supported and can be freely assigned and modified.

All other variables have the restriction that if the compiler finds multiple possible values for a variable, it may not be accessed. For example, the following code will not compile:

if random() < 0.5:
    a = Vec2(1, 2)
else:
    a = Vec2(3, 4)
# This will not compile because `a` could have been defined in either branch.
debug_log(a.x)

Function Returns

Similar to variables, functions returning int, float, or bool can have any number of return statements. Functions returning None may also have any number of return or return None statements.

Functions returning any other type must have exactly one return statement, and it must be the only exit point of the function 1. It is ok, however, for a function to have other return statements that are eliminated at compile time. For example, the following code will compile successfully:

def fn(a: int | Vec2):
    if isinstance(a, Vec2):
        return Vec2(a.x, a.y)
    else:
        return Vec2(a, a)

fn(123)

Types

Numbers

Sonolus.py supports int, float, and bool types and most of the standard operations such as mathematical operations (+, -, *, /, //, %), comparisons (<, <=, >, >=, ==, !=), and boolean operations (and, or, not).

Record

Record is the main way to define custom types in Sonolus.py. It functions similarly to a data class and provides a way to define a type with named fields:

class MyRecord(Record):
    a: int
    b: float

record_1 = MyRecord(1, b=2.3)

Records may also be generic:

class MyGenericRecord[T](Record):
    value: T

record_1 = MyGenericRecord[int](123)
record_2 = MyGenericRecord(MyRecord(4, 5.6))  # Type arguments are inferred

Record arguments are retained by reference, so modifying the original record will also modify the record in the array:

record_1 = MyRecord(1, 2.3)
record_2 = MyGenericRecord(record_1)
record_2.value.a = 789  # This also affects `record_1` since they're the same object.
assert record_1.a == record_2.value.a == 789

Array

Array is a type that represents a fixed-size array of elements of a specific type:

array_1 = Array[int, 3](1, 2, 3)
array_2 = Array(4, 5, 6)  # Type arguments are inferred

When given record or array values as arguments, the array constructor will copy them:

record_1 = MyRecord(1, 2.5)
array_1 = Array(record_1)
array_1[0].a = 789  # This has no effect on `record_1` since it was copied.
assert record_1.a == 1

Operations

This section is an overview of the operations available for records and arrays. For full details see the Record documentation and Array documentation.

Records and arrays come with the == and != operators predefined to compare their values for equality:

assert MyRecord(1, 2.3) == MyRecord(1, 2.3)
assert Array(1, 2, 3) != Array(4, 5, 6)

The unary + operator makes a copy of a record or array, creating a new instance with the same values:

record_2 = +record_1
array_2 = +array_1

Similarly, a new zero initialized value can be created using the unary + operator on a record or array type:

record_1 = +MyRecord
record_2 = +Array[int, 3]

Records and arrays can be mutated in-place using the @= operator:

record_1 @= MyRecord(1, 2.3)
array_1 @= Array(4, 5, 6)

Record fields and array elements of numeric types can be set using the = operator:

record_1.a = 123
array_1[1] = 456

Setting a record field that's a record or array using the = operator will modify the field in-place:

record_1 = MyRecord(1, 2.3)
record_2 = MyGenericRecord(record_1)
record_2.value = MyRecord(4, 5.6)  # This modifies `record_1` in-place.
assert record_1 == record_2.value == MyRecord(4, 5.6)

Setting an array element that's a record or array using the = operator will also modify the element in-place:

array_1 = Array(MyRecord(1, 2.3))
record_1 = array_1[0]
array_1[0] = MyRecord(4, 5.6)  # This modifies `record_1` in-place.
assert record_1 == array_1[0] == MyRecord(4, 5.6)

Other Types

Sonolus.py has limited support for other types of values such as strings, tuples, and functions. These have restrictions such as not being valid as Record field types or Array element types.

Modules

Sonolus.py provides a number of built-in modules that can be used in Sonolus engines. These include:

  • Project
    • Project: Configuration for a Sonolus.py project.
    • Engine: Configuration for a Sonolus.py engine.
    • Level: Configuration for a Sonolus.py level.
    • Archetype: Engine archetypes and their configuration.
  • Core Types
    • Array: Fixed-size arrays.
    • Num: Numeric values (int, float, bool).
    • Record: User-defined types with named fields.
  • Engine Resources
  • Sonolus Runtime
    • Globals: Level data and level memory definition.
    • Runtime: Runtime functions like time and ui configuration.
    • Stream: Data streams recorded in play mode and used in watch mode.
    • Text: Standard Sonolus text constants.
    • Timing: Beat and timescale related functions.
  • Python Builtins
    • builtins: Supported Python builtins.
    • math: Supported math functions.
    • random: Supported random functions.
    • typing: Supported typing functions.
  • Utilities
    • ArrayLike: Mixin for array functionality.
    • Containers: Additional container types like VarArray and ArrayMap.
    • Debug: Debugging utilities.
    • Easing: Easing functions for animations.
    • Interval: Mathematical intervals.
    • Iterator: Iterators over collections.
    • Maybe: Optional function return values.
    • Printing: Preview mode number printing.
    • Quad: Quadrilaterals.
    • Transform: Transformations like translation, rotation, and scaling.
    • Values: Generic utilities for working with values.
    • Vec: The Vec2 type and related functions.

For more details, see the Reference section.


  1. The Maybe type is an exception to this rule. See the Maybe documentation for details.