← Prism
Docs
Overview defer orelse zeroinit raw auto-unreachable
Spec Draft
GitHub ↗

zeroinit

Every local variable starts at zero — automatically. No uninitialized reads, no garbage values, no undefined behavior from forgotten initialization.

Overview

Uninitialized local variables are the source of a persistent class of C bugs. The language permits them, -Wall only catches obvious cases, and flow analysis misses conditional paths. The result is undefined behavior that compilers can silently miscompile in ways that are extremely difficult to debug.

Prism eliminates the problem at the language level: every local variable is zero-initialized at its declaration point unless you explicitly opt out with raw.

Standard C — UB on some paths
int sum_positive(int *arr, int n) {
    int total;  // garbage value
    for (int i = 0; i < n; i++)
        if (arr[i] > 0)
            total += arr[i];
    // UB if no positives: total never set
    return total;
}
With Prism — always defined
int sum_positive(int *arr, int n) {
    int total;  // always 0
    for (int i = 0; i < n; i++)
        if (arr[i] > 0)
            total += arr[i];
    return total;  // returns 0 if no positives
}

Behavior

Prism inserts initializers before handing off to the backend compiler. Declarations you write get a zero added — you never see the transformed output unless you run prism transpile.

Your declarationEmitted as
int x;int x = 0;
char *p;char *p = 0;
double ratio;double ratio = 0;
struct Rect r;struct Rect r = {0};
int arr[64];int arr[64] = {0};
int arr[n]; (VLA)int arr[n]; memset(arr, 0, sizeof(arr));
typedef int Vec[n]; Vec v; (typedef VLA)Vec v; memset(v, 0, sizeof(v));
int x = 5; (already initialized)int x = 5; (unchanged)

For aggregates, = {0} zeroes all members recursively per C99 — including nested structs, arrays of structs, and pointer fields (which become NULL).

Examples

Accumulators and counters

// No need to write = 0 — Prism does it
int count;
size_t total_bytes;
double sum;

for (int i = 0; i < n; i++) {
    count += items[i].valid;
    total_bytes += items[i].size;
    sum += items[i].value;
}

Struct initialization

struct HttpRequest req;  // all fields zeroed: method=0, url=NULL, headers=NULL, ...
req.method = GET;
req.url = path;
// No memset() needed; all members are zero (padding may not be, see Edge Cases)

Pointer safety

char *result;  // NULL — safe to check before use
if (condition)
    result = compute();
if (result)  // always well-defined
    use(result);

Error code patterns

int rc;  // 0 = success — safe default
if (step_a() != 0) rc = -1;
if (step_b() != 0) rc = -1;
return rc;  // 0 if both succeeded

VLA zeroing

int n = get_size();
int buf[n];  // Prism emits: int buf[n]; memset(buf, 0, sizeof(buf));
process(buf, n);

Patterns & Tricks

Linked-list node initialization

Zero-initialization is critical for pointer-heavy data structures. Linked lists work correctly without explicit member zeroing:

struct Node {
    int value;
    struct Node *next;
};

struct Node *create_node(int val) {
    struct Node *n = malloc(sizeof *n);
    if (!n) return NULL;
    // n->next is already NULL (zero-initialized)
    n->value = val;
    return n;
}

In standard C, you'd need n->next = NULL; explicitly. With Prism, it's automatic.

Struct builder pattern

Zero-initialization enables a safe "builder" pattern where unspecified fields stay at their safe defaults:

struct Config config;  // all fields zero: timeout=0, flags=0, ptr=NULL, ...
config.timeout_ms = 5000;
config.flags = CONFIG_VERBOSE;
// Any unset field is safely zero — no garbage, no UB
apply_config(&config);

Without zero-initialization, you'd need to explicitly initialize every field or use = {0}.

Safe conditional initialization

Variables that might not be assigned on all paths default to zero — a safe value for many domains:

int result;  // 0 by default
if (condition1)
    result = expensive_compute();
else if (condition2)
    result = cheap_compute();
// If neither condition is true, result is still 0 — not garbage
log_result(result);

This eliminates an entire class of latent bugs where a variable might be used uninitialized.

Exclusions

Zeroinit applies only to local variable declarations inside function bodies. It does not touch:

Edge Cases

Typedef-hidden types

Prism builds a complete symbol table in Pass 1 to distinguish declarations from expressions. size_t x; is a declaration and gets zero-initialized. size_t * x; could be a multiplication expression — Prism uses the symbol table to correctly identify it as a pointer declaration and initialize it too.

VLAs and goto

Jumping past a VLA declaration is always a hard error — skipping a VLA declaration bypasses its implicit stack allocation, regardless of whether it's zero-initialized or marked raw. This is enforced by the CFG verifier (Phase 2A).

Aggregate zero vs memset

= {0} guarantees all struct members are zero per ISO C99. However, C does not guarantee padding bytes are zeroed — only initialized members. In practice, GCC and Clang both emit code that zeroes padding (via memset-equivalent sequences), but the C standard allows uninitialized padding bytes. If you need byte-exact zeroing before passing a struct to the kernel via copy_to_user, DMA, or other sensitive contexts, use raw + explicit memset(&s, 0, sizeof(s)) instead:

// Safe for kernel boundary:
raw struct ioctl_args args;
memset(&args, 0, sizeof args);  // byte-exact zero including padding
args.cmd = MY_CMD;
return ioctl(fd, ..., &args);

Global opt-out: prism -fno-zeroinit src.c disables zero-initialization for the entire file. For per-variable opt-out, use raw.

Spec Reference

zeroinit — Prism Spec §6.3