Appendix A: A Simple Allocator
free-with-coalescing

Write free with forward coalescing:

fn free(ptr: i64) i64 {
    var block: i64 = ptr - 8;
    var size: i64 = load64(block);

    // Check if the next block in memory is on the free list
    var next_block: i64 = block + size;
    var prev: i64 = 0;
    var curr: i64 = free_list;
    while (curr != 0) {
        if (curr == next_block) {
            // Adjacent! Merge: absorb next block's size
            size += load64(curr);
            store64(block, size);
            // Remove next_block from free list
            var after: i64 = load64(curr + 8);
            if (prev == 0) { free_list = after; }
            else { store64(prev + 8, after); }
            break;
        }
        prev = curr;
        curr = load64(curr + 8);
    }

    // Add merged (or unmerged) block to free list
    store64(block + 8, free_list);
    free_list = block;
    return 0;
}

When we free block A, we check if block B (at address A + A's size) is on the free list. If it is, we remove B from the free list, add B's size to A, and put the merged block on the free list. One large block instead of two small ones.

Test -- allocate three blocks, free the middle one, free the first one, they should coalesce:

    check(
        \\ ... allocator with coalescing free ...
        \\var a: i64 = alloc(32);
        \\var b: i64 = alloc(32);
        \\var c: i64 = alloc(32);
        \\free(b);
        \\free(a);
        \\var d: i64 = alloc(72);
        \\d == a
    , 1);

a and b were adjacent in memory. Freeing b puts it on the free list. Freeing a checks if the block after a (which is b) is free -- it is. They merge into one 80-byte block (40 + 40). When we allocate 72 bytes (total 80 with header), the merged block fits perfectly.