Classes

Classes

Classes define custom types with fields and methods.

Defining a Class

class Point {
    var x: i32;
    var y: i32;
 
    init(px: i32, py: i32) {
        this.x = px;
        this.y = py;
    }
 
    method magnitude_squared() i32 {
        return this.x * this.x + this.y * this.y;
    }
}

Key Features

  • Fields: Declared with var name: type; inside the class body
  • Constructor: init(args) { } runs during instantiation. It is written bare — no public, no method, no return type — and so is the destructor deinit() { }; both are always public, and no method may take their names. A constructor that returns errors is written init(args) Result<void> { }; calling the class then returns Result<Class>. Library-defined result enums use their first type parameter for the constructed value. Use try Class(args) to propagate failure. See returned errors.
  • Methods: Declared with the method keyword inside the class body; a method can access this
  • Member access: Use . to access fields and call methods
  • Interfaces: Classes can implement one or more interfaces
  • Visibility: Fields and methods are private by default; mark the API public

Field Initializers

Fields can supply values that run before the constructor body:

class Point {
    var x: i32 = 10;
    var y: i32 = this.x + 2;
 
    init() {}
    init(x: i32) { this.x = x; }
}

Initializers run in field declaration order on every constructor call. They can use names from the class's definition scope, class type parameters, and this. Constructor parameters and locals are not visible, even when they share a name with an outer variable. Reading a field before it has been initialized, or passing an incomplete this elsewhere, remains an error.

Defaults always run, including when the constructor subsequently replaces a field. Replacing an owning value drops the previous value normally; defaults do not introduce implicit copies.

When every field has a default and there is no constructor, Sun supplies init(). If only some fields have defaults, write a constructor that initializes the rest, including fields inherited from interfaces. A default that propagates an error uses try and requires an explicit constructor returning a compatible result enum; the supplied constructor is infallible.

Field initializers also work in packed classes. Fields still require explicit types. Struct literals must name every field and do not run these defaults. They remain available when Sun supplies the constructor; a class with an explicit init must still be constructed through it.

Visibility

Class members are private by default; public is the only modifier. Privacy is module-scoped: a private member is accessible from any code in the module that defines the class (see Modules → Visibility), and from nowhere outside it.

public module bank {
    public class Account {
        var balance: i64;                       // private
        public var owner: i32;                  // public field
        init(owner: i32) {                      // always public
            this.owner = owner; this.balance = 0;
        }
        public method deposit(n: i64) void { this.balance = this.balance + n; }
        method audit() bool { return this.balance >= 0; }   // private
    }
}
  • init and deinit take no visibility keyword: constructors are always public, and deinit is invoked by the compiler and is always callable.
  • To restrict who can build a class, give init a parameter whose type is a module-private class. Code outside the module cannot name that class, so it cannot produce the argument — the standard library's SharedGuard uses this to ensure only lock() hands out guards.
  • Operator methods (__index__, __setindex__, __slice__) follow the normal rules: v[i] from outside the module needs a public __index__.
  • Struct literals { field: value } name fields directly, so every field written must be accessible from the literal's location.
  • A method that implements a public interface member must itself be public.

Packed Classes

By default the compiler inserts padding between fields so each one lands on its natural alignment. Declare a class with packed_class when the layout itself is the contract — binary file headers, wire protocols, hardware registers, or FFI against a C struct declared __attribute__((packed)):

packed_class Header {
    var magic: u8;
    var length: i32;
    var flags: u8;
 
    init() {}
}

Fields are laid out end to end with no padding, and the whole class has alignment 1:

Declarationmagiclengthflags_sizeof<T>()
class Header04812
packed_class Header0156

Everything else about a packed class is unchanged — constructors, methods, generics, and implements all work as usual, and the whole object can still be passed by ref.

Restrictions

Because a packed field sits at an arbitrary offset, its address cannot be handed out. Borrowing one is a compile error, the same restriction C++ places on references to packed members:

var h: Header = Header();
 
ref r = h.length;   // ERROR: not aligned enough to be borrowed
takes_a_ref(h.length);  // ERROR: same reason
 
var len: i32 = h.length;  // OK: copy the value out
takes_a_ref(h);           // OK: the object itself is fine to borrow

Field types are restricted to what can actually be packed:

  • Arrays and interface values are rejected — both are multi-word fat pointers. Use raw_ptr<T> instead.
  • Class-typed fields must themselves be packed_class, otherwise the nested class's interior padding would survive and defeat the purpose.
  • packed_class cannot be combined with partial, which declares methods only and so has no layout.

Partial Classes

A partial class adds methods to a class declared elsewhere in the program. It cannot declare fields or init, and cannot redefine a method the class already has. Methods on either side can call each other through this.

class Point {
    var x: i32;
    var y: i32;
    init(x: i32, y: i32) { this.x = x; this.y = y; }
}
 
partial class Point {
    method sum() i32 { return this.x + this.y; }
}

Implementing Interfaces

Classes can implement interfaces using the implements keyword:

interface Printable {
    method print() void;
}
 
class Point implements Printable {
    var x: i32;
    var y: i32;
 
    init(px: i32, py: i32) {
        this.x = px;
        this.y = py;
    }
 
    method print() void {
        println(this.x);
        println(this.y);
    }
}

Implementing Multiple Interfaces

Separate multiple interfaces with commas:

interface Named {
    var name: i32;
}
 
interface Runnable {
    method run() void;
}
 
class Task implements Named, Runnable {
    // 'name' field comes from Named interface
    
    init(n: i32) {
        this.name = n;
    }
 
    method run() void {
        println(this.name);
    }
}

Implementing Generic Interfaces

Generic classes can implement generic interfaces with matching type parameters:

using std;
 
class Box<T> implements IIterator<T, Box<T>> {
    var value: T;
    var returned: bool;
 
    init(v: T) {
        this.value = v;
        this.returned = false;
    }
 
    method next(self: ref Box<T>) Option<T> {
        if (this.returned) {
            return Option.None;
        }
        this.returned = true;
        return Option.Some(this.value);
    }
}

See Interfaces for more details on interface definitions.

Constructors

A constructor runs in two phases. The first lasts until every field has a value. All the body may do there is give the fields their values — reading a field that has none yet, or handing this to anything else, is rejected, because the object is not a whole value yet. The second phase begins once every field is assigned, and the body may do anything a method can.

class Account {
    var owner: i32;
    var balance: i64;
 
    init(owner: i32) {
        this.owner = owner;
        this.balance = 0;
        this.audit();       // fine: every field has a value by here
    }
 
    method audit() void { ... }
}

A constructor must leave every field assigned, on every path out of it — the same rule a struct literal follows by having to name every field. A field left out would silently be zero, and that is exactly the bug the rule exists to prevent:

class Broken {
    var owner: i32;
    var balance: i64;
 
    init(owner: i32) {
        this.owner = owner;
    }   // ❌ ERROR: constructor of 'Broken' can finish with field 'balance'
        //    unassigned
}

A constructor may hand the work to the object's own methods, and what they assign counts towards the obligation. Such a method may only read fields that already have a value:

class Buffer {
    var data: Vec<u8>;
    var length: i64;
 
    init(alloc: ref HeapAllocator) {
        this.fill(alloc);   // assigns both fields, so the object is whole
    }
 
    method fill(alloc: ref HeapAllocator) void {
        this.data = Vec<u8>(alloc, 16);
        this.length = 0;
    }
}

Creating Instances

Stack Allocation (Value Types)

Classes are value types in Sun. Create instances by calling the class name with constructor arguments:

function main() i32 {
    var p = Point(3, 4);  // Create a Point on the stack
    return p.magnitude_squared();  // 25
}

With explicit type annotation:

function main() i32 {
    var p: Point = Point(3, 4);
    return p.x + p.y;  // 7
}

The init method is called automatically when you create an instance. Arguments passed to ClassName(args...) are forwarded to the init method.

Struct Literals (Classes Without init)

A class that declares no init is constructed with a struct literal, naming every field:

class Car {
    var color: static_ptr<u8>;
    var speed: i32;
}
 
function main() i32 {
    var car: Car = { color: "red", speed: 120 };
    return car.speed;
}

The target type comes from the annotation, so var car = { ... } is an error — a literal has no type of its own.

Field order does not matter, and a class-typed field takes a nested literal:

class Inner { var a: i32; var b: i32; }
class Outer { var inner: Inner; var tag: i32; }
 
var o: Outer = { tag: 12, inner: { a: 10, b: 20 } };

Every field must be named. Leaving one out is an error rather than a silent zero — that silence is exactly the bug this syntax exists to prevent.

⚠️

Positional construction is not available for classes without init: Car("red", 120) is rejected. Field order is a layout detail, and a positional call would silently change meaning if two same-typed fields were ever reordered. Add an init if you want positional arguments — a class cannot use both forms.

Heap Allocation (Using Allocator)

For heap-allocated objects, use an allocator from the standard library:

import "stdlib/allocator.sun";
 
function main() i32 {
    var allocator = make_heap_allocator();
    
    // Create a Point on the heap
    var p: raw_ptr<Point> = allocator.create<Point>(3, 4);
    
    var result = p.magnitude_squared();  // 25
    
    // Manual cleanup required for raw_ptr
    unsafe { _free(p); };
    
    return result;
}

Automatic Cleanup with Unique

For automatic memory management, wrap the raw pointer in Unique<T>:

import "stdlib/allocator.sun";
import "stdlib/unique.sun";
 
function main() i32 {
    var allocator = make_heap_allocator();
    var p = Unique<Point>(allocator.create<Point>(3, 4));
    
    return p.get().magnitude_squared();  // 25
}  // p.deinit() called automatically, memory freed

Comparison

AllocationSyntaxTypeMemoryCleanup
StackPoint(...)PointStackAutomatic (scope exit)
Heap (manual)allocator.create<Point>(...)raw_ptr<Point>HeapManual (_free)
Heap (auto)Unique<Point>(allocator.create<Point>(...))Unique<Point>HeapAutomatic (deinit)

Passing Objects to Functions

By Value

By default, objects are passed by value (copied):

function modify_point(p: Point) void {
    p.x = 100;  // Modifies the copy
}
 
function main() i32 {
    var p = Point(1, 2);
    modify_point(p);
    return p.x;  // Still 1 - original unchanged
}

By Reference (Borrowing)

Use ref to pass a reference that allows reading without copying:

function read_point(p: ref Point) i32 {
    return p.x + p.y;
}
 
function main() i32 {
    var p = Point(3, 4);
    return read_point(p);  // 7
}

Methods as Values

A method accessed without parentheses (obj.method) is a first-class value of lambda type: it can be stored in a variable or passed as a callback. The receiver is captured by reference, so the value carries the <'_> type marker and calls through it mutate the original object. See Bound Methods for the full rules.

var c = Counter();
var tick = c.increment;   // type: <'_>() => void
tick();                   // increments c

Lifetime Parameters

A class whose fields hold <'_> lambdas or references may declare lifetime parameters — Rust-style leading-apostrophe names in the angle brackets — to say which frame those fields are tied to. Methods use the declared names in their signatures, and other signatures bind the class's slot with a type application:

class Bus<'a> {
    var cb: <'a>(i32) => i32;    // stored callbacks must outlive 'a
    public method subscribe(cb: <'a>(i32) => i32) void { this.cb = cb; }
}
 
class Node {
    /* a bus whose slot is bound to this node's lifetime */
    public method attach(bus: ref Bus<'this>) void {
        bus.subscribe(this.onMsg);
    }
}

Inside any class or interface member the builtin 'this names the receiver's lifetime without declaring anything — a field may even be typed <'this>(…) => …, meaning "whatever is stored here outlives this object". Lifetimes never change a class's layout, code, or generic instantiations: Bus<'a> is one class, and construction sites write plain Bus(). The checking rules live in Memory Safety.

Const Methods

A method that does not change its object is declared const method. The modifier sits where public does (public const method). Inside it, this is immutable: the body cannot assign to a field, take a ref to one, pass one to a ref parameter, or call a non-const method on this.

class Counter {
    var n: i32;
    init() { this.n = 0; }
    public const method get() i32 { return this.n; }
    public method increment() void { this.n = this.n + 1; }
}

Only const methods may be called on a constant receiver — a const variable, a const ref, or this inside another const method:

const c = Counter();
c.get();          // ✓ OK
c.increment();    // ❌ ERROR: cannot call non-const method 'increment' on constant 'c'

A const method may still hand out a borrow of its object (method get(i) ref T, method first() Option<ref T>). Seen through a constant receiver every ref in the result becomes const ref — const ref T, Option<const ref T> — so the element can be read but not changed, while a var receiver gets the writable borrow the signature declares. Inside the method the body is checked against that same read-only view, which is what lets it return a borrow of the immutable this. init is never const, and a bound method value (c.increment) follows the same rule as a call.

An interface method declared const method must be implemented by a const method; a class may mark further methods const on its own.

Returning Objects from Functions

Functions can return class instances by value:

function make_point(x: i32, y: i32) Point {
    return Point(x, y);
}
 
function main() i32 {
    var p = make_point(5, 6);
    return p.magnitude_squared();  // 61
}

Counter Example

class Counter {
    var value: i32;
 
    init(start: i32) {
        this.value = start;
    }
 
    method increment() void {
        this.value = this.value + 1;
    }
 
    method get() i32 {
        return this.value;
    }
}
 
function main() i32 {
    var c1 = Counter(10);
    c1.increment();
    c1.increment();
    var result1 = c1.get();  // 12
    
    var c2 = Counter(0);
    c2.increment();
    var result2 = c2.get();  // 1
    
    return result1 + result2;  // 13
}

Arithmetic operator hooks

Classes may define __add__ for + and __multiply__ for *. Each hook is an ordinary method taking one explicit operand. The left operand is its receiver; the right operand is its argument. Normal overload resolution, visibility, const, unsafe, borrowing, and move rules apply. Operands execute once, left to right. Primitive arithmetic and operator precedence are unchanged.

/** Stores an integer operand. */
class Number {
    var value: i32;
    /** Initializes the operand. */
    init(value: i32) { this.value = value; }
    /** Borrows the receiver and adds a scalar. */
    public const method __add__(other: i32) i32 { return this.value + other; }
    /** Borrows the receiver and multiplies by a scalar. */
    public const method __multiply__(other: i32) i32 { return this.value * other; }
}

Hooks may return owning values, references, or result enums. Result-returning operators require explicit handling, such as try (a * b) inside a compatible result-returning function. There is no automatic result unwrapping, reflected right-hand dispatch, or compound-assignment hook. Other arithmetic operators are not overloadable yet.