Ownership & Borrowing
Ownership rules
- Every managed value (string, struct with string fields) has exactly one owner
- When the owner goes out of scope, the value is cleaned up
- Assignment produces a deep copy (for dynamic strings)
What is managed
The compiler tracks ownership for:
stringvalues (24-byte descriptor: ptr + len + cap)- Structs that contain
stringfields
Raw pointers, integers, slices, and other types are unmanaged — no automatic cleanup.
Scope-based cleanup
fn process() {
let greeting: string = "Hello, " + name;
let data: string = read_file("input.txt");
// both are freed automatically when this scope exits
}The compiler emits string_drop in reverse declaration order at every scope exit point.
Parameters are borrows
Function parameters are borrowed — the callee does not free them:
fn print_length(s: string) {
io.print_i64(str.len(s));
// s is NOT freed here — caller still owns it
}
let name: string = "hello";
print_length(name);
// name is still valid hereReturn transfers ownership
Returning a managed local transfers ownership to the caller (the local’s drop is skipped):
fn make_greeting(name: string) -> string {
let result: string = "Hello, " + name;
return result; // no drop — ownership moves to caller
}Copy on assignment
Assigning a managed variable to another produces a deep copy:
let a: string = build_name();
let b: string = a; // b is an independent deep copyStatic strings (literals, cap == 0) are shallow-copied. Dynamic strings (cap > 0) are heap-duplicated.
Reassignment drops the old value
When reassigning a managed variable, the old value is dropped before the new one is stored:
var s: string = "first";
s = "second"; // "first" is freed, then "second" is assignedStruct fields
Structs with string fields are managed as a whole. When the struct goes out of scope, each string field is dropped:
struct Person {
name: string;
email: string;
}
let p = Person { name: "Alice"; email: "alice@example.com"; };
// when p goes out of scope, both name and email are freedSlices (non-owning views)
Slices borrow data without ownership — no cleanup runs for them:
fn first_word(s: []const u8) -> []const u8 {
for (i in 0..s.len) {
if (s[i] == 32 as u8) { return s[0..i]; }
}
return s;
}The caller must ensure the source data outlives the slice.
Raw pointers
Raw pointers opt out of automatic cleanup entirely:
let buf: *u8 = mem.alloc(4096);
defer mem.free(buf, 4096);
// you are responsible for cleanupLast modified: