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 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);
Prepend prism check to the analyzer you already run. Source
arguments are transpiled to standard C behind the scenes, every other flag passes through
untouched, the tool's exit code is preserved, and #line directives map each
finding back to your original line numbers.
Analyzing the expansion beats analyzing the source: the analyzer sees the real
control flow, cleanup included. A manual fclose(f) before an early return
alongside defer fclose(f); comes back as doubleFree. No
source-level C analyzer catches that, because none of them parse defer.
prism check cppcheck --enable=all src.c
// src.c:12:4: error: Array 'arr[8]' accessed
// at index 8, out of bounds
// The finding cites src.c line 12, not a line
// in the generated artifact.
FILE *f = fopen(path, "r")
orelse return -1;
defer fclose(f);
if (bad) {
fclose(f); // then defer runs too
return -1;
}
// src.c:14:9: error: doubleFree: Resource
// handle 'f' freed twice
// Anything after the tool name is verbatim
prism check clang-tidy src.c -- -I include
// Prism flags before `check` still apply
prism -fno-zeroinit check cppcheck src.c
// Artifact is shaped for analysis: headers stay
// unflattened, subscripts stay bare, so the tool
// sees arr[8] and not a bounds-check wrapper.
// Your shipping build keeps both features on.
Not happy-path coverage. Edge cases, degenerate control flow, and patterns specifically designed to break a transpiler.
Prism is open source under Apache 2.0. Read every line, fork it, ship it. No strings.
OpenSSL, SQLite, Bash, Curl, GNU Coreutils, and Make, compiled unmodified through Prism.
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 any output is emitted.
size_t x; and size_t * x; are not the same thing. Prism knows that.
Prism compiles itself, four stages deep. stage0 through stage3 produce byte-identical transpiled output: 1,058,598 bytes, the same 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 formal specification: every feature, edge case, and invariant documented and cross-referenced against the test suite.
v1.1.5 · 13,685 tests · self-hosting · 8 invariants
Continuous integration across Linux, macOS, and Windows on 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.