Thanks to visit codestin.com
Credit goes to github.com

Skip to content

dawnlarsson/prism

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

285 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Prism Banner

Robust C by default

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=prism in 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, ensuring defer fires exactly when it should.
  • Detect Errors: It catches unsafe patterns (like jumping into a scope with goto) before they become runtime bugs.

Quick Start

Linux / macOS

cc prism.c -flto -s -O3 -o /tmp/prism && /tmp/prism install && rm /tmp/prism

Windows (MSVC)

Open a Developer Command Prompt (or run vcvars64.bat) and build:

cl /Fe:prism.exe prism.c /O2 /D_CRT_SECURE_NO_WARNINGS /nologo

Requires 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_type in defer return values are not yet MSVC-compatible. Use CC=gcc or CC=clang as the backend compiler on Windows for full transpiled-code support.

Defer

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
  • switch fallthrough — defers don't double-fire between cases
  • Nested loops — break/continue unwind 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

Zero-Init

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.

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;
}

orelse

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.

Forms

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;

Works with any falsy value

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 = done

Limitation: struct/union values

orelse 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;  // Error

The 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 scalar

Opt-out: prism -fno-orelse src.c

Safety Enforcement

Prism acts as a static analysis tool, turning common C pitfalls into compile-time errors.

No Uninitialized Jumps

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'

Defer in Forbidden Contexts

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.

Downgrade to Warnings

Use -fno-safety to turn safety errors into warnings (for gradual adoption):

prism -fno-safety legacy.c  # Compiles with warnings instead of errors

Multi-File & Passthrough

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 mixed

Passthrough files: .s, .S (assembly), .cc, .cpp, .cxx, .mm (C++), .m (Objective-C) are passed directly to the compiler without transpilation.

Error Reporting

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)

CLI

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/prism

Drop-in Compiler Overlay

Prism can replace gcc or clang in any build system:

# Instead of:
CC=gcc make

# Use:
CC=prism make

All standard compiler flags (-O2, -Wall, -I, -L, -l, etc.) pass through automatically to the backend compiler.

Library Mode

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.o

API:

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.

Repo

Apache 2.0 license (c) Dawn Larsson 2026