Memory Model
Joule uses an ownership-based memory model inspired by Rust. Memory is managed at compile time with no garbage collector.
Ownership
Every value has exactly one owner. When the owner goes out of scope, the value is dropped (memory freed):
fn example() {
let s = String::from("hello"); // s owns the string
process(s); // ownership moves to process()
// s is no longer valid here
}
Move Semantics
Assignment and function calls transfer ownership by default:
let a = Vec::new();
let b = a; // a is moved to b
// a is no longer valid
References
References borrow a value without taking ownership:
Immutable References
fn inspect(data: &Vec<i32>) {
let len = data.len();
// data is borrowed, not consumed
}
let v = Vec::new();
inspect(&v); // borrow v
// v is still valid here
Multiple immutable references can coexist:
let r1 = &v;
let r2 = &v; // ok: multiple immutable borrows
Mutable References
fn modify(data: &mut Vec<i32>) {
data.push(42);
}
let mut v = Vec::new();
modify(&mut v); // mutable borrow
Only one mutable reference can exist at a time:
let r1 = &mut v;
// let r2 = &mut v; // error: cannot borrow mutably twice
Borrowing Rules
The borrow checker enforces these rules at compile time:
- At any given time, you can have either:
- One mutable reference, OR
- Any number of immutable references
- References must always be valid -- no dangling references
- No mutable aliasing -- if a mutable reference exists, no other references to the same data can exist
Lifetimes
Lifetimes ensure references don't outlive the data they point to (planned):
fn first_word<'a>(s: &'a str) -> &'a str {
// The returned reference lives as long as the input
s.split(" ").next().unwrap_or("")
}
Box
Heap allocation with single ownership:
let boxed = Box::new(42); // allocate on the heap
let value = *boxed; // dereference
// Required for recursive types
pub enum List<T> {
Cons(T, Box<List<T>>),
Nil,
}
Box auto-derefs for field access:
let expr = Box::new(Expr { kind: ExprKind::Literal(42), span: Span::dummy() });
let kind = expr.kind; // auto-deref through Box
Raw Pointers
For unsafe, low-level memory access:
unsafe {
let ptr: *mut i32 = addr as *mut i32;
*ptr = 42;
}
Raw pointers bypass the borrow checker. Use only when necessary and always within unsafe blocks.
Stack vs. Heap
| Allocation | When | Performance |
|---|---|---|
| Stack | Local variables, small types | Fast (pointer bump) |
| Heap (Box) | Recursive types, large data, dynamic size | Slower (allocator call) |
| Heap (Vec) | Dynamic arrays | Amortized fast |
The compiler places values on the stack by default. Use Box<T> to explicitly heap-allocate.