← Prism
Docs
Overview defer orelse zeroinit raw auto-unreachable
Spec Draft
GitHub ↗

auto-unreachable

After every call to a noreturn function, Prism injects __builtin_unreachable(). This tells the backend compiler that execution cannot continue past that point — enabling dead code elimination, tail-call optimization, and register pressure relief with zero effort.

Overview

When you call a noreturn function like abort() or your own die(), the compiler doesn't automatically know the call won't return unless it has type information from the declaration. Even when it does, Prism adds transitive noreturn knowledge — if your error handler calls exit(), Prism tags it as noreturn and injects __builtin_unreachable() after every call site, propagating the hint through your entire codebase.

On MSVC, Prism injects __assume(0) instead — semantically equivalent.

Standard C — compiler can't see past the call
void die(const char *msg) {
    fprintf(stderr, "%s
", msg);
    exit(1);
}

int parse(const char *src) {
    Token *t = tokenize(src);
    if (!t) die("out of memory");
    // compiler: t might be NULL here
    // keeps NULL check code, registers live
    return process(t);
}
With Prism — optimizer knows die() doesn't return
void die(const char *msg) {
    fprintf(stderr, "%s
", msg);
    exit(1);
}

int parse(const char *src) {
    Token *t = tokenize(src);
    if (!t) { die("out of memory"); __builtin_unreachable(); }
    // compiler: t is guaranteed non-NULL here
    // eliminates NULL branch, frees registers
    return process(t);
}

The emitted __builtin_unreachable() is invisible to you — it only appears in the transpiled output, not your source. You just call die() as normal and the optimizer benefits automatically.

Behavior

After Prism emits a statement-level call to a noreturn function, it immediately appends __builtin_unreachable();:

// Your source:
die("fatal error");

// Prism emits:
die("fatal error"); __builtin_unreachable();

Five injection conditions must all hold:

  1. The identifier is tagged TT_NORETURN_FN — it must be a known noreturn function
  2. The identifier is followed by ( — it's a call, not a function pointer reference
  3. The matching ) is followed by ; — it's a statement-level call, not a subexpression
  4. Inside a function body — block_depth > 0
  5. Not inside a braceless control body — would create a two-statement body without braces, which changes semantics

Disable for a whole file with -fno-auto-unreachable.

Detection — noreturn recognition

Prism recognizes noreturn functions from all common declaration syntaxes. All occurrences of the function name are tagged during tokenization — so every call site benefits, even in files that don't include the header:

SyntaxExample
C11 keyword_Noreturn void die(const char *msg);
noreturn macronoreturn void die(const char *msg);
C23 attribute[[noreturn]] void die(const char *msg);
GCC attribute__attribute__((noreturn)) void die(const char *msg);
GCC cold+noreturn__attribute__((cold, noreturn)) void die(...);
MSVC declspec__declspec(noreturn) void die(const char *msg);
Known builtinsexit, abort, _Exit, _exit, quick_exit, __builtin_trap, __builtin_unreachable, thrd_exit

Examples

Custom error handler — transitive noreturn

The most common pattern: your own fatal error function. As long as it's declared noreturn, every call site gets the hint:

_Noreturn void fatal(const char *fmt, ...) {
    // va_args, fprintf, exit...
}

int init(struct App *app) {
    app->db = db_open("app.db") orelse {
        fatal("cannot open db: %s", strerror(errno));
        // __builtin_unreachable() injected here by Prism
    }
    // optimizer: app->db is guaranteed non-NULL from here
    return 0;
}

Assertion macro — eliminate post-assert null checks

// Your assert calls abort() — which is a known builtin noreturn
void check_ptr(void *p, const char *name) {
    if (!p) {
        fprintf(stderr, "null: %s
", name);
        abort();
        // __builtin_unreachable() injected — optimizer knows abort() doesn't return
    }
    // branch after abort is dead — compiler eliminates it
}

Platform panic — kernel / embedded

[[noreturn]] void panic(const char *msg) {
    disable_interrupts();
    uart_puts(msg);
    for(;;);  // halt
}

void handle_fault(uint32_t addr) {
    if (addr == 0) {
        panic("null deref");
        // Prism injects __builtin_unreachable() — dead code after eliminated
    }
    // optimizer knows addr != 0 here
    map_fault(addr);
}

What the optimizer sees

The __builtin_unreachable() injection has three main effects on the backend compiler:

Dead code elimination

Code after the noreturn call is proven unreachable. The compiler eliminates entire branches, NULL checks, and error paths from the final binary:

if (!ptr) {
    die("null pointer");
    // __builtin_unreachable() here
}
// compiler: ptr is non-NULL from here — no NULL check in generated code
use(ptr);

Register pressure relief

When the compiler knows a branch is unreachable, it doesn't need to keep values alive across it. Registers that would have been spilled to handle the error path can stay hot — especially valuable in tight inner loops:

for (int i = 0; i < n; i++) {
    Item *item = get_item(i) orelse {
        abort();
        // __builtin_unreachable() — item is guaranteed non-NULL below
    }
    process(item);  // no NULL guard needed in emitted asm
}

Tail-call optimization

When a noreturn call is the last statement before a return, the compiler can convert it to a tail call or eliminate the return entirely:

void fatal_oom(void) {
    log_error("out of memory");
    abort();
    // __builtin_unreachable() — no return epilogue needed
    // stack frame may be eliminated entirely
}

Edge Cases

Spec Reference

auto-unreachable — Prism Spec §6.4