Memory Model
Reference-by-Default Assignment
Assigning a struct-typed variable to another variable copies the heap pointer, creating a reference (alias) to the same object:
type Point export var x as int export var y as intend 'Point'
var a = Point{x: 1, y: 2}var b = a // b is an alias for a -- both point to the same objectb.x = 99print("{a.x}") // 99 -- a and b share the same objectField mutation through an alias affects the original, because both variables point to the same heap-allocated object. Reassignment, however, rebinds the variable to a new object without affecting the original:
var a = Point{x: 1, y: 2}var b = a // aliasb = Point{x: 5, y: 6} // rebinds b to a new object -- a is unaffectedprint("{a.x}") // 1 -- a still points to the originalAll types in Maxon use reference semantics on assignment — the variable is rebound to a new value only when reassigned with an expression. The practical distinction is that struct field mutations are visible through aliases, whereas arithmetic on ranged integers and primitives always produces a new value and rebinds the variable, leaving the original unchanged.
Stack Promotion
The compiler performs escape analysis to identify struct literals that can be safely stack-allocated instead of heap-allocated. A struct is promoted to the stack when all of the following are true:
- All fields are primitive types (no heap-allocated field types)
- Neither the variable nor any alias escapes the function (not returned, stored into a heap field, captured by a closure, or passed to a function that escapes it)
- The
@heapdirective is not used
Stack-promoted structs are freed automatically when the stack frame is reclaimed — no reference counting overhead is incurred. This optimization is transparent and preserves the same semantics as heap allocation.
The @heap annotation forces a struct to be heap-allocated, bypassing stack promotion:
@heap var p = Point{x: 1, y: 2} // always heap-allocated@heap is only valid on var or let declarations with struct literal initializers.
Explicit Cloning
To create an independent deep copy of a struct, use the .clone() method:
var a = Point{x: 1, y: 2}var b = a.clone() // deep copy -- b is independent of ab.x = 99print("{a.x}") // 1 -- a is unchangedThe type must implement the Cloneable interface to use .clone(). See Cloneable Interface below.
Cloneable Interface
The Cloneable interface is defined in the standard library:
interface Cloneable function clone() returns Selfend 'Cloneable'Auto-conformance: The compiler automatically generates Cloneable conformance for any struct whose fields are all Cloneable types. You do not need to write the conformance manually unless you need custom clone behavior.
Built-in Cloneable types:
- All primitives (
int,float,bool,byte) StringArray(when the element type is Cloneable)
When auto-conformance fails: If a struct contains a field whose type is not Cloneable (such as an enum with associated values), the compiler will not auto-generate conformance. You must implement clone() manually or restructure the type.
Auto-Equatable
The compiler also auto-generates Equatable conformance for structs whose fields all implement Equatable. The synthesized equals() method compares each field using == (for primitives) or .equals() (for nested structs).
type Point export var x as int export var y as intend 'Point'
// Point auto-conforms to Equatable (all fields are primitive)var a = Point{x: 1, y: 2}var b = Point{x: 1, y: 2}if a == b 'equal' // true -- content equality print("equal")end 'equal'If a struct contains a field that doesn’t implement Equatable (such as a function type), using == produces error E3069.
To compare reference identity (whether two variables point to the same heap object), use the is operator:
var a = Point{x: 1, y: 2}var b = a.clone() // deep copyvar c = a // alias (reference to same object)a is b // false -- different objectsa is c // true -- same objectScope Cleanup
When a struct variable goes out of scope, the compiler automatically releases its heap allocation. The runtime uses reference counting: each heap allocation has a refcount header. When a reference is created (via assignment), the refcount is incremented. When a variable goes out of scope, the runtime decrements the refcount and frees the memory if it reaches zero.
function compute() returns int var a = Point{x: 10, y: 20} // allocated on heap, refcount = 1 var b = Point{x: 30, y: 40} // allocated on heap, refcount = 1 return a.x + b.y // a and b released here (refcount -> 0 -> freed)end 'compute'Return values: When a struct is returned from a function, the returned variable is not released at scope exit — the caller takes responsibility for its lifetime.
function makePoint() returns Point var p = Point{x: 1, y: 2} return p // p is NOT freed; caller is responsibleend 'makePoint'Container cleanup: Containers with heap-allocated elements (e.g., List with MyStruct) perform deep cleanup when freed. Each element’s refcount is decremented, and elements whose refcount reaches zero are freed recursively. For List, the compiler walks all managed list nodes and decrefs their stored values before freeing the managed list itself.
typealias TokenList = List with Token
function example() returns int var list = TokenList.create() list.append(Token{id: 1}) // Token incref'd by the managed list node list.append(Token{id: 2}) // Token incref'd by the managed list node return 0 // list freed: each Token decref'd (rc→0→freed), // then managed list nodes freed, then managed list freedend 'example'Stack Allocation
- Local variables (
var,let) - Function parameters
- Automatic lifetime (scope-based)
Heap Allocation
- Arrays (all array types)
- Strings (when dynamically allocated)
- Automatically freed at end of scope
- No manual
freeor garbage collector needed
Borrow Checking
The borrow checker prevents mutation of a collection while references to its elements are alive. When you obtain a reference from a mutable source (e.g., var s = try arr.get(0) otherwise ""), that source cannot be mutated until the reference is no longer used.
Borrows use non-lexical lifetimes (NLL): a borrow ends at the last use of the borrowing variable, not at the end of its scope.
var arr = ["hello"]var s = try arr.get(0) otherwise ""arr.push("world") // ERROR E3070: cannot mutate 'arr' via 'push' while it is borrowed by 's'var arr = ["hello"]var s = try arr.get(0) otherwise ""print("{s}\n") // last use of s — borrow expires herearr.push("world") // OK: borrow has expiredThe borrow checker also detects indirect mutation through helper functions:
function clearList(list StringList) list.clear()end 'clearList'
var val = try list.first() otherwise "none"clearList(list) // ERROR E3070: cannot mutate 'list' via 'clearList' while it is borrowed by 'val'Safety
- No bounds checking on arrays
- No null checks
- Use-after-move prevented at compile time (see Ownership System above)
- Mutation of borrowed collections prevented at compile time (see Borrow Checking above)
Calling Convention
- Parameters that are only read are passed by value (simple types in registers, arrays as pointer)
- Parameters that are assigned to inside the callee are passed by reference (pointer to caller’s storage)
- Return values passed by value (in register or stack)
Code Generation
Native x86-64 Backend
- Maxon uses a custom x86-64 backend (no LLVM dependency)
- Generates native Windows PE executables directly
Optimizations
- Constant folding
- Dead code elimination
Runtime Library
- Provides implementations for intrinsic functions
- Auto-linked with all programs
- No C runtime dependency