Skip to content
Volume Z · The TensorCore.si Library

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.

Prologue

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.
Prologue

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.

terminal
zig version
zig run hello.zig
zig init
hello.zig
const std = @import("std");

pub fn main() void {
    const name = "Ada";
    std.debug.print("Hello, AI! Welcome to Zig, {s}.\n", .{name});
}
TipZig changes between versions before 1.0; check the docs for the version you install.
Chapter 1

Variables and types

const values never change; var can. Integer types state their size, like u8, i32 and u64. Unused variables are compile errors.

variables.zig
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 });
}
Chapter 2

Control flow

if, while, for and switch are expressions. switch must cover every case.

control.zig
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});
}
Chapter 3

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.

errors.zig
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});
}
Chapter 4

Structs and methods

Structs group data and functions. Methods are functions whose first parameter is the struct.

structs.zig
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) });
}
Chapter 5

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.

memory.zig
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]});
}
Part II · AI with Zig

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

ZMLHigh-performance model inference built with Zig.
@VectorBuilt-in SIMD vector types.
std.httpHTTP client in the standard library.
std.jsonJSON parsing and writing.

A SIMD dot product

The core of every neural network layer, using Zig's built-in vector type.

dot.zig
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
}
Part III · Build an AI agent

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.

Your goal→Model plans→Tool call→Your code runs the tool→Result back to model→Final answer
chat.zig
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.

Run it locallyInstall Ollama, run 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.
Part IV · Your path

Zig learning roadmap

Tick items off as you go. Your progress is saved in this browser.

Beginner

Intermediate

Advanced

Part IV · Your path

Projects to build

CLI toolA file word counter with careful memory use.
Matrix librarySIMD matrix multiply with benchmarks.
LLM clientA tiny command-line chat client for a local model.
Reference

Zig cheat sheet

TaskSyntax
Constantconst x: u32 = 5;
Variablevar y: f32 = 1.0;
Slice loopfor (items) |it| { ... }
Error returnfn f() !u32 { ... }
Handle errorconst v = f() catch 0;
Free laterdefer alloc.free(buf);
Reference

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.
Reference

Where to go next

On TensorCore.si: C volume, Rust volume.

Keyboard: [ previous language · ] next language · / search the library