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.