DEV Community

Dipankar Sarkar
Dipankar Sarkar

Posted on

Why I built a C-like language for agent-written code

Most of the code my tools write now is written by an agent and read by me.

That single shift breaks a lot of assumptions baked into the languages we use. C
trusts the author completely. It has no idea whether a function does I/O, whether
an integer just wrapped, or whether a build script is about to run curl | sh on
your laptop. When a human wrote every line, that trust was earned slowly. When an
LLM emits 200 lines in two seconds, the trust model is wrong.

So I built fastC. It is a small systems language that compiles to readable C11,
and it is designed around one question: what does a language look like when you
assume the author is an agent and the reviewer is a tired human?

The problem with C for generated code

C's failure modes are exactly the ones an agent hits.

Silent integer overflow is the classic one. Ask an open-weight model to "sum an
array" and it will happily hand you code that wraps at scale and returns a wrong
answer with no warning. I measured this. On a "sum 1..100000" task with GLM,
three trials each:

  • Go silently wrapped 3/3.
  • Rust wrapped 2/3.
  • fastC and Zig either refused to compile or computed correctly. Neither shipped a silently-wrong binary.

The other failure mode is ambient authority. In C, any function can open a socket
or read a file. There is no signal in the type of a function that says "this one
touches the network." When you are reviewing agent output, you want that signal
badly. You want to look at a signature and know the blast radius.

And then there is the build step. build.rs, build.zig, cgo all run
arbitrary code at build time. That is a supply-chain hole that an agent pulling
a dependency will walk straight into. fastC ships a side-by-side demo where
cargo build executes a malicious build.rs, and fastc.toml rejects the same
shape at parse time because it structurally cannot execute anything at build time.

The core idea: capabilities as typed arguments

The center of fastC is that I/O capabilities are typed function arguments, not
ambient powers.

If a function wants to reach the network, it must declare a CapNetConnect
parameter. A function that does not take that parameter structurally cannot open
a socket. The type system makes it impossible, not discouraged. The same holds
for reading files with CapFsRead and spawning processes with CapProcSpawn.
The capability set is finite and named.

The capability tokens can only be minted in main, through caps::init().
Library code never fabricates them, and the compiler has a fabrication check that
enforces exactly that. So when you read a fastC signature, the capability
arguments are an honest, compiler-checked manifest of what the function can do.

That changes code review. A pure transform has no cap arguments, and you can
trust that claim without reading the body. That is the whole wedge for reviewing
code you did not write.

Hello world, and how it differs from C

Here is the smallest program.

fn main() -> i32 {
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Compile it and run it through a normal C compiler:

echo 'fn main() -> i32 { return 0; }' > hello.fc
fastc compile hello.fc -o hello.c
cc hello.c -o hello && ./hello && echo OK
Enter fullscreen mode Exit fullscreen mode

fastC emits C11. That is deliberate. It means any C cross-compiler in the world
can target a fastC binary. The stripped hello binary is 53 KB, in the same class
as C and Zig, not the 342 KB of a Rust hello or the 2.4 MB of a Go one. A fastC
binary that does real work fits inside a container cold-start budget and inside
embedded flash. That is a real constraint for a lot of the systems I care about.

The differences from C that matter day to day:

  • Memory safety without a garbage collector.
  • Capability-typed I/O, as above.
  • Mandatory pre- and postconditions on public APIs, @requires and @ensures, discharged through a three-tier pipeline: a syntactic checker that is always on, an opt-in Z3 tier for linear-integer tautologies, and a runtime fallback. Proven obligations cost zero at runtime.
  • Vendored, content-hashed dependencies with no central registry and no executable build steps.

Contracts, concretely

Contracts are not a bolt-on. They are compile-time obligations on public APIs.

fastc compile --prove program.fc
Enter fullscreen mode Exit fullscreen mode

That runs every @requires and @ensures clause through the three tiers. The
syntactic discharger constant-folds and catches tautological comparisons with no
Z3 dependency at all. The Z3 tier handles linear-integer cases with a 500 ms
per-obligation budget. Anything neither tier can prove falls back to a runtime
fc_trap. The build emits a discharge.json per-obligation report with
proven/runtime/unknown counts and the tier that handled each one. If Z3 is not on
your PATH, the obligation degrades to the runtime tier with a structured reason.
The build does not break.

The point is that "prove what you can, trap the rest, never silently drop a
check" is the right default when you cannot assume the author reasoned carefully.

The honest numbers

fastC compile time is around 30 to 40 percent faster than Rust to a release
binary. Runtime matches C on floating-point-heavy work. It is about 26 percent
slower on recursive integer work like fib(40) because of overflow-check cost.
That is the trade. You pay a little on integer-heavy recursion to never ship a
silent wrap.

On agent first-compile success for a sum_array task across four open-weight
models, fastC scored 12/12, matching or beating C, Rust, Zig, and Go. But there
is a sharp caveat in that number, and it leads into the trade-offs.

Where fastC does not fit

This is the part that matters most, so I will be blunt.

The agent success number is only 12/12 when the cheatsheet shipped with the
prompt is faithful. An earlier run against an inaccurate cheatsheet scored 0/9.
The language is new, so models do not know it from training. You have to feed
them an accurate worked example and a "common mistakes" guide. Without that
scaffolding, an agent flounders, because there is no Stack Overflow corpus for
fastC yet. That is a real adoption cost.

fastC also loses on ecosystem. Rust has 150K crates. fastC has an eleven-package
core set in preview. If your project needs a mature library for something
specific, you will be writing FFI bindings or doing without.

The contract and capability model is friction. If your team ships internal
scripts fast and does not review agent output carefully, all of this ceremony is
overhead you will resent. fastC pays off precisely when the review burden is real
and the blast radius matters.

And on raw single-threaded integer throughput, plain C at -O2 is still ahead. If
that 26 percent is your hot path and you trust your authors, use C.

The honest framing, including which rows fastC loses on, is in the repo's
MANIFESTO.md. I would rather you read that than my pitch.

Takeaways

  • Assume the author is an agent, and the language design changes. You want I/O in the type signature and you want overflow to be loud.
  • Compiling to readable C11 buys you portability and small binaries for free.
  • "Prove what you can, trap the rest" beats both "assume it is fine" and "block the build."
  • New languages have no training corpus. That is a first-class adoption problem, not a footnote.

fastC is v1.0 feature-complete with 337+ passing tests. It is MIT licensed.

Repo: https://github.com/fastc-lang/fastc

The honest ask: clone it, run the supply-chain demo and the cross-lang
benchmarks, and tell me where the capability model gets in your way. The failure
reports are more useful to me than the stars. Issues welcome.

Top comments (0)