Memory Management
This chapter introduces heap allocation. If you are new to the stack vs heap distinction, this is a fundamental concept in systems programming.
In Thirdlang, objects live on the heap and must be explicitly freed. This is different from garbage-collected languages like Java or Python, and similar to C++ or Rust (without RAII). Compare this to Secondlang where all values lived on the stack.
Stack vs Heap
The stack is like a stack of cafeteria trays - you can only add or remove from the top, and they’re all the same size. The heap is like a parking lot - you can park (allocate) anywhere there’s space, leave your car as long as you want, but you must remember to retrieve it (free) or it stays forever (memory leak).
Let us review the two types of memory:
Stack Memory
- Automatic - Allocated/freed with function calls
- Fast - Just move a pointer
- Limited size - Typically a few MB
- LIFO - Last in, first out
In Secondlang, all values live on the stack:
def foo() {
x = 10 # x is on the stack
y = 20 # y is on the stack
return x + y
} # x, y automatically freed
Heap Memory
- Manual - You allocate and free
- Slower - System call to OS
- Large - Can use all available RAM
- Flexible - Allocate any time, free any time
In Thirdlang, objects live on the heap:
p = new Point(1, 2) # Allocate on heap
# ... use p ...
delete p # Must free manually!
Why the heap? We chose heap allocation because:
- Objects can outlive the function that created them
- Multiple variables can reference the same object
- It mirrors how most OOP languages work (Java, Python, etc.)
Note: Some languages (like Rust or C++) can place objects on the stack for efficiency. We keep things simple with heap-only allocation.
The new Operator
new allocates heap memory:
p = new Point(10, 20)
Behind the scenes:
- Calculate size - How many bytes for a Point?
- Call malloc - Ask OS for memory
- Call constructor - Initialize the memory
- Return pointer - Give caller the address
Size Calculation
class Point {
x: int # 8 bytes (i64)
y: int # 8 bytes (i64)
} # Total: 16 bytes
LLVM calculates this for us using sizeof.
Malloc
We declare malloc from the C library:
declare ptr @malloc(i64) ; Takes size, returns pointer
Then call it:
%ptr = call ptr @malloc(i64 16) ; Allocate 16 bytes
The delete Operator
delete frees heap memory:
delete p
Behind the scenes:
- Call destructor (if exists) - Run
__del__ - Call free - Return memory to OS
Free
We declare free from the C library:
declare void @free(ptr) ; Takes pointer, returns nothing
Then call it:
call void @free(ptr %p) ; Free the memory
Destructors: __del__
The destructor is called automatically when you delete an object:
class Resource {
id: int
def __init__(self, id: int) {
self.id = id
}
def __del__(self) {
# Cleanup code here
# (In a real language, might close files, release handles, etc.)
}
}
r = new Resource(42)
delete r # Calls __del__, then free()
Destructor Rules
- Named
__del__ - Only parameter is
self - No return type
- Called before memory is freed
- Optional - if not defined, just free memory
Use Cases
In real languages, destructors:
- Close file handles
- Release network connections
- Free nested allocations
- Log cleanup events
In Thirdlang, we keep it simple - destructors can run any code.
Code Generation for delete
Here is how we compile delete:
Stmt::Delete(expr) => {
let obj_val = self.compile_expr(expr)?;
let obj_ptr = obj_val.into_pointer_value();
// Call destructor if exists
if let Type::Class(class_name) = &expr.ty {
let class_info = self.classes.get(class_name);
if let Some(info) = class_info {
if info.has_destructor {
let dtor_name = format!("{}____del__", class_name);
if let Some(dtor) = self.functions.get(&dtor_name) {
self.builder
.build_call(*dtor, &[obj_ptr.into()], "dtor")
.unwrap();
}
}
}
}
// Call free
let free_fn = self.module.get_function("free").unwrap();
self.builder
.build_call(free_fn, &[obj_ptr.into()], "")
.unwrap();
Ok(None)
}
Generated LLVM IR:
; delete p (where p is a Point with destructor)
call void @Point__del(ptr %p) ; Destructor
call void @free(ptr %p) ; Free memory
Memory Safety Issues
Without automatic memory management, several bugs become possible:
Memory Leak
Forgetting to delete:
def leak() {
p = new Point(1, 2)
# Oops, forgot delete p!
} # Memory is lost forever
The memory stays allocated until the program exits.
Use After Free
Using an object after deleting it:
p = new Point(1, 2)
delete p
p.x # BUG! Memory already freed
This is undefined behavior - anything can happen.
Double Free
Deleting the same object twice:
p = new Point(1, 2)
delete p
delete p # BUG! Already freed
Also undefined behavior - might crash, might corrupt memory.
Dangling Pointer
Multiple variables pointing to freed memory:
p = new Point(1, 2)
q = p # Both point to same object
delete p
q.x # BUG! q is now dangling
Why Manual Memory Management?
We chose explicit new/delete for educational purposes:
| Approach | Pros | Cons |
|---|---|---|
| Manual (C, C++) | Fast, predictable, teaches fundamentals | Error-prone |
| Garbage Collection (Java, Python) | Safe, convenient | Overhead, pauses |
| RAII (Rust, C++) | Safe, no runtime cost | Complex ownership rules |
| Reference Counting (Swift, Python) | Predictable cleanup | Cycles, overhead |
Understanding manual management helps you appreciate what other approaches solve.
Memory Layout Example
Let us trace through a complete example:
class Point {
x: int
y: int
def __init__(self, x: int, y: int) {
self.x = x
self.y = y
}
def __del__(self) {
# Cleanup
}
}
p = new Point(10, 20)
delete p
Step 1: new Point(10, 20)
malloc(16)returns0x1000Point__init(0x1000, 10, 20)initializes fieldspholds the pointer0x1000
Step 2: delete p
Point__del(0x1000)runs destructorfree(0x1000)returns memory to OSpstill holds0x1000but it is invalid now!
Best Practices
Even though our language is simple, good habits help:
1. Delete What You Allocate
p = new Point(1, 2)
# ... use p ...
delete p # Always clean up
2. Set to “null” After Delete (If We Had Null)
In real languages:
delete p
p = null # Mark as invalid
3. One Owner
Have a clear owner responsible for deletion:
# Function creates and returns - caller owns it
def make_point() -> Point {
return new Point(1, 2)
}
p = make_point() # Caller is responsible
# ... use p ...
delete p # Caller deletes
Summary
| Operation | What It Does | When |
|---|---|---|
new Class(args) | Allocate + initialize | When you need an object |
delete obj | Destruct + free | When done with object |
__init__ | Initialize fields | Called by new |
__del__ | Cleanup before free | Called by delete |
Memory management is one of the hardest parts of systems programming. Our simple model teaches the fundamentals without the full complexity of real-world solutions.
At this point, you should understand:
- Why objects live on the heap (can outlive functions, shared references)
- How
newcalls malloc then the constructor - How
deletecalls the destructor then free - Common memory bugs: leaks, use-after-free, double free
Next, let us look at LLVM code generation for classes.