Read the spec
Tokenizer, Pass 1 analysis, CFG verification, Pass 2 codegen, all language semantics.
Bound to scope, not to every exit path. Runs in reverse order at scope end — whether you return early, fall through, or hit an error branch.
FILE *f = fopen(path, "r");
if (!f) return -1;
int r = parse(f);
if (r < 0) {
fclose(f); // easy to forget
return -1;
}
fclose(f);
return r;
FILE *f = fopen(path, "r")
orelse return -1;
defer fclose(f);
return parse(f);
FILE *a = fopen(pa, "r");
if (!a) return -1;
FILE *b = fopen(pb, "r");
if (!b) {
fclose(a); // must remember order
return -1;
}
process(a, b);
fclose(b);
fclose(a);
FILE *a = fopen(pa, "r")
orelse return -1;
defer fclose(a);
FILE *b = fopen(pb, "r")
orelse return -1;
defer fclose(b);
process(a, b);
pthread_mutex_lock(&m);
if (x < 0) {
pthread_mutex_unlock(&m);
return -1; // deadlock if missed
}
do_work();
pthread_mutex_unlock(&m);
pthread_mutex_lock(&m);
defer pthread_mutex_unlock(&m);
if (x < 0) return -1;
do_work();
char *buf = malloc(n);
if (buf == NULL) {
log_error("OOM");
return -1;
}
// ... 20 more lines
char *buf = malloc(n)
orelse return -1;
char *src = read_file(path);
if (!src) return -1;
Token *tok = tokenize(src);
if (!tok) {
free(src);
return -1;
}
Node *ast = parse(tok);
if (!ast) {
token_free(tok);
free(src);
return -1;
}
int r = emit(ast);
node_free(ast);
token_free(tok);
free(src);
return r;
char *src = read_file(path)
orelse return -1;
defer free(src);
Token *tok = tokenize(src)
orelse return -1;
defer token_free(tok);
Node *ast = parse(tok)
orelse return -1;
defer node_free(ast);
return emit(ast);
const char *get_name(User *u) {
if (!u) return "unknown";
if (!u->profile) return "unknown";
if (!u->profile->name) return "unknown";
return u->profile->name;
}
const char *get_name(User *u) {
Profile *p = (u orelse return "unknown")->profile
orelse return "unknown";
return p->name orelse "unknown";
}
Checks any falsy scalar — null pointer, zero integer, zero return code — inline. No
nested if-blocks, no repeated
cleanup calls, no diverging error paths. Works with return, break,
continue, goto, or a fallback value.
Every local variable is automatically zeroed at declaration — scalars, pointers, arrays, structs. The entire class of bugs from uninitialized reads is simply closed.
typedef struct {
int width;
int height;
int flags; // garbage
} Config;
Config cfg; // uninitialized
if (cfg.flags & VERBOSE) {
// undefined behavior
}
typedef struct {
int width;
int height;
int flags;
} Config;
Config cfg; // zeroed automatically
if (cfg.flags & VERBOSE) {
// always safe — flags == 0
}
char *result; // dangling garbage
if (condition) {
result = compute();
}
use(result); // crash if !condition
char *result; // NULL automatically
if (condition) {
result = compute();
}
use(result); // NULL check handles it
int process(const char *path) {
FILE *f = fopen(path, "r");
if (!f) goto done;
defer fclose(f); // skipped!
do_work(f);
done:
return 0; // f never closed
}
int process(const char *path) {
FILE *f = fopen(path, "r");
if (!f) goto done;
defer fclose(f);
do_work(f);
done:
// error: goto 'done' skips over
// defer fclose(f)
return 0;
}
switch (cmd) {
case CMD_INIT:
db = db_open(path);
// forgot break — falls through
case CMD_RUN:
// db may not be open here
db_exec(db, query);
break;
}
switch (cmd) {
case CMD_INIT:
db = db_open(path);
defer db_close(db);
db_init(db);
case CMD_RUN: // error: case CMD_RUN
// skips over defer db_close(db)
db_exec(db, query);
break;
}
// somewhere in your codebase:
jmp_buf g_recover;
int parse_file(const char *path) {
char *buf = read_file(path);
if (!buf) return -1;
// if inner longjmp fires: buf leaks
int r = parse(buf); // may longjmp
free(buf);
return r;
}
int parse_file(const char *path) {
char *buf = read_file(path)
orelse return -1;
defer free(buf);
// error: defer forbidden —
// function calls setjmp.
// longjmp would bypass cleanup.
return parse(buf);
}
CFG analysis runs before any output is emitted. goto jumping over
declarations, switch fallthrough skipping active defers, defer in
setjmp/vfork/asm functions — all compile errors, not
silent bugs.
Zero-init has a cost on large buffers. raw skips initialization for a
single variable without disabling anything globally.
// zeroed by default — 64k wasted memset
char buf[65536];
ssize_t n = read(fd, buf, sizeof(buf));
// raw: skip it — read fills it anyway
raw char buf[65536];
ssize_t n = read(fd, buf, sizeof(buf));
// getaddrinfo fills this completely
raw struct addrinfo hints;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
hints.ai_protocol = 0;
struct addrinfo *res;
getaddrinfo(NULL, port, &hints, &res)
orelse return -1;
defer freeaddrinfo(res);
Not happy-path coverage. Edge cases, degenerate control flow, and patterns specifically designed to break a transpiler.
Prism is fully open source — Apache 2.0 licensed. Read every line, fork it, ship it. No strings.
OpenSSL, SQLite, Bash, Curl, GNU Coreutils, and Make — compiled unmodified through Prism. If it compiles and runs correctly there, it compiles and runs correctly everywhere.
Nested loops with break and continue, switch
fallthrough into deferred scopes, goto jumping over declarations, statement
expressions inside defer bodies, computed goto with active defers. Every combination that could
silently misfire.
Every typedef, enum constant, VLA tag, and parameter
shadow — registered at all depths before a single byte of output is emitted.
size_t x; and size_t * x; are not the same thing. Prism knows that.
Prism compiles itself. Stage 1 and Stage 2 transpiled output is byte-for-byte identical — verified by CI on Linux x86_64, macOS x86_64/arm64, Linux arm64, and Linux riscv64. If it couldn't compile itself correctly, it couldn't ship.
Prism has a complete formal specification — every feature, every edge case, every invariant documented and cross-referenced against the test suite.
v1.1.1 · 5,400 tests · fully self-hosting · 14 invariants
Continuous integration across Linux, macOS, and Windows — x86_64, arm64, and riscv64.
Prism is built and maintained by one person, in the open, for free.
If it saves you time or makes your C safer, sponsoring is how you keep that going. Every sponsor directly funds time on new features, bug fixes, and keeping the test suite green.
Available for consulting work across design, branding, and engineering.
Compiler work, systems programming, C codebase hardening, dev tooling, or a visual identity for your project — send an email.