Skip to content

Structs


Structs (currently the only classes in SPy) are immutable data structures analogous to C structs.

Warning

Class construction and layout are a part of SPy that's rapidly evolving. All of the constructs, names, functions, or decorators here are likely to change!

Declaration

Structs are declared with the @struct decorator on a CPython-style class definition. Their fixed list of fields follows with type annotations. Note that default values for these fields are not currently supported.

@struct
class Person:
    name: str
    age: int

def main() -> None:
    p = Person('Alice', 99)
    print(p.name, "is", p.age, "years old") # Alice is 99 years old

Structs may also be defined with the generic class syntax, which is syntactic sugar for a generic function which defines an internal struct. See the generics documentation for more info.

Attributes

Structs are shallow immutable. Attributes cannot be set after creation, nor can new attributes be assigned to an instance of a struct class after creation:

@struct
class Person:
    name: str
    age: int

def main() -> None:
    p = Person('Alice', 99)
    p.name = 'Bob' # TypeError: type `Person` does not support assignment to attribute 'name'
    p.height = 190 # TypeError: type `Person` does not support assignment to attribute 'height'

However, objects which are attributes of struct classes may be mutated.

@struct
class Person:
    name: str
    age: int
    books: list[str]


def main() -> None:
    p = Person('Alice', 99, [])
    print(p.name, "has", len(p.books), "book(s)") # Alice has 0 book(s)

    p.books.append("An Introduction to Python")
    print(p.name, "has", len(p.books), "book(s)") # Alice has 1 book(s)

If the struct attribute is a pointer to an object, that object at that pointer may also be mutated. (See Constructors below for info on the __new__ method):

from unsafe import gc_ptr, gc_alloc

@struct
class Person:
    name: str
    age: gc_ptr[int]

    def __new__(name: str, age: int) -> Person:
        _age = gc_alloc[int](1)
        _age[0] = age
        return Person.__make__(name, _age)

    def do_birthday(self) -> None:
        self.age[0] = self.age[0] + 1


def main() -> None:
    p = Person('Alice', 99)
    print(p.name, "is", p.age[0], "years old") # Alice is 99 years old

    p.do_birthday()
    print(p.name, "is", p.age[0], "years old") # Alice is 100 years old

Pointers to Structs

If a struct is manually allocated using gc_alloc or similar, its attributes can be mutated. See the low level memory docuementary on heap-allocated structs for more info.

from unsafe import gc_alloc, gc_ptr

@struct
class Point:
    x: int

# spy build foo.spy
def main() -> None:
    p = gc_alloc[Point](1)
    p.x = 1
    print(p.x)
    p.x = 2
    print(p.x)

Constructors

Structs use a default constructor which populates all their attributes in-order, and all attributes must be provided each time the default constructor is called. E.g. the example above, the constructor p = Person('Alice', 99, []) requires an empty list to be passed for the books attribute. We can call this constructor explicitly using the __make__ method:

@struct
class Person:
    name: str
    age: int
    books: list[str]

def main() -> None:
    p = Person.__make__('Bob', 6, [])
    print(p.age) # 6

User-facing constructors can be customized by overwriting the __new__ method; the __make__ must be called within to handle the initialization of the complete struct. Note that, as in Python, __new__ functions like a staticmethod (it does not take a self parameter)

@struct
class Person:
    name: str
    age: int
    books: list[str]

    def __new__(name: str, age: int) -> Person:
        _books: list[str] = []
        return Person.__make__(name, age, _books)

def main() -> None:
    p = Person('Alice', 99)
    print(p.name, "has", len(p.books), "books") # Alice has 0 books

By convention, if a struct is simply being used to wrap a lower-level type (to add constructors, methods, etc), that lower level type is denoted as __ll__.

See the smallpoint example for a more in-depth example and explanation. See the implementations of the list, dict, and file objects in spy for usage of this pattern in the SPy standard library.

/// info Eventually, the __ll__ notation may likely be special-cased to make it a truly private attribute within the struct. Currently, it is just a naming convetion. ///

from unsafe import gc_ptr, gc_alloc

@struct
class BookData:
    name: str
    stars: int # out of 10

@struct
class Book:
    __ll__: gc_ptr[BookData]

    def __new__(name: str, stars: float) -> Book:
        data = gc_alloc[BookData](1)
        data.name = name
        data.stars = stars
        return Book.__make__(data)

    def plus_one_star(self) -> None:
        self.__ll__.stars = self.__ll__.stars + 1.0

    def __repr__(self) -> str:
        return "Book(name='" + self.__ll__.name + "', stars=" + str(self.__ll__.stars) + ")"


def main() -> None:
    b = Book("An Introduction to Python", 10.0)
    print(b) # Book(name='An Introduction to Python', stars=10.0)
    b.plus_one_star() # Book(name='An Introduction to Python', stars=11.0)
    print(b)

Methods

Structs may have methods defined inside their class body; the struct object itself is passed as the first parameter (usually called self), just as in CPython:

@struct
class Person:
    name: str

    def say_hi_to(self, other: Person) -> None:
        print("Hi " + other.name + "! My name is " + self.name)

    def is_teenager(self) -> bool:
        return 13 <= self.age and self.age <= 19

def main() -> None:
    c = Person("Charlie", 55)
    c.say_hi_to(Person("Donna", 57))     # Hi Donna! My name is Charlie
    print(c.is_teenager())               # False

Methods may also be blue functions, generic blue functions, or metafunctions.

Inheritance

Inheriting from a base class is not currently implemented in SPy.

Structs as Arguments

When used as a function argument, Structs are passed by value, meaning a copy of the struct is created for each call. The performance implications of this should be kept in mind for large structs.