Storage Organization

A compiler does not just translate expressions — it decides, for every name and every value in the program, where in memory it will live and for how long. Before code generation can emit a single load or store, the compiler needs a storage organization: a plan that carves the running program's address space into regions, each with its own allocation discipline. Get this plan right and recursion, dynamic data structures, and global constants all coexist happily. Get it wrong and you get dangling pointers, leaks, or a stack that eats the heap alive.

The symbol table told the compiler which names exist and what they mean. Storage organization answers the next question: what memory does each of those names map to at run time? The answer is not one-size-fits-all — a global constant, a loop counter, and a linked-list node each want a different home.

The four regions of an address space

A classical run-time image divides memory into four regions, laid out from low addresses to high. The two at the bottom have sizes fixed before the program runs; the two at the top grow and shrink while it runs — and, cunningly, they grow toward each other so that whichever one needs more room can borrow from the shared gap between them.

Reveal the column region by region. At the bottom sits the code (or text) segment — the machine instructions themselves, fixed in size and usually read-only. Above it the static (or global) area holds data whose size and address are both known at compile time: global variables, string literals, static locals. Then comes the heap, which grows upward as the program allocates dynamic objects, and — from the very top of the address space downward — the stack, which grows downward as calls nest.

Why the stack and heap grow toward each other

This is a genuinely elegant design decision, not an accident. The compiler cannot know in advance how a program will divide its dynamic memory between the two: a deeply recursive numerical routine is stack-hungry and heap-light, while a program building a huge graph is the reverse. If you gave each a fixed budget you would waste memory in one and starve the other.

By putting the heap at the bottom of the dynamic area growing up, and the stack at the top growing down, both draw from a single shared pool of free addresses in the middle. Either can expand as far as it likes until they meet — and that meeting point, not an arbitrary partition, is the true out-of-memory condition. One pool, allocated on demand from both ends: maximum flexibility from a single contiguous span.

What decides an object's storage class?

The compiler chooses a home for each object by asking two questions. First, is its size known at compile time? A fixed-size record can be placed statically or on the stack; an array whose length is a run-time value, or a structure that must outlive its creator, is forced onto the heap. Second, what is its lifetime? — how long must the storage remain valid?

Kind of objectLifetimeSize known at compile time?Lives in
Global / static variableWhole program runYesStatic area
String / numeric literalWhole program runYesStatic area
Local variable, parameterOne activation of its procedureUsually yesStack
Compiler temporaryWithin one expression / callYesStack (or register)
new / malloc objectArbitrary — until freed / unreachableOften noHeap

Notice the pattern: lifetime that nests with the call structure ⇒ stack; lifetime independent of the call structure ⇒ heap; lifetime = the whole program ⇒ static. A language like C makes the programmer pick (static, an automatic local, or malloc); a language like Java hides the choice but the compiler still makes exactly these decisions underneath.

A worked decision

Walk through where each name in this fragment ends up. The reasoning is exactly what a compiler's storage planner does:

let counter = 0; // module-level → STATIC (lives whole run) const GREETING = "hello"; // literal → STATIC (read-only) function build(n: number) { // n → STACK (one per call) let sum = 0; // sum → STACK (dies at return) const node = { value: n }; // {value:n} → HEAP (may escape via return) return node; // the object outlives this frame → must be heap }

counter and GREETING exist for the program's entire life, so they go in the static area. n and sum live exactly as long as one call to build, so they ride the stack and vanish when the call returns. But the object { value: n } is returned — it must survive after build's frame is gone — so it cannot live on the stack; it is allocated on the heap. This "does it escape the frame?" test is precisely what real escape-analysis passes compute.

Static vs stack: the same idea in a tiny simulator

The essential difference between static and stack storage is reuse. A static slot is written once and keeps its address forever; a stack slot's address is handed out on entry and reclaimed on exit, so a later call reuses the very same bytes. This short model shows the stack pointer bumping up on each entry and back down on each exit, while a static counter simply persists.

let stackTop = 1000; // stack grows DOWNWARD from a high address let staticCounter = 0; // one fixed slot, never reclaimed function enter(name: string, bytes: number): number { stackTop -= bytes; // allocate = bump the pointer console.log(`enter ${name}: frame at addr ${stackTop} (${bytes} bytes)`); return stackTop; } function leave(name: string, bytes: number): void { stackTop += bytes; // free = un-bump; the bytes are now reusable console.log(`leave ${name}: stackTop back to ${stackTop}`); } enter("f", 32); staticCounter += 1; // the static slot survives across calls enter("g", 16); leave("g", 16); enter("h", 16); // reuses the SAME address g just freed leave("h", 16); leave("f", 32); console.log(`staticCounter persisted = ${staticCounter}`);

Watch g and h receive the identical address: stack storage is recycled the instant a frame is popped, which is exactly why a local's value is meaningless after its procedure returns.

String and numeric literals go in the static area, and in most systems that region is mapped read-only by the loader. That is why, in C, taking char *s = "hi"; and then writing s[0] = 'H'; is undefined behaviour that typically segfaults: the compiler put "hi" in a read-only static page, and the store faults. It also lets the compiler share identical literals — two occurrences of "hello" may point at the same bytes. Statically-known, program-lifetime, immutable: the perfect tenant for the static segment.

The classic catastrophe is returning a pointer to a local. A local variable has automatic lifetime: its storage is on the stack and is reclaimed the moment its procedure returns. If you return the address of that local, you hand back a pointer into a frame that no longer exists — a dangling pointer. The next call overwrites those bytes and your pointer now reads garbage.

function bad(): { value: number } { const local = { value: 42 }; // in a language with stack objects, this is on the frame… return &local; // …and this hands back an address that dies at return — BUG }

The fix is to match storage to lifetime: an object that must outlive its creating call needs dynamic (heap) allocation, not automatic (stack) allocation. Languages with garbage collection sidestep the trap by heap-allocating any object that escapes; C and C++ leave the choice — and the danger — to you.