# Memory Leak Detection

### Memory Leak Detection

A memory leak happens when a program allocates memory and then loses the ability to free it.

Here is the simplest leak:

```zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    _ = try allocator.alloc(u8, 1024);
}
```

The program asks for 1024 bytes, but it never stores the slice in a useful place and never calls `free`.

This line allocates memory:

```zig
_ = try allocator.alloc(u8, 1024);
```

But there is no matching cleanup:

```zig
allocator.free(...);
```

That is a leak.

#### Why Leaks Matter

A short program may finish before a small leak becomes visible. A long-running program is different.

A server, editor, database, game, or background worker may run for hours, days, or months. If it leaks memory repeatedly, memory use grows over time.

The pattern looks like this:

```text
handle request 1: leak 1 KiB
handle request 2: leak 1 KiB
handle request 3: leak 1 KiB
...
```

At first, nothing looks wrong. Later, the process may become slow, consume too much memory, or crash.

Zig’s allocator model helps because allocation and cleanup are visible in the code.

#### Use the General Purpose Allocator During Development

The general purpose allocator can help detect leaks.

A better beginner setup is:

```zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};

    defer {
        const status = gpa.deinit();
        if (status == .leak) {
            std.debug.print("memory leak detected\n", .{});
        }
    }

    const allocator = gpa.allocator();

    const buffer = try allocator.alloc(u8, 1024);
    defer allocator.free(buffer);

    buffer[0] = 42;
}
```

The important part is:

```zig
defer {
    const status = gpa.deinit();
    if (status == .leak) {
        std.debug.print("memory leak detected\n", .{});
    }
}
```

When `gpa.deinit()` runs, it can report whether memory was still allocated.

#### The Normal Shape

Most heap allocation should have a visible cleanup plan.

```zig
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);
```

Read this as:

```text
allocate now
free when this scope exits
```

This is the easiest pattern when the current function owns the memory for the whole scope.

#### Leak Caused by Early Return

Leaks often happen when code returns early.

Broken version:

```zig
const std = @import("std");

fn process(allocator: std.mem.Allocator, ok: bool) !void {
    const buffer = try allocator.alloc(u8, 1024);

    if (!ok) {
        return error.InvalidInput;
    }

    allocator.free(buffer);
}
```

If `ok` is false, the function returns before `allocator.free(buffer)` runs.

Correct version:

```zig
const std = @import("std");

fn process(allocator: std.mem.Allocator, ok: bool) !void {
    const buffer = try allocator.alloc(u8, 1024);
    defer allocator.free(buffer);

    if (!ok) {
        return error.InvalidInput;
    }
}
```

Now the cleanup runs even when the function returns early.

That is why `defer` is so useful in Zig.

#### Leak Caused by Partial Initialization

A more subtle leak happens when a function allocates several resources, then fails halfway.

Broken version:

```zig
const std = @import("std");

const Pair = struct {
    left: []u8,
    right: []u8,
};

fn makePair(allocator: std.mem.Allocator) !Pair {
    const left = try allocator.dupe(u8, "left");
    const right = try allocator.dupe(u8, "right");

    return Pair{
        .left = left,
        .right = right,
    };
}
```

If allocating `right` fails, `left` has already been allocated. Since the function exits with an error, `left` is leaked.

Correct version:

```zig
const std = @import("std");

const Pair = struct {
    left: []u8,
    right: []u8,

    pub fn deinit(self: Pair, allocator: std.mem.Allocator) void {
        allocator.free(self.left);
        allocator.free(self.right);
    }
};

fn makePair(allocator: std.mem.Allocator) !Pair {
    const left = try allocator.dupe(u8, "left");
    errdefer allocator.free(left);

    const right = try allocator.dupe(u8, "right");
    errdefer allocator.free(right);

    return Pair{
        .left = left,
        .right = right,
    };
}
```

`errdefer` runs only if the function exits with an error.

On success, the returned `Pair` owns both slices. The caller must clean them up:

```zig
const pair = try makePair(allocator);
defer pair.deinit(allocator);
```

This is a key Zig ownership pattern.

#### Leak Caused by Losing the Pointer

You can only free memory if you still have the slice or pointer needed to free it.

Broken version:

```zig
var buffer = try allocator.alloc(u8, 1024);
buffer = try allocator.alloc(u8, 2048);
defer allocator.free(buffer);
```

The first allocation is lost when `buffer` is overwritten. Only the second allocation is freed.

Correct version:

```zig
const first = try allocator.alloc(u8, 1024);
defer allocator.free(first);

const second = try allocator.alloc(u8, 2048);
defer allocator.free(second);
```

Do not overwrite your only reference to allocated memory unless you have already freed it or transferred ownership somewhere else.

#### Leak Caused by Containers

Zig containers often allocate memory internally.

For example, `ArrayList` can allocate as it grows.

```zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    var list = std.ArrayList(u8).init(allocator);
    try list.append(1);
    try list.append(2);
    try list.append(3);
}
```

This leaks because the list is never deinitialized.

Correct version:

```zig
var list = std.ArrayList(u8).init(allocator);
defer list.deinit();

try list.append(1);
try list.append(2);
try list.append(3);
```

A container that owns memory usually has a `deinit` method.

The pattern is:

```text
init
defer deinit
use the value
```

#### Leak Detection in Tests

Zig tests commonly use `std.testing.allocator`.

```zig
const std = @import("std");

test "build a list" {
    const allocator = std.testing.allocator;

    var list = std.ArrayList(u8).init(allocator);
    defer list.deinit();

    try list.append(10);
    try list.append(20);

    try std.testing.expectEqual(@as(usize, 2), list.items.len);
}
```

If the test leaks memory, the testing allocator can report it.

This makes tests a good place to catch ownership mistakes.

#### Leaks with Arenas

An arena allocator changes how leak detection feels.

With an arena, you usually do not free each allocation individually.

```zig
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();

const allocator = arena.allocator();

const a = try allocator.dupe(u8, "red");
const b = try allocator.dupe(u8, "blue");

_ = a;
_ = b;
```

There is no leak as long as `arena.deinit()` runs.

All arena allocations are released together.

But this can hide a design problem if the arena lives too long.

For example, one arena for an entire server process may keep growing forever. Technically, the memory belongs to the arena, but practically, the program behaves like it is leaking.

Arenas should have clear lifetimes:

```text
one request
one parse
one compiler pass
one frame
one temporary operation
```

#### Deinit Order Matters

When several objects depend on the same allocator, free the objects before deinitializing the allocator.

Good order:

```zig
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();

const allocator = gpa.allocator();

var list = std.ArrayList(u8).init(allocator);
defer list.deinit();
```

Because `defer` runs in reverse order, `list.deinit()` runs before `gpa.deinit()`.

That is what we want.

Bad order:

```zig
var list = std.ArrayList(u8).init(allocator);
defer _ = gpa.deinit();
defer list.deinit();
```

This is only shown to make the point. Arrange setup so that owned memory is released before the allocator itself is destroyed.

#### A Practical Checklist

When reviewing Zig code, ask:

```text
Where is memory allocated?
Who owns it?
Where is it freed?
Can the function return early?
Can a later allocation fail?
Does the container need deinit?
Does the allocator outlive the memory allocated from it?
```

These questions catch many leaks.

#### The Core Idea

Memory leak detection in Zig depends on clear ownership and good allocator choice.

Use `GeneralPurposeAllocator` or `std.testing.allocator` while developing. Use `defer` for normal cleanup. Use `errdefer` for cleanup during failed initialization. Call `deinit` on containers that own memory.

The rule is:

```text
Every owned allocation needs exactly one cleanup path.
```

