If your program has nice boundaries, an alternative to realloc is to just calloc far more memory than you are ever likely to need. Obviously this is a bad idea in certain circumstances, but works really nicely for things like a compiler with well defined entry/exit points. With this technique, you only need one branch in the up allocator to check for an oom (or you can yolo and remove this check if you are quite sure you won't oom).
In code, I essentially implement bump allocation (for a global allocator, though this could be adapted easily to reference an arena) in the following way:
static unsigned char *__mem_base = 0;
static unsigned char *__mem_limit = 0;
static unsigned char *__mem_current = 0;
void init (size_t max_mem) {
__mem_base = (unsigned char*) calloc(1, max_mem);
__mem_current = __mem_base;
__mem_limit = __mem_base + max_mem;
}
#define ALLOC(name, type) \
type *name = (type *) __mem_current; \
__mem_current += (sizeof(type) + 0x7) & ~0x7; \
if (__mem_current >= __mem_lim) { fprintf(stderr, "OOM %d", __LINE__); exit(1) }
I tend to use a huge value like 1GB for max_mem, which I know is extremely unlikely to be triggered when compiling a normal sized source program.