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
initis 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
}
}deinitis invoked by the compiler and is always callable.initfollows the normal rules, so a privateinitplus 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:
| Declaration | magic | length | flags | _sizeof<T>() |
|---|---|---|---|---|
class Header | 0 | 4 | 8 | 12 |
packed_class Header | 0 | 1 | 5 | 6 |
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 borrowField 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_classcannot be combined withpartial, 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 freedComparison
| Allocation | Syntax | Type | Memory | Cleanup |
|---|---|---|---|---|
| Stack | Point(...) | Point | Stack | Automatic (scope exit) |
| Heap (manual) | allocator.create<Point>(...) | raw_ptr<Point> | Heap | Manual (_free) |
| Heap (auto) | Unique<Point>(allocator.create<Point>(...)) | Unique<Point> | Heap | Automatic (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 cConst 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 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 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
}