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.
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;
}
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 declaration | Emitted 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:
- File-scope globals — C already guarantees these are zero-initialized by the loader
- Static locals (
static int x;) — initialized once at startup per C semantics; emitting= 0would move them from.bssto.data - extern declarations — not a definition, no storage allocated
_Thread_local/thread_local— initialized once per thread by the runtime- Struct/union field declarations — inside a struct body, not local variables
- Variables declared with
raw— explicitly opted out - Already-initialized declarations (
int x = 5;) — not touched
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.