9Classes
Classes are heap-allocated reference types with single inheritance, virtual dispatch, and constructor/destructor support. They cover the design space that genuinely benefits from runtime polymorphism (interpreters with heterogeneous AST nodes, GUI frameworks, plugin systems), while structs remain the default for everything else.
9.1 Declaration
A class is declared with type class. Members live inside visibility blocks (public:, private:, protected:).
type class Person {
public:
name: string;
age: i32;
Person(_name: string, _age: i32) {
this.name = _name;
this.age = _age;
}
greet(&this) -> void {
printf("Hi, I'm %s, age %d\n", this.name, this.age);
}
}
Class instances are always allocated on the heap with new, and the result is a pointer:
const p: Person* = new Person("Alice", 30);
p.greet();
9.2 Constructors and Destructors
Constructors share the class name. They are real language constructs that new invokes, not a naming convention as on structs.
A destructor is prefixed with ~, takes no parameters, and runs when the instance is deallocated. It is the right place to release resources acquired in the constructor (heap memory, file handles, sockets), following the RAII pattern familiar from C++.
type class Buffer {
public:
data: u8*;
size: u64;
Buffer(_size: u64) {
this.data = malloc(_size);
this.size = _size;
}
~Buffer() -> void {
free(this.data);
}
}
9.3 Inheritance
A class may extend exactly one base class. The derived constructor must chain to the base constructor with : Base(args):
type class Animal {
public:
kind: string;
Animal(_kind: string) { this.kind = _kind; }
describe(&this) -> void {
printf("Animal: %s\n", this.kind);
}
}
type class Dog : Animal {
public:
name: string;
Dog(_name: string) : Animal("Dog") {
this.name = _name;
}
bark(&this) -> void {
printf("%s says: Woof!\n", this.name);
}
}
Single inheritance avoids the diamond problem and the complexity of multiple inheritance. Composition handles the cases multiple inheritance is sometimes used for.
9.4 Virtual Methods and Override
virtual marks a method whose dispatch is resolved via a vtable at runtime. override is required on a derived method that replaces a base virtual; there is no implicit override.
type class Shape {
public:
Shape() {}
virtual area(&this) -> f64;
virtual name(&this) -> string {
return "Shape";
}
}
type class Circle : Shape {
public:
radius: f64;
Circle(r: f64) : Shape() {
this.radius = r;
}
override area(&this) -> f64 {
return 3.14159 * this.radius * this.radius;
}
override name(&this) -> string {
return "Circle";
}
}
A virtual method without a body declares an interface point that derived classes are expected to implement; a virtual method with a body provides a default that derived classes may override.
9.5 Polymorphic Dispatch
Code written against a base-class pointer dispatches automatically to the actual derived implementation:
function print_area(shape: Shape*) -> void {
printf("%s: area = %f\n", shape.name(), shape.area());
}
function main() -> i32 {
const c: Circle* = new Circle(10.0);
print_area(c); // Circle::name() and Circle::area()
return 0;
}
9.6 Structs vs. Classes
| Struct | Class | |
|---|---|---|
| Allocation | Stack (value type) | Heap via new (reference type) |
| Inheritance | No | Single inheritance |
| Virtual dispatch | No | virtual / override |
| Receivers | &this / mut &this | &this / mut &this |
| Use when | Plain data, generics, hot paths | Polymorphism, object hierarchies |
Default to structs. Reach for a class only when you need inheritance and virtual dispatch.