Memory model
In Python and JavaScript, you never think about who frees memory. You allocate something, and at some point later a garbage collector notices nothing references it anymore and reclaims it. It works, but it has costs: unpredictable pauses, extra memory overhead, and you genuinely don't know when destructors fire.
In C, the opposite extreme: you call malloc and you call free. Forget the free and you leak. Call it twice and you crash.
Glide finds a middle path with two tools: for scope-local values the compiler inserts the free for you (auto-drop); for values that must outlive a scope you hold a raw pointer and free it yourself — with the borrow checker making sure that lending a value out never turns into a use-after-free. No garbage collector. No lifetime annotations like Rust's 'a. This chapter is how both work.
What lives where
Two places a value can live:
- The stack: scalar values (
i32,bool,f64...) and value structs (let p: Point = Point { x: 1, y: 2 };). Cheap, automatic, scoped to the function call. Copied on assignment and return. - The heap: anything that grows at runtime —
*Vector<T>,*HashMap<V>, user-defined*MyThingfromMyThing::new(). The*prefix on the type is your cue that the value lives on the heap and is reached through a pointer.
let n: i32 = 42; // stack
let v: *Vector<i32> = Vector::new(); // heap — a pointer to it
Two kinds of heap pointer
Here's the one idea that makes Glide's memory model click. A heap pointer comes in two flavours, and you choose which by how you write the binding:
let v* = Vector::new(); // ① auto-drop: freed at end of scope
let p: *Vector<i32> = Vector::new(); // ② raw: you manage it
① Auto-drop — the * written after the binding name means "I own this; free it for me when this scope ends." You write no free; the compiler inserts it at the closing brace. The catch: an auto-drop value can't leave its scope. You can't return it, can't assign it to another binding, can't hand it to a function that takes the pointer by value — all of those would let it be freed twice.
② Raw — a plain *T binding with no * marker is just an address. The compiler does not track it and does not free it. You're responsible: call free(p as *void) when you're done, hand it to an arena, or — for a short-lived program — just let it live until the process exits. Constructors like Vector::new() and Type::new() hand back raw pointers.
Auto-drop: cleanup you don't write
Use let name* = ... for a heap value that lives and dies inside one function:
fn process() -> i32 {
let v* = Vector::new(); // owned here
v.push(10);
v.push(20);
return v.len();
} // compiler frees v here — you wrote no `free`
Try to let it escape and the compiler stops you:
fn make_list() -> *Vector<i32> {
let v* = Vector::new();
v.push(1);
return v; // ✗ error: cannot return owned value `v` (auto-drop would free it)
}
That error is the whole safety story in one line: the compiler will free v at the end of make_list, so handing it back to the caller would be a use-after-free. It won't let you.
Returning a heap value: use a raw pointer
When a function genuinely needs to build something and hand it back, return a raw pointer (no * marker). The caller takes responsibility:
fn make_list() -> *Vector<i32> {
let v: *Vector<i32> = Vector::new(); // raw — not auto-dropped
v.push(1);
v.push(2);
return v; // fine: nothing frees it here
}
fn main() -> i32 {
let list: *Vector<i32> = make_list(); // we now hold a raw pointer
println!(list.len());
// not auto-freed; for a short program, fine to let it live until exit.
// long-running code would `free(list as *void)` (or use an arena).
return 0;
}
Notice the trade-off: the auto-drop version is safer but can't escape; the raw version can escape but you own the cleanup. There's no third option that's both — that honesty is the point.
Borrows: lending, not giving
A borrow is temporary read or write access to a value the borrower doesn't own. Two flavours:
&T— shared borrow. Read-only. Many borrowers can have one at the same time.&mut T— exclusive borrow. Read/write. Only one at a time, no others.
fn print_first(v: &*Vector<i32>) { // takes a shared borrow
println!(v.get(0));
}
fn append_one(v: &mut *Vector<i32>) { // takes a mutable borrow
v.push(99);
}
fn main() -> i32 {
let v* = Vector::new();
v.push(1);
v.push(2);
print_first(&v); // lend it out, read-only
append_one(&mut v); // lend it out, writable
println!(v.len()); // we still own it — borrows ended when the calls returned
return 0;
}
Read the patterns:
- The function's parameter type says what flavour of access it needs.
- The caller marks the call site with
&or&mutto match. - Ownership stays with the caller throughout.
Why Glide doesn't need lifetimes
Rust forces you to annotate how long borrows live when the compiler can't figure it out (fn foo<'a>(...)). Glide's flow analysis is strong enough that you almost never need that — the lifetime of a borrow is inferred from where it's used.
The trade-off Glide makes: in the rare cases where the analysis can't figure things out, you can't paper over it with an annotation. You restructure the code instead (often by cloning, sometimes by changing ownership patterns). For the 95% case this is a huge ergonomics win.
Auto-drop in action
Watch the compiler decide, using the auto-drop (*-marker) form:
fn main() -> i32 {
let v* = Vector::new();
v.push(1);
v.push(2);
if some_condition() {
let w* = Vector::new();
w.push(10);
// w is dropped at the end of this branch
}
// v is dropped at the end of main
return 0;
}
The compiler computes: w is only reachable inside the if-block, so its drop goes at the closing } of the block. v is reachable throughout main, so its drop goes just before return 0;. You see no free calls in your code; the compiler emits them at exactly the right points.
This even works correctly across early returns and ? propagation — every exit path drops the live values.
Arenas: bulk allocation for hot paths
Some programs allocate hundreds of small things during a single operation (parsing a request, processing a frame), all of which become useless at the end of that operation. Calling free on each one is wasteful; you'd rather bulk-free them.
That's an arena: a chunk of memory that you allocate from cheaply, then free in one shot. Glide ships with one:
let arena: *void = __glide_palloc_make();
let prev: *void = __glide_palloc_get();
__glide_palloc_set(arena);
defer __glide_palloc_free(arena);
defer __glide_palloc_set(prev);
// every Vector::new(), .concat(), .substring(), etc. inside this scope
// allocates from `arena` and is freed all at once when arena is freed.
You'll see this pattern at the top of an HTTP request handler — every per-request allocation goes into the arena, and the whole thing is freed when the response is sent. Way faster than tracking each allocation individually.
Why no garbage collector
Three reasons Glide skips the GC:
- Predictable performance. A GC introduces pauses you can't time. For a server handling thousands of requests per second, a 50ms GC pause is a 50ms latency spike for any user unlucky enough to land in the middle of it.
- Lower memory overhead. GCs typically keep 1.5–3× the working set alive at any time. Compile-time ownership uses just what you need.
- Honesty. A GC hides allocation. With explicit ownership, you can read a function and know what allocates and what doesn't.
The cost is what this chapter just covered: you think a bit about who owns what. The payoff is fast, predictable, lean programs.
Comparison cheat sheet
| What Python does | What Glide does |
|---|---|
| Reference-count + GC | No GC — auto-drop or a raw pointer you manage |
| Freed when last reference drops | let v* freed at scope end; raw *T freed by you |
| Everything is a reference | Stack values copy; heap reached via *T; lend with & / &mut |
| No annotation on memory | *T heap pointer, let v* auto-drop, &T borrow |
| Use-after-free can crash at runtime | Returning or moving an auto-drop value is a compile error |
Where to next
You can now read Glide code and understand who owns what. The next chapter — Structs, enums, traits, generics — is about modelling your own data and behaviour.