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.
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);
}
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:
- The identifier is tagged
TT_NORETURN_FN— it must be a known noreturn function - The identifier is followed by
(— it's a call, not a function pointer reference - The matching
)is followed by;— it's a statement-level call, not a subexpression - Inside a function body —
block_depth > 0 - 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:
| Syntax | Example |
|---|---|
| C11 keyword | _Noreturn void die(const char *msg); |
noreturn macro | noreturn 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 builtins | exit, 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
- Subexpression calls are not injected.
result = die(msg)orf(die(msg))— injection only fires for statement-level calls ending with);. - Braceless control bodies are skipped.
if (err) die(msg);— no injection, because adding__builtin_unreachable();would create a two-statement body, changing the if's semantics. Wrap with braces to enable:if (err) { die(msg); }. - Function pointer calls are not injected.
fp = die; fp("msg");— the identifierdiehas the noreturn tag, butfpdoes not. Injection requires the noreturn-tagged identifier directly before(. - Only statement-level. The
)must be immediately followed by;. If there's a trailing expression (die(), cleanup()comma expression), no injection. - Disable globally with
-fno-auto-unreachablewhen using sanitizers or unusual stack unwinding where unreachable hints cause false positives.