Zig
Simple, explicit systems programming.
Zig is a modern systems language that aims to be a better C: no hidden control flow, no hidden memory allocations and compile-time code execution. It is used for fast runtimes such as Bun, the TigerBeetle database and the Ghostty terminal, and its toolchain can compile C and C++ too.
Why learn Zig?
Zig gives you C-level speed and control with clearer error handling and a much nicer build system.
- No hidden behaviour: every allocation and error path is visible in the code.
- comptime: run ordinary Zig code at compile time for generics and code generation.
- C interop: import C headers directly and cross-compile to almost any target.
Set up and run your first program
Download Zig from ziglang.org or your package manager. One binary includes the compiler, build system and test runner.
zig version
zig run hello.zig
zig initconst std = @import("std");
pub fn main() void {
const name = "Ada";
std.debug.print("Hello, AI! Welcome to Zig, {s}.\n", .{name});
}Variables and types
const values never change; var can. Integer types state their size, like u8, i32 and u64. Unused variables are compile errors.
const std = @import("std");
pub fn main() void {
const layers: u32 = 32;
var tokens: u64 = 0;
tokens += 4096;
const params: f64 = 8.03e9;
std.debug.print("{d} layers, {d} tokens, {d:.1} GB\n", .{ layers, tokens, params * 2 / 1e9 });
}Control flow
if, while, for and switch are expressions. switch must cover every case.
const std = @import("std");
pub fn main() void {
const gpus = [_][]const u8{ "H100", "H200", "B200" };
for (gpus, 0..) |g, i| std.debug.print("{d}: {s}\n", .{ i, g });
const bits: u8 = 8;
const name = switch (bits) {
32 => "FP32",
16 => "FP16",
8 => "FP8",
else => "other",
};
std.debug.print("{s}\n", .{name});
}Errors and optionals
Functions that can fail return an error union like !u32. try passes errors up; catch handles them. ?T is an optional value.
const std = @import("std");
const VramError = error{TooLarge};
fn fit(model_gb: u32, vram_gb: u32) VramError!u32 {
if (model_gb > vram_gb) return VramError.TooLarge;
return vram_gb - model_gb;
}
pub fn main() void {
const free = fit(16, 24) catch 0;
std.debug.print("free: {d} GB\n", .{free});
const maybe: ?u32 = null;
std.debug.print("{d}\n", .{maybe orelse 42});
}Structs and methods
Structs group data and functions. Methods are functions whose first parameter is the struct.
const std = @import("std");
const Gpu = struct {
name: []const u8,
vram: u32,
fn fits(self: Gpu, gb: u32) bool {
return gb <= self.vram;
}
};
pub fn main() void {
const g = Gpu{ .name = "RTX 5090", .vram = 32 };
std.debug.print("{s} fits 16 GB: {}\n", .{ g.name, g.fits(16) });
}Allocators and defer
Zig has no garbage collector and no hidden allocations. You pass an allocator to anything that needs memory, and defer frees it when the scope ends.
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
const weights = try alloc.alloc(f32, 1024);
defer alloc.free(weights);
for (weights, 0..) |*w, i| w.* = @as(f32, @floatFromInt(i)) * 0.001;
std.debug.print("last weight: {d}\n", .{weights[1023]});
}AI with Zig
Zig is used to write fast inference engines and kernels, much like C and C++. Its SIMD vectors, explicit memory control and easy C interop make it good for running small models on CPUs and edge devices, and projects such as ZML use Zig to run models on GPUs and TPUs.
The essential libraries
A SIMD dot product
The core of every neural network layer, using Zig's built-in vector type.
const std = @import("std");
fn dot(a: []const f32, b: []const f32) f32 {
const V = @Vector(8, f32);
var acc: V = @splat(0);
var i: usize = 0;
while (i + 8 <= a.len) : (i += 8) {
const va: V = a[i..][0..8].*;
const vb: V = b[i..][0..8].*;
acc += va * vb;
}
var sum = @reduce(.Add, acc);
while (i < a.len) : (i += 1) sum += a[i] * b[i];
return sum;
}
pub fn main() void {
const a = [_]f32{1} ** 20;
const b = [_]f32{0.5} ** 20;
std.debug.print("dot = {d}\n", .{dot(&a, &b)}); // 10
}Call an LLM from Zig
The standard library includes an HTTP client and JSON support, so Zig can talk to a local model with no dependencies. A full tool-calling loop follows the same pattern as the other volumes: send messages, check for tool_calls, append results and repeat.
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
var client = std.http.Client{ .allocator = alloc };
defer client.deinit();
const body =
\\{"model":"llama3.1","messages":[{"role":"user","content":"Explain tensor cores in one sentence."}]}
;
var out = std.ArrayList(u8).init(alloc);
defer out.deinit();
_ = try client.fetch(.{
.location = .{ .url = "http://localhost:11434/v1/chat/completions" },
.method = .POST,
.payload = body,
.headers = .{ .content_type = .{ .override = "application/json" } },
.response_storage = .{ .dynamic = &out },
});
const parsed = try std.json.parseFromSlice(std.json.Value, alloc, out.items, .{});
defer parsed.deinit();
const msg = parsed.value.object.get("choices").?.array.items[0].object.get("message").?;
std.debug.print("{s}\n", .{msg.object.get("content").?.string});
}The std.http API changes between Zig releases; this example targets Zig 0.13–0.14.
ollama pull llama3.1, and keep it running. The code talks to its OpenAI-compatible API at http://localhost:11434/v1, so the same program also works with vLLM, llama.cpp server or a cloud API by changing the URL, model name and adding an API key.Zig learning roadmap
Tick items off as you go. Your progress is saved in this browser.
Beginner
Intermediate
Advanced
Projects to build
Zig cheat sheet
| Task | Syntax |
|---|---|
| Constant | const x: u32 = 5; |
| Variable | var y: f32 = 1.0; |
| Slice loop | for (items) |it| { ... } |
| Error return | fn f() !u32 { ... } |
| Handle error | const v = f() catch 0; |
| Free later | defer alloc.free(buf); |
Common mistakes
- Unused variables and parameters are errors; use
_ = x;while prototyping. - Integer overflow is checked in debug builds and crashes; use
+%for wrapping. - The language and standard library still change before 1.0.
Where to go next
On TensorCore.si: C volume, Rust volume.
Keyboard: [ previous language · ] next language · / search the library