Zig interpreter
Hello Ziggy
I wrote1 an interpreter for a subset of Zig and named it zig1. Here is a
demo:
$ wget -O zig1.c https://m.jakstys.lt/2026/zig-interpreter/stage1-0.17.1-1.c
$ cc -O3 zig1.c -o zig1 -lm
$ cat <<EOF > hello.zig
pub fn main() void {
@import("std").debug.print("Hello Ziggy!\n", .{});
}
EOF
$ ./zig1 --run --zig-lib-dir ../lib hello.zig
Hello Ziggy!
real 0m0.337s
user 0m0.233s
sys 0m0.104s
Why a Zig subset, you don’t ask? Because it only works with valid Zig: an invalid Zig program is undefined behavior. It will also sometimes crash, or just run incorrectly, on correct Zig programs. How cheeky! It is also slow – hundreds to thousands of times slower than a Zig-compiled binary, depending on the program:
$ hyperfine -Np 'zig build-exe hello.zig' './hello' './zig1 --run --zig-lib-dir ../lib hello.zig'
Benchmark 1: ./hello
Time (mean ± σ): 767.9 µs ± 158.2 µs [User: 407.3 µs, System: 207.8 µs]
Range (min … max): 583.9 µs … 1101.5 µs 10 runs
Benchmark 2: ./zig1 --run --zig-lib-dir ../lib hello.zig
Time (mean ± σ): 246.9 ms ± 3.9 ms [User: 173.3 ms, System: 70.7 ms]
Range (min … max): 241.0 ms … 252.9 ms 10 runs
Summary
./hello ran
321.52 ± 66.44 times faster than ./zig1 --run --zig-lib-dir ../lib hello.zig
It’s a really shitty interpreter.
Now for the fun stuff
Now that we got the intro out of the way, we can do fun stuff with it. For example, use real Zig through interpreter to compile that hello ziggy:
$ git clone -b 0.17.1 https://codeberg.org/ziglang/zig; cd zig
$ mv $OLDPWD/zig1 .
$ ./zig1 --run --zig-lib-dir lib --bootstrap-profile \
--dep build_options -Mroot=src/main.zig \
-Mbuild_options=<(./zig1 --bootstrap-build-options | sed 's/= \.bootstrap;/= .@"x86_64-linux";/') \
-- build-exe -fno-llvm -fno-lld -target x86_64-linux -OReleaseSmall \
--name hello -femit-bin=hello.small -Mroot=hello.zig
$ file hello.small
hello.small: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
$ hyperfine -N ./hello.small
Time (mean ± σ): 779.1 µs ± 149.5 µs [User: 414.9 µs, System: 249.4 µs]
Range (min … max): 554.1 µs … 1594.5 µs 2810 runs
This can take upwards an hour on cold caches (compared to milliseconds with
zig build-exe), but the result is identical to:
zig build-exe -fno-llvm -fno-lld -target x86_64-linux -OReleaseSmall \
--name hello -femit-bin=hello.small -Mroot=hello.zig
… except we didn’t have zig in the first place.
Compile zig itself
Since zig is just another Zig program, we can compile it with itself
directly, just like we did with hello ziggy:
$ cat <<EOF > config.zig
pub const have_llvm = false;
pub const llvm_has_m68k = false;
pub const llvm_has_csky = false;
pub const llvm_has_arc = false;
pub const llvm_has_xtensa = false;
pub const version: [:0]const u8 = "0.17.1-bootstrap";
pub const semver = @import("std").SemanticVersion.parse(version) catch unreachable;
pub const enable_debug_extensions = false;
pub const enable_logging = false;
pub const enable_link_snapshots = false;
pub const enable_tracy = false;
pub const value_tracing = false;
pub const skip_non_native = false;
pub const debug_gpa = false;
pub const dev = .core;
pub const io_mode: enum { threaded, evented } = .threaded;
pub const value_interpret_mode = .direct;
EOF
$ ./zig1 --run --zig-lib-dir lib --bootstrap-profile \
--dep build_options -Mroot=src/main.zig \
-Mbuild_options=<(./zig1 --bootstrap-build-options | sed 's/= \.bootstrap;/= .@"x86_64-linux";/') \
-- build-exe -fno-llvm -fno-lld -target x86_64-linux -OReleaseSmall \
--name zig2 -femit-bin=zig2 \
--dep build_options -Mroot=src/main.zig -Mbuild_options=config.zig
$ file zig2
zig2: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
Bootstrapping
You may remember Andrew removing Zig C++ implementation (stage1) in favor of
zig1.wasm, and leaving it for the void to implement an
alternative:
I openly acknowledge that this cost is being paid, however, I strongly believe that it is worth it in the end. I suspect that combined with an official language specification and a growing popularity of Zig, we will see a third-party project start an alternate Zig implementation in C, similar to how mrustc exists for Rust (despite not yet having a spec). This would fill the necessary role to solve O(1) source bootstrapping again.
So let’s replicate the bootstrap, but instead of a checked-in Zig
implementation in zig1.wasm, use the interpreter:
./zig1 lib build-exe -ofmt=c -lc -OReleaseSmall --name zig2 \
-femit-bin=zig2.c -target x86_64-linux \
--dep build_options -Mroot=src/main.zig \
-Mbuild_options=config.zig
./zig1 lib build-obj -ofmt=c -lc -OReleaseSmall --name compiler_rt \
-femit-bin=compiler_rt.c -target x86_64-linux \
-Mroot=lib/compiler_rt.zig
And there you have it after a few hours — zig2.c and compiler_rt.c,
byte-identical just like how zig1.wasm would compile them. Resume the
standard bootstrap to compile them to zig2 and eventually zig3.
About zig1.c
zig1.c is ~120k lines of C, about the same amount Andrew removed in
#13560. zig1.c includes lexer, parser, AstGen, Sema and AIR
interpreter2. When Zig implementation was available (i.e. everything but
interpreter), I tried to write a direct port, except for error handling.
Test harness, lexer, and 5% of parser were hand crafted by yours truly in early 2025, then I shelved it. Once powerful-enough LLMs came about, I resumed, and now, after lots of tokens and micro-management, it is complete.
For me, zig1.c concludes something I started 2 years ago to make Zig
bootstrapping easier. I did it; maybe someone will find it useful, even if
upstream won’t. Zig1 shares the zig license and obviously builds on top of it.
It can bootstrap Zig, it is mostly LLM-written, is licensed the same way as
Zig, and I do not promise anything about its maintenance. Full code with git
history can be found in stage1-0.17.x branch.
If you are hand-crafting another Zig implementation for bootstrapping purposes, learning that LLM wrote it first may be deeply sad. I am truly sorry. If not for LLMs, it is quite likely I would have picked it up continued to grind for the next few years for the fun of it and for myself and everyone’s benefit. Maybe you will be inspired and hand-craft a true, piece-of-art, artisanal, bootstrap-worthy, much smaller implementation?
Final thoughts
Since upstream is against LLMs (rightfully so), I added a disclaimer to the top of the README:
This is an LLM-tainted fork. Zig community is against any LLM use, please be respectful where you share & discuss it.
Extending my thought from the previous post:
Therefore, finishing a project with LLMs feels like eating a cake solo. Not only it’s unhealthy, it’s not-fun and wrong — cakes are meant to be eaten with others in celebration, just like Open Source was meant to be created, shared and enjoyed together. The world is very different now.
Now that some of us have dayjobs that expect LLM-enhanced project delivery, the act of writing code by hand feels akin to long hikes: set aside motor transportation, bed and kitchen. Instead, use a backpack, tent and a stove. Comfortable? Not really. Fun and deeply rewarding? Hell yes. Can be done for 250-365 days/year? Yes, but only by a few.
We went from slow, fun and collaborative to fast and depressingly lonely. If not for LLMs, I most likely would have picked this up and grinded it out in a few years, learning about compilers a bunch and, in procecss, bringing value to the community. Now I feel like I ate a bunch of cake.
Appendix
Hardware (for timings):
- CPU: AMD Ryzen 7 8745HS
- Linux 7.2.0
Tested with the following compilers:
- gcc 15.3.0
- clang 22.1.8
- tcc 22.1.8
- x86_64-w64-mingw32-gcc (GCC) 14-win32
$ /run/current-system/sw/bin/time -v ./zig1 lib build-obj -ofmt=c -lc -OReleaseSmall --name compiler_rt \
-femit-bin=compiler_rt.c -target x86_64-linux \
-Mroot=lib/compiler_rt.zig
Command being timed: "./zig1 lib build-obj -ofmt=c -lc -OReleaseSmall --name compiler_rt -femit-bin=compiler_rt.c -target x86_64-linux -Mroot=lib/compiler_rt.zig"
User time (seconds): 286.84
System time (seconds): 0.75
Percent of CPU this job got: 57%
Elapsed (wall clock) time (h:mm:ss or m:ss): 8:20.85
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 438496
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 311163
Voluntary context switches: 426
Involuntary context switches: 1649
Swaps: 0
File system inputs: 488
File system outputs: 39136
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0
$ /run/current-system/sw/bin/time -v ./zig1 --run --zig-lib-dir lib --bootstrap-profile \
--dep build_options -Mroot=src/main.zig \
-Mbuild_options=<(./zig1 --bootstrap-build-options | sed 's/= \.bootstrap;/= .@"x86_64-linux";/') \
-- build-exe -fno-llvm -fno-lld -target x86_64-linux -OReleaseSmall \
--name zig2 -femit-bin=zig2 \
--dep build_options -Mroot=src/main.zig -Mbuild_options=config.zig
Command being timed: "./zig1 --run --zig-lib-dir lib --bootstrap-profile --dep build_options -Mroot=src/main.zig -Mbuild_options=/dev/fd/63 -- build-exe -fno-llvm -fno-lld -target x86_64-linux -OReleaseSmall --name zig2 -femit-bin=zig2 --dep build_options -Mroot=src/main.zig -Mbuild_options=config.zig"
User time (seconds): 15669.04
System time (seconds): 14.09
Percent of CPU this job got: 96%
Elapsed (wall clock) time (h:mm:ss or m:ss): 4:31:15
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 1722732
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 4115356
Voluntary context switches: 1397
Involuntary context switches: 73168
Swaps: 0
File system inputs: 46560
File system outputs: 455840
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0
$ /run/current-system/sw/bin/time -v ./zig1 lib build-exe -ofmt=c -lc -OReleaseSmall --name zig2 \
-femit-bin=zig2.c -target x86_64-linux \
--dep build_options -Mroot=src/main.zig \
-Mbuild_options=config.zig
Command being timed: "./zig1 lib build-exe -ofmt=c -lc -OReleaseSmall --name zig2 -femit-bin=zig2.c -target x86_64-linux --dep build_options -Mroot=src/main.zig -Mbuild_options=config.zig"
User time (seconds): 16751.33
System time (seconds): 7.14
Percent of CPU this job got: 97%
Elapsed (wall clock) time (h:mm:ss or m:ss): 4:46:01
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 2245828
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 763145
Voluntary context switches: 692
Involuntary context switches: 74872
Swaps: 0
File system inputs: 11848
File system outputs: 577816
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0
-
LLM disclaimer applies. ↩︎
-
For a better description what those words mean, start here. ↩︎