The Stack & The Heap
in reviewThis lesson is complete and being validated before release - details may still change.
in reviewThis lesson is complete and being validated before release - details may still change.
process-memory.ts
import type { Tracer } from "../trace";/** The words a process gets; the picture is small enough to read every one. */export const MEMORY_SIZE = 32;export const MAX_OPERATIONS = 48;/** What a script asks the memory to do, one operation at a time. */export type MemoryOperation = | { kind: "call"; name: string; locals: number } | { kind: "return" } | { kind: "alloc"; id: string; size: number } | { kind: "free"; id: string };export interface Frame { name: string; /** The frame's lowest address; it holds the return address and the locals above it. */ base: number; size: number;}export interface HeapBlock { /** Null for a free block. */ id: string | null; address: number; size: number;}export interface MemoryReport { operations: number; frames: Frame[]; heap: HeapBlock[]; stackTop: number; heapTop: number; /** Why the script stopped early, or null. */ fault: string | null; /** Free heap words as a share of the heap's reach; blocks too small for the next request are what fragmentation means. */ freeWords: number; largestFree: number;}/** * The memory of one running program, as every C compiler lays it out: * the code and the static data at the bottom, a heap above them that * grows upward as the program asks for blocks, and a stack at the top * that grows downward one frame per call. A call pushes a frame of the * return address plus the callee's locals and a return pops it, so the * stack is free of charge and never fragments. The heap is a first-fit * free list: an allocation takes the first free block big enough, * splitting off the rest, and a free gives the block back and merges * it with free neighbours. Blocks freed in a different order from * their allocation leave holes, and a request can fail with enough * free words in total but none in one place. When the stack reaches * the heap, the program has run out of memory. * * Time: O(blocks) per heap operation, O(1) per stack operation. * Space: MEMORY_SIZE words. */export class ProcessMemory { private readonly frames: Frame[] = []; private heap: HeapBlock[] = []; private heapTop: number; private stackTop = MEMORY_SIZE; private operations = 0; private fault: string | null = null; constructor( /** Words taken by the code and the static data at the bottom. */ readonly staticWords: number, private readonly tracer?: Tracer, ) { if (!Number.isInteger(staticWords) || staticWords < 0 || staticWords >= MEMORY_SIZE) throw new RangeError(`static words must be in [0, ${MEMORY_SIZE})`); this.heapTop = staticWords; } /** The owner of a word: "static", a frame by index, a heap block id, "free" inside the heap, or null in the gap. */ owner(address: number): string | null { if (address < this.staticWords) return "static"; const frame = this.frames.findIndex((f) => address >= f.base && address < f.base + f.size); if (frame !== -1) return `frame ${frame}`; const block = this.heap.find((b) => address >= b.address && address < b.address + b.size); if (block) return block.id ?? "free"; return null; } /** Applies the script in order, stopping at the first fault. */ run(script: readonly MemoryOperation[]): MemoryReport { if (script.length > MAX_OPERATIONS) throw new RangeError(`${script.length} operations exceeds ${MAX_OPERATIONS}`); for (const operation of script) { if (this.fault) break; this.apply(operation); } return this.report(); } apply(operation: MemoryOperation): void { this.operations++; switch (operation.kind) { case "call": return this.call(operation.name, operation.locals); case "return": return this.pop(); case "alloc": return this.alloc(operation.id, operation.size); case "free": return this.free(operation.id); } } /** Pushes a frame: the return address and the locals, growing the stack downward. */ private call(name: string, locals: number): void { const size = locals + 1; const base = this.stackTop - size; if (base < this.heapTop) { this.fail(`stack overflow: ${name} needs ${size} words and only ${this.stackTop - this.heapTop} lie above the heap`); return; } this.stackTop = base; const frame: Frame = { name, base, size }; this.frames.push(frame); // ▸ a call pushes a frame below the stack top: the return address and the callee's locals this.tracer?.emit("call", { name, base, size, depth: this.frames.length, stackTop: this.stackTop, heapTop: this.heapTop }); } /** Pops the top frame; the words are free again at no cost. */ private pop(): void { const frame = this.frames.pop(); if (!frame) { this.fail("return with no frame on the stack"); return; } this.stackTop = frame.base + frame.size; // ▸ a return pops the top frame; its words are free again with no bookkeeping at all this.tracer?.emit("return", { name: frame.name, base: frame.base, size: frame.size, depth: this.frames.length, stackTop: this.stackTop }); } /** First fit: the first free block that is big enough, split if larger; otherwise grow the heap toward the stack. */ private alloc(id: string, size: number): void { if (!Number.isInteger(size) || size < 1) throw new RangeError(`block ${id} must be at least one word`); if (this.heap.some((block) => block.id === id)) throw new RangeError(`block ${id} is already allocated`); const searched = this.heap.filter((block) => block.id === null).map((block) => block.size); const index = this.heap.findIndex((block) => block.id === null && block.size >= size); let block: HeapBlock; let split = false; if (index !== -1) { const hole = this.heap[index]!; const freeBlock = this.heap[index]!; block = { id, address: hole.address, size }; block = { id, address: freeBlock.address, size }; const rest: HeapBlock[] = hole.size > size ? [{ id: null, address: hole.address + size, size: hole.size - size }] : []; const rest: HeapBlock[] = freeBlock.size > size ? [{ id: null, address: freeBlock.address + size, size: freeBlock.size - size }] : []; split = rest.length > 0; this.heap.splice(index, 1, block, ...rest); } else { if (this.heapTop + size > this.stackTop) { this.fail(`out of memory: ${id} needs ${size} words, ${this.freeWords()} are free in holes too small, and ${this.stackTop - this.heapTop} lie below the stack`); return; } block = { id, address: this.heapTop, size }; this.heap.push(block); this.heapTop += size; } // ▸ take the first hole that fits, splitting off the rest, or grow the heap toward the stack this.tracer?.emit("alloc", { id, address: block.address, size, searched, reused: index !== -1, split, heapTop: this.heapTop, freeWords: this.freeWords() }); } /** Marks a block free and merges it with free neighbours, so holes do not multiply. */ private free(id: string): void { const index = this.heap.findIndex((block) => block.id === id); if (index === -1) { this.fail(`free of ${id}, which is not allocated`); return; } const block = this.heap[index]!; let start = index; let end = index; if (this.heap[index - 1]?.id === null) start--; if (this.heap[index + 1]?.id === null) end++; const merged = this.heap.slice(start, end + 1); const neighboursJoined = this.heap.slice(start, end + 1); const hole: HeapBlock = { id: null, address: merged[0]!.address, size: merged.reduce((sum, b) => sum + b.size, 0) }; const freeBlock: HeapBlock = { id: null, address: neighboursJoined[0]!.address, size: neighboursJoined.reduce((sum, b) => sum + b.size, 0) }; this.heap.splice(start, merged.length, hole); this.heap.splice(start, neighboursJoined.length, freeBlock); // A hole at the top of the heap gives the words back to the gap. if (this.heap.at(-1)?.id === null) { this.heapTop = this.heap.at(-1)!.address; this.heap.pop(); } // ▸ give the block back and merge it with any free neighbour, so holes do not multiply this.tracer?.emit("free", { id, address: block.address, size: block.size, mergedWith: merged.length - 1, holeSize: hole.size, heapTop: this.heapTop, freeWords: this.freeWords() }); this.tracer?.emit("free", { id, address: block.address, size: block.size, mergedWith: neighboursJoined.length - 1, holeSize: freeBlock.size, heapTop: this.heapTop, freeWords: this.freeWords() }); } private fail(message: string): void { this.fault = message; // ▸ the stack met the heap, the heap met the stack, or a return or free had nothing to act on this.tracer?.emit("fault", { message, stackTop: this.stackTop, heapTop: this.heapTop, depth: this.frames.length }); } private freeWords(): number { return this.heap.filter((block) => block.id === null).reduce((sum, block) => sum + block.size, 0); } report(): MemoryReport { const holes = this.heap.filter((block) => block.id === null); return { operations: this.operations, frames: this.frames.map((frame) => ({ ...frame })), heap: this.heap.map((block) => ({ ...block })), stackTop: this.stackTop, heapTop: this.heapTop, fault: this.fault, freeWords: this.freeWords(), largestFree: holes.reduce((max, block) => Math.max(max, block.size), 0), }; }}12 operations done: 1 frame left on the stack, 8 heap words in use with 0 free in holes, the largest hole 0.