Classes

Classes

Classes define custom types with fields and methods.

Defining a Class

class Point {
    var x: i32;
    var y: i32;
 
    function init(px: i32, py: i32) {
        this.x = px;
        this.y = py;
    }
 
    function 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: A method named init is called automatically during instantiation
  • Methods: Functions inside the class that 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

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
        function init(owner: i32) {             // private constructor ...
            this.owner = owner; this.balance = 0;
        }
        public function open(owner: i32) Account {   // ... with a public factory
            return Account(owner);
        }
        public function deposit(n: i64) void { this.balance = this.balance + n; }
        function audit() bool { return this.balance >= 0; }   // private
    }
}
  • deinit is invoked by the compiler and is always callable.
  • init follows the normal rules, so a private init plus a public factory is a supported pattern.
  • 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;
 
    function 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;
    function init(x: i32, y: i32) { this.x = x; this.y = y; }
}
 
partial class Point {
    function sum() i32 { return this.x + this.y; }
}

Implementing Interfaces

Classes can implement interfaces using the implements keyword:

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

Implementing Multiple Interfaces

Separate multiple interfaces with commas:

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

Implementing Generic Interfaces

Generic classes can implement generic interfaces with matching type parameters:

using sun;
 
class Box<T> implements IIterator<T, Box<T>> {
    var value: T;
    var returned: bool;
 
    function init(v: T) {
        this.value = v;
        this.returned = false;
    }
 
    function 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.

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
    _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 calls through the value mutate the original object. See Bound Methods for the full rules.

var c = Counter();
var tick = c.increment;   // type: () void
tick();                   // increments c

Const Methods

A method that does not change its object is declared const function. The modifier sits where public does (public const function). 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;
    function init() { this.n = 0; }
    public const function get() i32 { return this.n; }
    public function 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 (function get(i) ref T, function first() Option<ref T>). Seen through a constant receiver every ref in the result becomes const refconst 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 function 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;
 
    function init(start: i32) {
        this.value = start;
    }
 
    function increment() void {
        this.value = this.value + 1;
    }
 
    function 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
}