fastC: Designing a Memory-Safe C Dialect for AI-Generated Code
LLM agents write systems code, but C is unsafe and Rust is hard to generate. fastC explores the middle path.
There is a growing mismatch at the heart of AI-assisted programming. Large language models are increasingly capable of generating systems code — kernel modules, device drivers, database engines, network stacks. The natural target language for this kind of code is C. But C is, by any modern standard, catastrophically unsafe. Buffer overflows, use-after-free, double-free, null pointer dereferences, undefined behaviour on signed integer overflow — C hands you a loaded gun and trusts you not to point it at your foot.
Humans have been shooting themselves with this particular gun for fifty years and have developed institutional knowledge to mostly avoid it. LLMs have not.
The obvious alternative is Rust. Rust eliminates the entire class of memory safety bugs through its ownership and borrowing system. But here we encounter the other side of the mismatch: LLMs are remarkably bad at generating Rust code that compiles. The borrow checker enforces subtle invariants about lifetimes and aliasing that current models struggle to satisfy. They produce code that is conceptually correct but syntactically or semantically wrong in ways that require deep understanding of the type system to fix.
fastC is our attempt to explore the middle ground. It is a restricted dialect of C in which the compiler enforces safety invariants that are simple enough for an LLM to satisfy, while preserving the performance characteristics and mental model that make C the natural choice for systems programming.
The Problem in Detail
To understand why this problem matters, consider what happens when an LLM agent is tasked with writing a memory allocator, a system call handler, or a packet parser. These are tasks that increasingly fall within the capabilities of frontier models.
In C, the agent generates code that compiles — the C compiler is permissive by design — but the resulting binary may contain critical vulnerabilities. In our internal testing, we found that LLM-generated C code compiled successfully approximately 92% of the time but contained at least one memory safety violation in roughly 37% of cases. The violations were not exotic: buffer overflows on array access, use-after-free on error paths, missing null checks after malloc.
In Rust, the agent generates code that is memory-safe by construction — if it compiles. But in our testing, LLM-generated Rust code compiled successfully only about 58% of the time. The failures were predominantly borrow checker errors: moved values used after move, mutable borrows conflicting with immutable borrows, lifetime annotations that the model could not infer correctly.
This creates an unpleasant choice. You can have code that compiles but crashes, or code that is safe but does not compile. Neither is acceptable for autonomous AI agents that need to write, compile, and execute systems code without human intervention.
The fastC Approach
fastC takes a different path. Rather than inventing a new language or trying to make LLMs better at Rust, we restrict C to a subset where the compiler can enforce safety without requiring the programmer (or the LLM) to express complex invariants.
The core insight is that most memory safety violations in C arise from a small number of patterns:
- Unbounded array access: Writing past the end of a buffer.
- Use-after-free: Accessing memory that has been deallocated.
- Double-free: Deallocating the same memory twice.
- Null dereference: Using a pointer without checking whether it is null.
- Uninitialised memory: Reading from memory that was never written to.
fastC addresses each of these through language restrictions and compiler-inserted checks.
Bounded Arrays
In fastC, all arrays carry their length. The syntax is familiar C:
int data[256];
But the compiler transforms every array access into a bounds-checked access. The check is not a runtime library call — it is an inline comparison and branch that the CPU’s branch predictor handles efficiently after the first few iterations. In our measurements, the overhead is typically under 3% for sequential access patterns and under 8% for random access.
Critically, the bounds information is part of the type. A function that takes an int[] parameter also receives the length, and the compiler verifies that all accesses within the function are in-bounds. This is invisible to the programmer — and to the LLM.
Region-Based Memory Management
fastC replaces malloc/free with a region-based allocation model. Memory is allocated within named regions, and entire regions are freed at once:
region r = region_create();
int *p = region_alloc(r, sizeof(int) * 100);
int *q = region_alloc(r, sizeof(int) * 200);
// ... use p and q ...
region_destroy(r); // frees both p and q
This eliminates use-after-free and double-free by construction. You cannot free an individual pointer — only an entire region. And the compiler tracks region lifetimes: if you try to use a pointer after its region has been destroyed, the compiler rejects the program.
The trade-off is granularity. You cannot free a single object within a region. This can increase peak memory usage for long-running programs with mixed allocation lifetimes. For the use cases we target — short-lived systems tasks executed by AI agents — this is an acceptable cost.
Non-Nullable Pointers by Default
In fastC, pointers are non-nullable by default. A plain int *p is guaranteed by the compiler to be non-null at every point where it is used. If you need a nullable pointer, you annotate it explicitly:
int *? p = may_return_null();
if (p != NULL) {
// inside this block, p is treated as non-nullable
*p = 42;
}
The compiler uses dataflow analysis to track nullability through branches. This is a well-understood technique — Kotlin, Swift, and TypeScript all do something similar. The novelty is applying it to a C-like language targeting systems programming.
Zero Initialisation
All local variables in fastC are zero-initialised. This eliminates the class of bugs where uninitialised stack memory leaks sensitive data or causes undefined behaviour. The performance cost is measurable but small — modern CPUs can zero a cache line in a single cycle.
Compiler Architecture
The fastC compiler is itself written in Rust. It consists of four stages:
-
Lexer and parser: A hand-written recursive descent parser that produces an AST. We chose hand-written parsing over a parser generator to give precise, LLM-friendly error messages. When the compiler rejects a program, the error message should be actionable by the LLM that generated the code.
-
Semantic analysis: This stage performs type checking, region lifetime analysis, nullability tracking, and bounds checking. It operates on the AST and produces an annotated IR (intermediate representation). The key data structures are a region lifetime graph and a nullability lattice.
-
Safety check insertion: This stage walks the IR and inserts runtime safety checks where static analysis cannot prove safety. For example, if a bounds check can be statically proven (the index is a constant less than the array length), no runtime check is inserted. If the index is a variable, a runtime check is inserted.
-
Code generation: The final stage emits LLVM IR, which is then compiled to native code by the LLVM backend. This gives us access to LLVM’s optimisation passes, including the ability to hoist or eliminate bounds checks that the optimiser can prove redundant.
What fastC Is Not
fastC is not a general-purpose replacement for C. It is designed for a specific use case: AI-generated systems code that needs to be safe without requiring the generating model to reason about complex ownership invariants.
fastC does not prevent all bugs. It prevents memory safety bugs. An LLM can still generate code with logic errors, algorithmic bugs, or performance problems. fastC does not address concurrency safety — there is no borrow checker for shared mutable state. If you need concurrent memory safety, you should use Rust.
fastC does not support all of C. The following features are deliberately excluded:
- Pointer arithmetic: You cannot increment a pointer. Use array indexing instead.
- Casts between pointer types: Reinterpret casts are a major source of type confusion bugs.
- goto: Control flow must be structured.
- Variable-length arrays on the stack: These interact badly with bounds checking and can cause stack overflows.
- Union types: These allow type punning, which undermines the type system.
Each of these restrictions eliminates a class of bugs at the cost of expressiveness. For the systems code that LLMs typically generate — parsers, serialisers, network protocol handlers, data structure implementations — these restrictions are rarely binding.
Early Results and Open Questions
We have tested fastC with several frontier LLMs by asking them to generate common systems programming tasks: a hash table, a ring buffer, a simple HTTP parser, a binary search tree.
The results are encouraging. LLM-generated fastC code compiles successfully approximately 89% of the time — comparable to plain C and dramatically better than Rust. Of the code that compiles, 0% contains memory safety violations, by construction. The runtime performance overhead compared to equivalent unsafe C is between 2% and 11%, depending on the workload.
Several open questions remain:
Can we relax restrictions without sacrificing safety? The current prohibition on pointer arithmetic is probably too strict. Bounded pointer arithmetic (where the compiler tracks the valid range) may be feasible.
How do we handle FFI? Real systems code needs to call into existing C libraries. The boundary between safe fastC and unsafe C needs careful design.
Can the safety checks be formally verified? We would like to prove that the compiler’s safety check insertion is correct — that every program accepted by the fastC compiler is free of the memory safety violations we claim to prevent.
What about performance-critical inner loops? The 2-11% overhead is acceptable for most code, but not for the innermost loops of a matrix multiply or a compression codec. We are exploring an unsafe escape hatch, similar to Rust’s, where the programmer can opt out of safety checks for verified hot paths.
Conclusion
fastC is not the final answer to safe AI-generated code. It is a research exploration of a specific point in the design space: what happens when you take C’s mental model, restrict it to a safe subset, and optimise the restrictions for the code that LLMs actually generate? The early results suggest this is a productive direction. The compiled code is safe, the performance overhead is modest, and — critically — LLMs can actually generate code in the dialect without requiring superhuman type system reasoning.
The project is open source, and we welcome contributions from anyone interested in the intersection of language design, compiler engineering, and AI code generation.