Zen C, four months later: from promising to shipping
When a hyped language survives its first quarter
Back in January, Zen C was making the rounds in my feeds: a fresh systems language transpiling to readable GNU C, with V flavour and a few ideas of its own. It looked promising, but new languages get hyped every other month and most of them quietly die before a 1.0. So I let it cook.
A quarter in seems about the right time to look again, and tell you whether it actually went anywhere.
What it is, in two paragraphs
Zen C is a systems programming language that transpiles to human-readable GNU C/C11. You write modern code with type inference, pattern matching, generics, traits, async/await and string interpolation, and the compiler emits plain C that you can read, debug and drop into any existing C project. 100% C ABI compatibility is non-negotiable.
Think of it as Rust’s ergonomics meeting C’s simplicity, without the borrow checker complexity. The flagship feature is the DSL plugin system: you embed Lisp, SQL, Forth, Befunge or Brainfuck blocks directly in your code, and a plugin emits C at compile time. You can write your own.
Four months of git log
I cloned the repo to take stock. The first commit lands on January 11th. By May 1st:
- 938 commits total, 859 of them since mid-January.
- Roughly 330 in January, 245 in February, 216 in March, 143 in April. The pace settled but stayed serious.
- One core maintainer (Zuhaitz) doing around 80% of the work, 30+ external contributors picking up the rest.
- Zero stagnation, no rebrand drama, just steady output.
The version I was looking at in January was 0.1.0. The latest tag today is 0.4.4:
v0.1.0 2026-01-25 initial public release
v0.4.0 2026-01-31 jump straight to 0.4 (regex stdlib, generic fixes, i18n)
v0.4.1 2026-02-13 arena pattern, CMake support, more types
v0.4.2 2026-02-26 default-init refactor, security fixes in utils.c
v0.4.3 2026-03-12 std/hash.zc, ARM64 fixes, generics polish
v0.4.4 2026-03-20 source-level debugging supportThe leap from 0.1.x to 0.4.0 in six days is not a fluke: it lines up with the stdlib expansion and the moment external contributors started arriving in numbers.
Building it, and the version trap
I cloned and built it, and the build told me something the version file did not.
$ git clone --depth 1 https://github.com/zenc-lang/zenc.git
$ cd zenc && cat .version
v0.4.4
$ make -j6
src/repl/repl_jit.h:15:10: fatal error: libtcc.h: No such file or directory
...The REPL’s JIT is built on LibTCC and its headers are a separate package. With that installed it compiles clean:
$ sudo apt install build-essential libtcc-dev
$ make -j6
$ ./zc --version
zc a471a6aA bare commit hash, not a version. That is the tell I should have read immediately, because .version says v0.4.4 and this binary is 408 commits past that tag. The version file on main lags the release, and a shallow clone of the default branch gives you neither the tag nor a warning.
It matters more than a label. src/repl/ in the v0.4.4 tarball contains repl.c and repl.h and no repl_jit.* at all, so the LibTCC dependency I just tripped over does not exist in the release. Same for the plugins: v0.4.4 ships them as .c, the .zc sources arrived later.
So everything below describes main at a471a6a, not the 0.4.4 you would download. For a project moving at this pace that distinction is the whole point, and I nearly published it the wrong way round.
The syntax that caught me
What makes Zen C interesting day to day is how aggressively it cuts boilerplate. A bare string statement is a println, and a ! prefix sends it to stderr:
fn main() {
"Hello, world!";
!"Error: file not found";
let user = "davlgd";
let tasks = 42;
"User {user} has {tasks} pending tasks";
}Error: file not found
Hello, world!
User davlgd has 42 pending tasksInterpolation happens in the same expression statement, with no print and no format. Note the ordering in that output: the stderr line arrived first, because it is unbuffered.
Pattern matching feels Rust-like and compiles down to a plain C switch:
enum Priority { Low, Medium, High, Critical }
fn describe(p: Priority) -> string {
match p {
Priority::Low => return "Can wait",
Priority::High => return "Do it now!",
Priority::Critical => return "Do it now!",
_ => return "Normal priority"
}
}Calling it with High gives Do it now! and with Medium falls through to Normal priority, as you would expect. Enums with payloads destructure right in the arm, and generics use monomorphisation, so Vec<T> and Option<T> cost nothing at runtime.
Memory management stays explicit, with three knobs: defer free(buf) for the manual case, autofree for scope-bound cleanup, and a Drop trait for RAII. No GC, no borrow checker.
The plugin system
This is what sets Zen C apart from the other modern-C attempts. You import a plugin, embed a domain-specific language as a name! { ... } block, and the plugin emits C at compile time:
import plugin "plugins/lisp" as lisp
fn main() {
lisp! {
(defun square (x) (* x x))
(print (square 5))
}
}That prints 25. There is a Lisp interpreter inside my C binary, and it got there through a compile-time plugin. On main the plugins/ directory ships lisp, sql, forth, befunge and brainfuck, each as a .zc source and a built .so. In the 0.4.4 release they are still .c.
Regular expressions are deliberately not a plugin: they live in the standard library as a normal API.
What landed since January
The headline changes, roughly in shipping order:
- APE builds via Cosmopolitan Libc, producing a single
zc.comthat runs on Linux, macOS, Windows and the BSDs, onx86_64andaarch64. - Multi-backend compilation:
zc run app.zc --cc gcc|clang|zig|tcc, with the test suite at 100% on GCC, Clang and Zig, 98% on TCC. - C++, CUDA and Objective-C interop through
--cpp,--cudaand--objc. - C23 support when the backend allows, including arbitrary-width integers compiling to
_BitInt(N). - MISRA C:2012 verification in the test suite, independent of any MISRA affiliation.
- REPL rewrite with JIT compilation powered by LibTCC, which is why that header matters.
- Source-level debugging in v0.4.4, and a stdlib grown to cover regex, hashing, networking, JSON and big numbers.
How it compares to V
The two are easy to mix up: both transpile to C, both go after C’s ergonomics. The differences show once you spend a day with each.
V has a few years of head start and a wider ecosystem out of the box: vweb, an ORM, a package manager, and several backends. Its C output is dense and machine-oriented, optimised for the compiler, not for a human reader.
Zen C aims elsewhere. One backend, but paired with multiple C compilers and bridges into C++, CUDA and Objective-C. The generated C is meant to be read and dropped into existing codebases. The DSL plugin system has no equivalent in V.
For application code with HTTP and a package manager already wired up, V wins today. For embedding modern code in a C-heavy project, hitting GPU kernels or shipping a single APE binary, Zen C is the more natural fit. They are not solving the same problem.
What is still missing
It is 0.4.x, and the honest list is short: no package manager, alpha IDE tooling, patchy documentation outside the language tour, and a smaller community than the mainstream. The Discord is active and the contributor count keeps growing.
In January the question was whether this was another shiny C-targeting language that would not survive six months. Four months later, with 938 commits, five releases, APE and CUDA interop, source-level debugging and a doubled stdlib, the question has changed. It is no longer “will it survive”, it is “what will it be at 1.0”.
If you write embedded code, game engines or anything where C is the deployment target, give it a try. And tell me what you build with it 😉
← → jump to the previous / next post