Memory Ownership
How Walrus manages memory through ownership, moves, and borrows.
Walrus uses a deterministic ownership model designed for predictable performance in gameplay scripting. Instead of a "one-size-fits-all" garbage collector, Walrus distinguishes between values that are cheap to copy and those that should be moved.
Value Kinds
Every type in Walrus falls into one of two categories: Copy Types or Move Types.
Copy Types (Cheap)
These types are small and passed by value. Assigning them or passing them to a function creates a bitwise copy.
int,float,boolenumvaluesfn(Function pointers)shared<T>(Shared pointers)handle<T>(Native handles)ref Tandmut ref T(Borrows)
let a = 42
let b = a # b is a copy of a
# Both a and b are validMove Types (Heavy)
These types are heap-allocated or own resources. To avoid expensive hidden copies, Walrus moves them by default.
string- Arrays (
[T]) - Objects (
{ ... }) - Class instances
let text = "walrus"
let moved = text # ownership moves to 'moved'
# print(text) # ERROR: use of moved value 'text'Moves and Consuming
A "move" happens when you transfer ownership. Once moved, the source local is considered uninitialized and cannot be used again unless it is re-assigned (if it was declared with var).
Ownership is consumed when:
- Assigning to another variable.
- Passing as an argument to a function that expects an owned type.
- Returning the value from a function.
- Inserting into a container (Array, Object, or Class).
- Capturing in an escaping closure.
fn take_data(data: [int]):
# ... data is owned here
let scores = [10, 20]
take_data(scores) # scores moved
# print(scores[0]) # ERRORBorrowing (ref and mut ref)
Borrowing allows you to grant a function or a block temporary access to a value without moving it.
Read-Only Borrows (ref)
A ref provides immutable access. You can have as many simultaneous read-only borrows as you like.
fn print_stats(s: ref string):
print(s)
let name = "Walrus"
print_stats(name) # Borrowed
print(name) # Still valid!Mutable Borrows (mut ref)
A mut ref provides exclusive read-write access.
fn clear(data: mut ref [int]):
# modify data...
let scores = [1, 2, 3]
clear(scores) # Exclusively borrowedThe Rules of Borrowing (v1)
- Named Locals Only: Borrows must target a named local variable. You cannot borrow a temporary expression result directly in v1.
- Exclusivity: You cannot create a
mut refif any other borrow (read or write) exists for that value. - No Storage: Borrowed references cannot be stored in arrays, objects, or classes. They are strictly lexical.
- No Escaping: You cannot return a
reffrom a function or capture it in an escaping closure.
Closures and Captures
Walrus closures handle captures based on the type:
- Copy Types are captured by copy.
- Move Types are captured by move. This is why escaping closures are a "consuming" use.
- Borrows can only be captured by non-escaping (local) functions. An escaping closure will report an error if it tries to capture a
ref.
fn make_printer() -> fn():
let text = "hello"
fn print_it():
print(text) # 'text' is MOVED into the closure
return print_itShared Ownership (shared<T>)
When you need multiple parts of your script to truly own the same object (like in a UI graph), use shared<T>.
class Node:
var parent: shared<Node>
let root = shared<Node> { ... }
let child = root # Shared ownership, no moveshared<T> is a copy-type that manages a reference count internally.
Native Handles (handle<T>)
Native handles represent objects owned by the C++ host engine.
- They are Copy Types.
- Walrus validates the handle on every access.
- If the C++ engine deletes the underlying object, Walrus throws a safe
RUNTIME_ERRORinstead of crashing.
Ownership Checklist
| If the value is... | Assignment / Passing | Original Variable |
|---|---|---|
| Copy Type | Copy | Still Valid |
| Move Type | Move | Invalid (Moved) |
Borrow (ref) | Copy (the reference) | Still Valid |
Shared (shared) | Copy (increment refcount) | Still Valid |
Handle (handle) | Copy (the handle) | Still Valid |