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:

  1. At any given time, you can have either:
    • One mutable reference, OR
    • Any number of immutable references
  2. References must always be valid -- no dangling references
  3. 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

AllocationWhenPerformance
StackLocal variables, small typesFast (pointer bump)
Heap (Box)Recursive types, large data, dynamic sizeSlower (allocator call)
Heap (Vec)Dynamic arraysAmortized fast

The compiler places values on the stack by default. Use Box<T> to explicitly heap-allocate.