A dialect of C with defer, orelse, automatic zero-initialization, bounds checking, and progressive optimization.
Prism is a transpiler that makes C safer and faster without changing how you write it. One source file, no dependencies beyond a C compiler.
- 46,261+ tests: edge cases, control flow, nightmares, fuzz, trying hard to break Prism
- Building Real C: OpenSSL, SQLite, Bash, GNU Coreutils, Make, Curl
- Two-pass transpiler: full semantic analysis before a single byte is emitted
- Progressive optimization: auto-unreachable after noreturn calls, const arrays promoted to static storage
- Opt-out features: Disable parts of the transpiler, like zero-init or bounds-check, with CLI flags
- Drop-in overlay: Use
CC=prismin any build system. GCC-compatible flags pass through automatically - Single Repo: zero dependencies, easy to audit, only need a C compiler
Prism is a proper transpiler, not a preprocessor macro.
- Track Types: Pass 1 walks every token at every depth, registering every
typedef,enumconstant, parameter shadow, and VLA tag into an immutable symbol table, no heuristics, no suffix guessing. If it wasn't declared, it's not a type - Respect Scope: A full scope tree maps every
{/}pair with parent links and classification (loop, switch, conditional, function body, statement expression).deferfires exactly when it should, no state machines in the emitter - Detect Errors Early: A CFG verifier checks every
goto→label andswitch→casepair against defers and declarations before code generation starts. If your code is unsafe, Prism errors before writing a single byte
cc prism.c -flto -s -O3 -o prism && ./prism installOpen a Developer Command Prompt or run vcvars64.bat, then 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.
Real codebases written using Prism as the compiler.
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 }.
For a braceless defer, the body is one complete statement: defer if (ok) a(); else b(); captures the entire if/else, not only its first branch.
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 - Computed goto:
goto *ptrwith active defers is a hard error
Forbidden patterns: Functions using setjmp/longjmp/pthread_exit, vfork, or asm goto are rejected to prevent resource leaks from non-local jumps. Regular inline assembly is fine.
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: Before code generation, Pass 1 walks the entire preprocessed token stream at all depths (not just file scope) to build a complete, immutable symbol table of every typedef, enum constant, parameter shadow, and VLA tag. This is deterministic: size_t, pthread_mutex_t, and every other typedef from system headers are resolved by name lookup, not pattern matching. 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. Exception: raw on a VLA does not exempt it from the goto check, because jumping past a VLA bypasses implicit stack allocation regardless of initialization.
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;
}; // the declaration still needs its terminating ';'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 scalarNote: Prism detects struct/union types through explicit keywords (
struct S,union U) and through typedefs that alias aggregates. However, whentypeof()is applied to an opaque expression (a variable name or function call whose type cannot be determined from tokens alone), Prism cannot detect the aggregate nature:struct S make(void); typeof(make()) v = make() orelse return -1; // Passes Prism, fails at CCThe backend compiler catches these with a clear error such as "wrong type argument to unary '!'" or "used type where arithmetic type is required".
Opt-out: prism -fno-orelse src.c
Prism acts as a static analysis tool, turning common C pitfalls into compile-time errors, all before a single byte of output is emitted.
Standard C allows goto to skip variable initialization, leading to undefined behavior. Prism's CFG verifier checks every goto→label pair in an O(N) linear sweep:
// 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 this variable declaration (bypasses initialization)The same analysis covers switch/case, jumping from one case into a nested block that has zero-initialized declarations or active defers is rejected:
void bad_switch(int n) {
switch (n) {
case 1: {
defer cleanup();
case 2: // Error: defer skipped by switch fallthrough
break;
}
}
}The label has to sit inside the block for this to be a hazard. Closing the block before the next case is accepted, because the defer has already fired:
void ok_switch(int n) {
switch (n) {
case 1: {
defer cleanup(); // runs at this '}'
}
case 2:
break;
}
}A bare defer directly under a case label is rejected separately, with
defer in switch case requires braces.
Prism rejects defer in functions that use non-local control flow:
void bad() {
jmp_buf buf;
defer cleanup(); // Error: defer cannot be used in functions that call setjmp/longjmp/pthread_exit
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 is not just a transpiler that adds explicit features. It is a progressive enhancement engine for standard C. Your binaries get smaller and faster automatically, without changing a single line of source.
Prism tracks _Noreturn, [[noreturn]], __attribute__((noreturn)), and standard library exit functions (exit, abort, _Exit, quick_exit) across your entire translation unit, including through transitive call chains. After every call to one of these functions, Prism silently injects __builtin_unreachable() (or __assume(0) on MSVC).
void fatal(const char *msg) __attribute__((noreturn));
int process(int *data) {
if (!data) fatal("null pointer");
// Without Prism: compiler doesn't know fatal() never returns.
// It emits a branch, preserves registers, generates a dead path.
// With Prism: __builtin_unreachable() tells the backend this path is dead.
// The compiler eliminates the dead code and optimizes the live path.
return data[0] + data[1];
}This feeds explicit control-flow termination data to the backend compiler, enabling:
- Dead-code elimination: unreachable paths after noreturn calls are removed entirely
- Smaller functions: unnecessary epilogues and stack cleanup are dropped
- Better register allocation: the compiler knows which paths are live
- Improved branch prediction: fewer branches means fewer mispredictions
The optimization propagates transitively: if wrapper() calls fatal(), and Prism sees that wrapper always exits via a noreturn path, callers of wrapper benefit too.
Opt-out: prism -fno-auto-unreachable src.c
Prism automatically promotes const arrays with literal initializers from stack to static storage:
void encrypt(uint8_t *block) {
const uint8_t sbox[8] = { 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5 };
// Without Prism: the table is copied to the stack on every call.
// With Prism: emitted as 'static const', read directly from .rodata.
block[0] = sbox[block[0] & 7];
}prism transpile shows the promotion: the emitted declaration reads
static const uint8_t sbox[8]. A 256-entry AES table costs 256 stack bytes and
a copy per call without it.
This eliminates hidden memcpy calls for cryptographic tables, lookup arrays, dispatch tables, and string constant arrays.
The transformation is conservative. It only fires when all of these hold:
- Block-scope
constarray with brace-enclosed initializer - Every initializer token is a literal, enum constant, or designator
- No
volatile,static,extern,register,_Thread_local, orconstexpr - No VLA dimensions, no
orelse, no attributes on the declarator - For pointer arrays (
const int *arr[3]), the array itself must beconst, i.e.const int * const arr[3]
Opt-out: prism -fno-auto-static src.c
Prism wraps array subscripts with a runtime bounds check, turning silent buffer overflows into immediate traps. This is on by default. Prism's philosophy is opt-out, not opt-in: you chose Prism for safety, not to configure it.
void process(int i, int j, int k, int m) {
int arr[100];
int n = get_count();
int vla[n];
arr[i] = 5; // traps if i >= 100
vla[j] = 7; // traps if j >= n
int x = arr[k]; // initializers are checked too
use(arr[m]); // so are function-call arguments
}Each wrapped subscript becomes:
arr[__prism_bchk((__prism_bchk_size_t)(i), sizeof(arr)/sizeof(arr[0]))]The sizeof ratio gives the correct length for both fixed arrays (compile-time constant) and VLAs (runtime, per C99 §6.5.3.4) with no transpile-time size tracking. If the index is out of range, the inline helper calls __builtin_trap() (GCC/Clang) or __debugbreak() + abort() (MSVC). The unsigned cast also catches negative indices: they wrap to huge values and fail the check. The helper avoids any #include so it works both with flattened (already-preprocessed) output and with system-header-retaining output.
What's checked:
- Fixed-size local arrays:
int arr[100]; arr[i] - Local VLAs:
int vla[n]; vla[i] - File-scope and block-scope
staticarrays - Every dimension of a multi-dim array:
m[i][j]checks bothiandj, andm[i][j][k]checks all three - Nested subscripts in index expressions: both outer and inner are wrapped in
arr[m[i]] - Declaration initializers and function-call arguments
What's intentionally NOT wrapped (to avoid false positives or false negatives):
- Unevaluated operands:
sizeof(arr[i]),_Alignof(arr[i]),typeof(arr[i]),offsetof(T, arr[i]),__builtin_offsetof(T, arr[i]), and the controlling expression of_Generic(arr[i], ...): operand is not evaluated; an offsetof subscript refers to a struct field whose size is unrelated to any same-named local - Struct/union member subscripts:
s.arr[i],p->arr[i]: field size ≠ any same-named local - Unary address-of:
&arr[i]: C permits one-past-end addresses (index == length is legal) - Pointer subscripts (
p[i]wherepisint *) - Derived array-pointer bases whose one true extent cannot be proven, such as
(a + i)[0],i[a], or(pick ? a : b)[i]. Strict safety mode diagnoses these rather than attaching one array's bound to another;-fno-safetyleaves valid host C unwrapped. - Array parameters:
void f(int a[4])declares a pointer, not an array (C11 §6.7.6.3p7), so there is no length to check against.int a[static 4]is checked: C promises at least 4 elements, and that promise is the bound raw { ... }blocks (Prism transformations are fully suppressed)
The check is a single predicted-not-taken branch per subscript; the backend compiler constant-folds the sizeof ratio for fixed arrays and often eliminates the whole check when it can prove the index is in range.
Pointer-to-array dereference is checked on the dimension the type carries:
int (*p)[4] bounds (*p)[i] and p[0][i] against the [4], whether p is a
local or a parameter. The array is what the pointer points at, so the extent
survives the decay that leaves ordinary array parameters unbounded. The pointer
hop itself is not checked — nothing says how many arrays p points at — so
p[i] is left alone, and int (*p)[n] carries no constant to check against.
Opt-out: prism -fno-bounds-check src.c
Prism 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 shells out to cc -E before transpiling, and that spawn dominates its
runtime: 39% to 99% of wall time, depending on how much the file includes. Run
prism --prism-prof transpile file.c to see the split on your own source. On a
four-line file that includes stdio.h, stdlib.h & string.h, the cold
numbers are preprocess=14.090ms against total=15.632ms, or 90% of the run.
Prism caches the preprocessor's output and skips the spawn whenever nothing it
read has changed. The same file warm reports preprocess=0.097ms total=1.177ms.
End to end, including the backend compile, that is a 2x speedup on a warm
cache.
It is on by default and needs no configuration.
prism --prism-cache-info # location, entry count, size, limits
prism --prism-cache-clear # delete every cached entry| variable | default | meaning |
|---|---|---|
PRISM_NO_PP_CACHE=1 |
off | disable the cache entirely |
PRISM_PP_CACHE_DIR |
$XDG_CACHE_HOME/prism-pp |
cache location |
PRISM_PP_CACHE_MAX_MB |
1024 |
size cap; oldest entries evicted first |
PRISM_PP_CACHE_MAX_DAYS |
14 |
age cap |
How an entry is invalidated. The cache key covers the exact preprocessor argv, the resolved compiler binary's size and timestamp, and the include-affecting environment (CPATH, SDKROOT, and the rest), so upgrading your compiler or changing a flag misses. An entry is only reused if every file that contributed to it still has the same size, mtime (to nanosecond resolution where the platform provides it) and ctime. That dependency list is recovered from the # N "file" linemarkers in the preprocessed output itself, so editing any transitive header invalidates the entry without prism needing a .d sidecar.
Every uncertainty resolves to a miss rather than a hit: unresolvable paths, filesystems too coarse to distinguish a same-second rewrite, and sources mentioning __DATE__, __TIME__ or __TIMESTAMP__ (whose expansion is not a function of the inputs) are never cached.
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)
Debuggers show your original Prism source, not the transpiled C. The same
#line directives that drive error reporting also drive the DWARF line tables
the backend compiler emits under -g, so lldb/gdb resolve everything to your
.c file:
$ prism -g -O0 app.c -o app && lldb app
(lldb) b app.c:10 # breakpoints by original file:line
(lldb) run
frame #0: app`load(id=1) at app.c:10:5
-> 10 if (id == 2) return -2;
(lldb) p buf[0] # your variable names, untouched
(char) 'B'
Source listings render the original file (defer and orelse lines included)
and backtraces cite original lines (load at app.c:10, main at app.c:17).
defer makes debugging better, not worse: a breakpoint on a defer body line
binds to every exit path's copy of the cleanup and fires exactly when the
cleanup runs. Hit it on an early return and the backtrace shows you which
exit triggered it. Stepping over a return walks through the pending defer
bodies at their original lines, in LIFO order, before leaving the function.
Prism-generated temporaries are namespaced __prism_* and easy to ignore in
frame variable. To inspect the generated C itself, prism transpile prints
it, and -fno-line-directives makes the debugger show the transpiled lines
instead, useful when debugging Prism's own output.
prism check wraps any analyzer the same way CC=prism wraps your compiler:
prepend it to the command you already run. Source args are transpiled to
standard-C artifacts behind the scenes; everything else passes through verbatim,
the tool's exit code is preserved, and the emitted #line directives map every
finding back to your original source lines:
prism check cppcheck --enable=all src.c
# → src.c:12:4: error: Array 'arr[8]' accessed at index 8, out of bounds
prism check clang-tidy src.c -- -I includecheck shapes the artifact for analysis automatically: #include lines stay
intact (no header flattening) and subscripts stay bare so the analyzer sees
arr[8] rather than a runtime bounds-check wrapper. Your shipping build keeps
both features on. This only affects the analysis artifact. Prism flags before
check still apply (e.g. prism -fno-zeroinit check cppcheck …).
The manual equivalent, if you need the artifact itself:
prism transpile -fno-flatten-headers -fno-bounds-check src.c > build/src.c
cppcheck --enable=all build/src.cAnalyzing the expansion is strictly stronger than analyzing the source:
the analyzer sees the real control flow including defer cleanup. A manual
fclose(f) before an early return alongside defer fclose(f); is reported by
CppCheck as doubleFree: Resource handle 'f' freed twice, a bug class no
source-level C analyzer can see, because none of them understand defer.
Raw Prism sources (files using orelse) stop CppCheck at the first keyword with
an unknownMacro configuration error, so point analyzers at the transpiled
artifact. Files that use no Prism keywords are plain C and analyze as-is. Note:
automatic zero-init may surface as redundantAssignment at style level when you
immediately overwrite a variable. Suppress that id or read it as intentional.
Prism uses a GCC-compatible interface. Most flags pass through to the backend compiler.
Prism v1.1.8 - Robust C transpiler
Usage: prism [options] source.c... [-o output]
prism [options] run src.c [-- prog_args...]
Commands:
run <src.c> [-- args] Transpile, compile, and run (args passed to binary)
transpile <src.c> Output transpiled C to stdout
check <tool> [args] Run a static analyzer (cppcheck, clang-tidy, ...) on
transpiled sources; .c/.i args are swapped for analysis
artifacts, findings map to original lines via #line
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
-fno-auto-unreachable Disable __builtin_unreachable after noreturn calls
-fno-auto-static Disable auto-static for const arrays with literal inits
-fno-bounds-check Disable runtime bounds checks on local, static and file-scope array subscripts
-fno-link-pragma Ignore #pragma link directives in source
(each -fno-X above also accepts -fX to re-enable it)
--prism-cc=<compiler> Use specific compiler
--prism-verbose Show commands
--prism-prof Print per-phase timing breakdown
--prism-verify Translation validation: re-transpile emitted C,
require a fixed point (also: PRISM_VERIFY env)
--prism-cache-info Show the preprocessor cache location and size
--prism-cache-clear Delete all cached preprocessor output
--prism-emit[=<file>] Write transpiled C to stdout, or to <file>
-- Separator: remaining args are passed to the binary in `run` mode
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 -O2 run foo.c -- arg1 arg2 Compile and run with program args
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
Link Pragma (source-embedded linker flags):
#pragma link <platform> <names...>
platform: * | macos | macos_arm64 | macos_x86_64 | linux | linux_arm64
linux_x86_64 | linux_riscv64 | windows | windows_x86_64 | windows_arm64
name: plain name (e.g. `Cocoa`, `m`): macOS => -framework, else -l<name>
or a literal flag starting with `-` (e.g. `-lm`, `-framework Foo`)
Apache 2.0 license (c) Dawn Larsson 2026
https://github.com/dawnlarsson/prismprism --prism-emit src.c writes the transpiled C to stdout, the same output
prism transpile produces. prism --prism-emit=out.c src.c writes it to
out.c instead.
Prism 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);
PrismResult prism_transpile_source(const char *source, const char *filename,
PrismFeatures features); // pre-preprocessed input
void prism_free(PrismResult *r);
void prism_reset(void); // reclaim arenas (automatic on error)
void prism_thread_cleanup(void); // free thread-locals before thread exitavailable for consulting work, (design, branding, engineering / software)
Apache 2.0 license (c) Dawn Larsson 2026
