Memory Overview
Current implementation
The compiler implements scope-based automatic cleanup for every managed value: strings, structs with managed fields, tagged unions with managed payloads, optionals and T or E results of those, fixed arrays of them, and the containers collections generates. All other heap memory is manual (mem.alloc / mem.free + defer).
Managed types
A value is managed when it owns heap memory:
string(a computed string owns its buffer; a literal owns nothing)- Structs with a managed field
- Tagged unions with a managed field in any variant
?Tof a managedT, andT or Ekept whole in a variable- Fixed arrays of managed values (
[4]string) - Generated containers (
Vec_string,Map_i64_Value, …)
Managed locals get automatic cleanup when their block ends.
Scope-based cleanup
fn process() {
let name = build_name(); // managed string
let data = read_input(); // managed string
// both are freed automatically at scope exit
}A scope is a block: a function body, an if branch, a loop body (every iteration), a match arm. The compiler drops at every exit point (end of block, return, break, continue, early return), in reverse declaration order. A value nobody keeps (a string built just for a call, an ignored result) is dropped by the compiler too.
Copy semantics
Assigning a managed value to a new variable produces a deep copy:
let a = "hello";
let b = a; // b is an independent copy (deep copy if dynamic)Static strings (string literals, cap == 0) are shallow-copied (just the pointer). Dynamic strings (built at runtime, cap > 0) are heap-duplicated. Structs, tagged unions and containers copy their managed parts the same way.
Move on return
When a function returns a managed local, the compiler skips the drop for that local — the value is “moved” to the caller:
fn make_greeting(name: string) -> string {
let s: string = "Hello, " + name;
return s; // no drop — ownership transfers to caller
}Returning anything else that is managed (a parameter, a field) returns a copy.
Manual allocation
For raw memory, use the mem module:
import mem from "mem";
let ptr: *u8 = mem.alloc_or_trap(1024); // or mem.alloc(1024) or return;
defer mem.free(ptr, 1024);Available functions:
| Function | Description |
|---|---|
mem.alloc(size: i64) -> *u8 or mem.MemError | Allocate bytes (OutOfMemory as an error) |
mem.alloc_or_trap(size: i64) -> *u8 | Allocate bytes (trap: out of memory when there are none) |
mem.realloc(p, old, new) -> *u8 or mem.MemError | Resize a block |
mem.free(ptr: *u8, size: i64) | Free allocation |
mem.set(dst, val, count) | Fill memory |
mem.copy(dst, src, count) | Copy bytes (non-overlapping) |
mem.move(dst, src, count) | Copy bytes (overlapping safe) |
mem.zero(dst, count) | Zero-fill |
mem.compare(a, b, count) -> i32 | Compare bytes |
mem.equal(a, b, count) -> bool | Equality check |
mem.live_allocs() -> i64 | Heap blocks currently allocated (strings included) |
Checking for leaks
mem.live_allocs() counts the heap blocks currently allocated, string buffers included, in the VM and natively alike. Compare it before and after a piece of code: if it grew, something leaked; if it shrank, something was freed twice.
let before: i64 = mem.live_allocs();
work();
if (mem.live_allocs() != before) { @trap("leak"); }Defer for cleanup
Use defer to schedule cleanup for manual resources:
var file: fs.File = fs.open("data.bin", fs.Access.Read, fs.Creation.Existing) or return;
defer file.close();
let buf: *u8 = mem.alloc_or_trap(4096);
defer mem.free(buf, 4096);See Defer & Errdefer for details.
Last modified: