A dialect of C with defer, orelse, and automatic zero-initialization.
Prism is a lightweight and very fast transpiler that makes C safer without changing how you write it.
- 1779 tests — edge cases, control flow, nightmares, trying hard to break Prism
- Building Real C — OpenSSL, SQLite, Bash, GNU Coreutils, Make, Curl
- Proper transpiler — tracks typedefs, respects scope, catches unsafe patterns
- Opt-out features — Disable parts of the transpiler, like zero-init, with CLI flags
- Drop-in overlay — Use
CC=prismin any build system — GCC-compatible flags pass through automatically - Single Repo — 7k lines, zero dependencies, easy to audit
Prism is a proper transpiler, not a preprocessor macro.
- Track Types: It parses
typedefs to distinguish pointer declarations from multiplication (the "lexer hack"), ensuring correct zero-initialization. - Respect Scope: It understands braces
{}, statement expressions({ ... }), and switch-case fallthrough, ensuringdeferfires exactly when it should. - Detect Errors: It catches unsafe patterns (like jumping into a scope with
goto) before they become runtime bugs.
cc prism.c -flto -s -O3 -o /tmp/prism && /tmp/prism install && rm /tmp/prismOpen a Developer Command Prompt (or run vcvars64.bat) and build:
cl /Fe:prism.exe prism.c /O2 /D_CRT_SECURE_NO_WARNINGS /nologoRequires Visual Studio Build Tools with the Desktop development with C++ workload.
Note: Prism builds and runs natively on Windows. The transpiler output is GCC/Clang-first — features like
__auto_typeindeferreturn values are not yet MSVC-compatible. UseCC=gccorCC=clangas the backend compiler on Windows for full transpiled-code support.
The problem: C requires manual cleanup at every exit point. Each new resource adds cleanup to every error path. Miss one and you leak.
// Standard C — cleanup grows with every new resource
int compile(const char *path) {
FILE *f = fopen(path, "r");
if (!f) return -1;
char *src = read_file(f);
if (!src) {
fclose(f);
return -1;
}
Token *tok = tokenize(src);
if (!tok) {
free(src);
fclose(f);
return -1;
}
Node *ast = parse(tok);
if (!ast) {
token_free(tok);
free(src);
fclose(f);
return -1;
}
int result = emit(ast);
node_free(ast); // remember all four
token_free(tok); // in the right order
free(src); // or you leak
fclose(f); // every single time
return result;
}With Prism: Write cleanup once. It runs on every exit.
int compile(const char *path) {
FILE *f = fopen(path, "r");
if (!f) return -1;
defer fclose(f);
char *src = read_file(f);
if (!src) return -1; // fclose runs
defer free(src);
Token *tok = tokenize(src);
if (!tok) return -1; // free, fclose run
defer token_free(tok);
Node *ast = parse(tok);
if (!ast) return -1; // token_free, free, fclose run
defer node_free(ast);
return emit(ast); // all four, reverse order
}It is better, but we can take it further, see orelse section.
Defers execute in LIFO order (last defer runs first) at scope exit — whether via return, break, continue, goto, or reaching }.
Edge cases handled:
- Statement expressions
({ ... })— defers fire at inner scope, not outer switchfallthrough — defers don't double-fire between cases- Nested loops —
break/continueunwind the correct scope
Forbidden patterns: Functions using setjmp/longjmp, vfork, or inline assembly are rejected to prevent resource leaks from non-local jumps.
Opt-out: prism -fno-defer src.c
The problem: Uninitialized reads are the #1 source of C vulnerabilities. Compilers don't require initialization, and -Wall only catches obvious cases.
// Standard C — compiles fine, undefined behavior at runtime
int sum_positive(int *arr, int n) {
int total; // uninitialized — could be anything
for (int i = 0; i < n; i++)
if (arr[i] > 0) total += arr[i];
return total; // UB: total was never set if no positives
}With Prism: All locals start at zero. The above code just works.
void example() {
int x; // 0
char *ptr; // NULL
int arr[10]; // {0, 0, ...}
struct { int a; float b; } s; // {0, 0.0}
}Typedef tracking: Prism parses headers to recognize size_t, uint8_t, FILE *, pthread_mutex_t, etc. This distinguishes size_t x; (declaration → initialize) from size_t * x; (expression → don't touch).
VLA support: Variable-length arrays get memset at runtime.
Opt-out: prism -fno-zeroinit src.c or per-variable with raw.
The raw keyword opts out of zero-initialization for a specific variable.
void example() {
raw int x; // Uninitialized
raw char buf[65536]; // No memset overhead
raw struct large data; // Skip zeroing
}When to use:
- Large buffers that will be immediately overwritten (
read(),recv()) - Performance-critical inner loops where zeroing is measurable overhead
- Interfacing with APIs that fully initialize the data
Safety interaction: Variables marked raw can be safely jumped over by goto — since they're not initialized anyway, skipping them isn't undefined behavior.
void allowed() {
goto skip;
raw int x; // OK: raw opts out of initialization
skip:
return;
}The orelse keyword handles failure inline — check a value and bail in one line.
defer solved the cleanup problem, but notice the function still has a repetitive pattern: call, null-check, bail. Four times. orelse collapses each check-and-bail into the declaration itself:
int compile(const char *path) {
FILE *f = fopen(path, "r") orelse return -1;
defer fclose(f);
char *src = read_file(f) 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);
}Same function, three versions: 32 lines → 19 lines → 15 lines. No cleanup bugs, no null-check boilerplate.
orelse checks if the initialized value is falsy (null pointer, zero). If so, the action fires. All active defers run — just like a normal return.
Control flow — return, break, continue, goto:
int *p = get_ptr() orelse return -1;
int *q = next() orelse break;
int *r = try_it() orelse continue;
int *s = find() orelse goto cleanup;Block — run arbitrary code on failure:
FILE *f = fopen(path, "r") orelse {
log_error("failed to open %s", path);
return -1;
}Fallback value — substitute a default:
char *name = get_name() orelse "unknown";Bare expression — check without assignment:
do_init() orelse return -1;orelse isn't limited to pointers — it works with any type where !value is meaningful:
int fd = open(path, O_RDONLY) orelse return -1; // 0 is falsy
size_t n = read_data(fd, buf) orelse break; // 0 bytes = doneorelse does not currently support struct or union values — it is a compile error:
struct Vec2 { int x, y; };
struct Vec2 v = make_vec2() orelse return -1; // ErrorThe reason: orelse works by testing !value, which is well-defined for scalars and pointers but not for structs. A whole-struct zero check would require memcmp, which can give false negatives due to padding bytes.
Struct and union pointers work fine:
struct Vec2 *p = get_vec2() orelse return -1; // OK — pointer is scalarOpt-out: prism -fno-orelse src.c
Prism acts as a static analysis tool, turning common C pitfalls into compile-time errors.
Standard C allows goto to skip variable initialization, leading to undefined behavior. Prism performs a single-pass dominator analysis to forbid this:
// THIS WILL FAIL TO COMPILE
void unsafe() {
goto skip;
int x; // Prism guarantees x is zero-initialized
skip:
printf("%d", x);
}
// Error: goto 'skip' would skip over variable declaration 'x'Prism rejects defer in functions that use non-local control flow:
void bad() {
jmp_buf buf;
defer cleanup(); // Error: defer forbidden with setjmp
if (setjmp(buf)) return;
}This prevents resource leaks when longjmp bypasses defer cleanup.
Use -fno-safety to turn safety errors into warnings (for gradual adoption):
prism -fno-safety legacy.c # Compiles with warnings instead of errorsPrism handles real-world build scenarios:
# Multiple source files
prism main.c utils.c -o app
# Mix with assembly
prism main.c boot.s -o kernel
# C++ files pass through untouched (uses g++/clang++ automatically)
prism main.c helper.cpp -o mixedPassthrough files: .s, .S (assembly), .cc, .cpp, .cxx, .mm (C++), .m (Objective-C) are passed directly to the compiler without transpilation.
Prism emits #line directives so compiler errors point to your original source, not the transpiled output:
main.c:42:5: error: use of undeclared identifier 'foo'
Not:
/tmp/prism_xyz.c:1847:5: error: use of undeclared identifier 'foo'
Disable: prism -fno-line-directives src.c (useful for debugging transpiler output)
Prism uses a GCC-compatible interface — most flags pass through to the backend compiler.
Prism v0.116.0 - Robust C transpiler
Usage: prism [options] source.c... [-o output]
Commands:
run <src.c> Transpile, compile, and run
transpile <src.c> Output transpiled C to stdout
install [src.c...] Install prism to /usr/local/bin/prism
Prism Flags (consumed, not passed to CC):
-fno-defer Disable defer
-fno-zeroinit Disable zero-initialization
-fno-orelse Disable orelse keyword
-fno-line-directives Disable #line directives
-fno-safety Safety checks warn instead of error
-fflatten-headers Flatten headers into single output
-fno-flatten-headers Disable header flattening
--prism-cc=<compiler> Use specific compiler
--prism-verbose Show commands
All other flags are passed through to CC.
Examples:
prism foo.c -o foo Compile (GCC-compatible)
prism run foo.c Compile and run
prism transpile foo.c Output transpiled C
prism -O2 -Wall foo.c -o foo With optimization
CC=clang prism foo.c Use clang as backend
Apache 2.0 license (c) Dawn Larsson 2026
https://github.com/dawnlarsson/prismPrism can replace gcc or clang in any build system:
# Instead of:
CC=gcc make
# Use:
CC=prism makeAll standard compiler flags (-O2, -Wall, -I, -L, -l, etc.) pass through automatically to the backend compiler.
Prism can be compiled as a library for embedding in other tools:
# Compile as library (excludes CLI)
cc -DPRISM_LIB_MODE -c prism.c -o prism.oAPI:
PrismFeatures prism_defaults(void);
PrismResult prism_transpile_file(const char *path, PrismFeatures features);
void prism_free(PrismResult *r);Note: Library mode is currently limited and not thread-safe. The transpiler uses global state internally. Call from a single thread only, or protect with a mutex.
Apache 2.0 license (c) Dawn Larsson 2026