Detailed Summary / Reference
1. The starting point: zig run vs zig build-exe
zig run main.zig— Compiles and runs without creating a named executable in the project folder. Great for quick iteration.zig build-exe main.zig -O ReleaseSmall— compiles only, leaving a persistent binary you can ship, inspect, or run repeatedly.
2. Zig 0.16.0 breaking change: std.io → std.Io
My original main.zig used:
const stdout = std.io.getStdOut().writer();
try stdout.print("Zig Day 01 — hello\n", .{});
This no longer compiles on Zig 0.16.0. The std.io namespace was replaced by a much larger std.Io interface (capital I) as part of a new explicit async I/O model — writing to real stdout now requires setting up an Io context object (e.g. std.Io.Threaded) and passing it around explicitly.
Fix used (Option A — simplest):
const std = @import("std");
pub fn main() void {
std.debug.print("Zig Day 01 — hello\n", .{});
}
std.debug.print sidesteps the new Io interface entirely — no setup required. Trade-off: it writes to stderr, not stdout (invisible difference in a terminal, but worth knowing). The proper std.Io-based approach is a meatier topic for a future lesson.
3. Optimization modes and binary size
Zig -O mode | Description |
|---|---|
Debug (default) | No optimization, safety checks on, full debug info |
ReleaseSafe | Optimized, safety checks retained |
ReleaseFast | Optimized for speed, safety checks stripped |
ReleaseSmall | Optimized for smallest binary size, safety checks stripped |
4. The real discovery: optimization ≠ stripping debug symbols
Measured across identical source code:
| Build | Flags | Size |
|---|---|---|
| Debug | (default) | 9.8M |
| ReleaseFast, symbols kept | -O ReleaseFast -fno-strip | 3.6M |
| ReleaseFast, default | -O ReleaseFast (no strip flag) | 3.6M |
| ReleaseFast, stripped | -O ReleaseFast -fstrip | 210K |
Key finding: -O ReleaseFast does not strip debug symbols by default — confirmed by main-fast-default and main-fast-debug landing at the exact same size (3.6M). Debug info had to be removed explicitly via -fstrip. That flag alone accounted for ~3.39M, roughly 94% of the binary’s size in that build.
What DWARF debug info actually contains:
- Line number tables (mapping machine code → source lines)
- Type information (struct/enum/variable shapes)
- Name tables (symbol names for debuggers)
- Call frame info (enables stack traces)
None of it affects runtime behavior — it’s metadata for tools like gdb/lldb, but it can dwarf a tiny program’s actual code size.
5. Zig vs Roc: a cross-language binary size comparison
Zig (ReleaseSmall) | Roc (--opt=size) | |
|---|---|---|
| Command | zig build-exe main.zig -O ReleaseSmall | roc build main.roc --opt=size |
| Binary size | 7.2K | 104K |
Roc’s binary is larger mainly because it statically links its platform and runtime (effects system, host binary, memory management) into every executable — a deliberate architectural trade-off for a higher-level, effect-based language, versus Zig’s near-zero-runtime systems approach.
Roc/Zig command parallels:
| Zig | Roc | Purpose |
|---|---|---|
zig run main.zig | roc main.roc | compile + run, throwaway |
zig build-exe ... -O ReleaseSmall | roc build main.roc --opt=size | compile only, persistent binary |
6. Exercise completed
Final main.zig:
const std = @import("std");
pub fn main() void {
std.debug.print("Zig Day 01 — Give me one step at a time\n", .{});
}
Ran successfully via zig run main.zig — Zig Day 01 exercise fully complete (all 4 steps).
Lessons for Next Time
A short “on deck” list to carry into future posts, so future-you has a trailhead:
- Finish Roc Day 01 — change the greeting string in
roc/day01/main.rocand re-run withroc main.roc(attempted once already; ananosave slipped through, so double-check withCtrl+O→Enter→Ctrl+Xnext time). - Explore
std.Ioproperly — today we sidestepped Zig’s new explicit I/O interface usingstd.debug.print. A future post could walk through setting upstd.Io.Threadedmanually and writing to real stdout, which is the forward-looking pattern in Zig 0.16+. - Close the
ReleaseSmallvsReleaseFastgap — we measuredReleaseFast -fstripat 210K, far above the very firstReleaseSmallbuild’s 7.2K — but those used two different versions ofmain.zig, so it’s not a clean comparison. Worth re-runningReleaseSmall -fstripon the current source to isolate whether the mode or the code path explains the gap. - Inspect the Roc binary’s sections — we never ran
readelf -S(or Roc’s equivalent) against the 104K Roc binary to see what’s actually inside it (platform code? debug info? both?). Doing so would ground the “Roc statically links its runtime” claim in real data, the same way we did for Zig’s DWARF sections. - Start
notes/compare.md— the blank template sitting in~/learn-zig-roc/notes/compare.mdis meant to capture personal impressions (“what felt fast,” “what felt explicit,” “try vs Result,” “when I’d pick each”) — a good five-minute wrap-up habit after each paired Zig/Roc day.