Chapter 56Builtins Quick Reference

Appendix B. Builtins Quick Reference

Overview

@builtins are the compiler’s verbs; they describe how Zig thinks about types, pointers, and program structure, and they are available in every file without imports. After experimenting with compile-time programming in Part III, this appendix captures the most common builtins, their intent, and the surface-level contracts you should remember when reading or writing metaprogramming-heavy Zig. 15

The 0.15.2 release stabilized several introspection helpers (@typeInfo, @hasDecl, @field) and clarified truncation semantics for new integer sizes, making it practical to rely on the behaviors summarized here. v0.15.2

Learning Goals

  • Spot the difference between reflection builtins, arithmetic helpers, and control builtins when scanning a codebase.
  • Combine type inspection builtins to build adapters that work with user-provided types.
  • Verify the runtime behavior of numeric conversions at the edges of range and safety modes.

Core Reflection Builtins

Reflection builtins give us structured information about user types without grabbing raw pointers or discarding safety checks. 15 The example below shows how to form a documented summary of any struct, including comptime fields, optional payloads, and nested arrays.

Zig
//! Summarizes struct metadata using @typeInfo and @field.
const std = @import("std");

fn describeStruct(comptime T: type, writer: anytype) !void {
    const info = @typeInfo(T);
    switch (info) {
        .@"struct" => |struct_info| {
            try writer.print("struct {s} has {d} fields", .{ @typeName(T), struct_info.fields.len });
            inline for (struct_info.fields, 0..) |field, index| {
                try writer.print("\n  {d}: {s} : {s}", .{ index, field.name, @typeName(field.type) });
            }
        },
        else => try writer.writeAll("not a struct"),
    }
}

test "describe struct reports field metadata" {
    const Sample = struct {
        id: u32,
        value: ?f64,
    };

    var buffer: [256]u8 = undefined;
    var stream = std.io.fixedBufferStream(&buffer);
    try describeStruct(Sample, stream.writer());
    const summary = stream.getWritten();

    try std.testing.expect(std.mem.containsAtLeast(u8, summary, 1, "id"));
    try std.testing.expect(std.mem.containsAtLeast(u8, summary, 1, "value"));
}

test "describe struct rejects non-struct types" {
    var buffer: [32]u8 = undefined;
    var stream = std.io.fixedBufferStream(&buffer);
    try describeStruct(u8, stream.writer());
    const summary = stream.getWritten();
    try std.testing.expectEqualStrings("not a struct", summary);
}
Run
Shell
$ zig test 01_struct_introspection.zig
Output
Shell
All 2 tests passed.

Use @typeInfo plus @field inside inline loops so the compiler still optimizes away branches after specialization. 17

Value Extraction Helpers

Builtins such as @field, @hasField, and @fieldParentPtr let you map runtime data back to compile-time declarations without violating Zig’s strict aliasing rules. The following snippet shows how to surface parent pointers while maintaining const-correctness. meta.zig

Zig
//! Demonstrates `@fieldParentPtr` to recover container pointers safely.
const std = @import("std");

const Node = struct {
    id: u32,
    payload: Payload,
};

const Payload = struct {
    node_ptr: *const Node,
    value: []const u8,
};

fn makeNode(id: u32, value: []const u8) Node {
    var node = Node{
        .id = id,
        .payload = undefined,
    };
    node.payload = Payload{
        .node_ptr = &node,
        .value = value,
    };
    return node;
}

test "parent pointer recovers owning node" {
    var node = makeNode(7, "ready");
    const parent: *const Node = @fieldParentPtr("payload", &node.payload);
    try std.testing.expectEqual(@as(u32, 7), parent.id);
}

test "field access respects const rules" {
    var node = makeNode(3, "go");
    const parent: *const Node = @fieldParentPtr("payload", &node.payload);
    try std.testing.expectEqualStrings("go", parent.payload.value);
}
Run
Shell
$ zig test 02_parent_ptr_lookup.zig
Output
Shell
All 2 tests passed.

@fieldParentPtr assumes the child pointer is valid and properly aligned; combine it with std.debug.assert in debug builds to catch accidental misuse early. 37

Numeric Safety Builtins

Numeric conversions are where undefined behavior often hides; Zig makes truncation explicit via @intCast, @intFromFloat, and @truncate, which all obey safety-mode semantics. 37 0.15.2 refined the diagnostics these builtins emit when overflow occurs, making them reliable guards in debug builds.

Zig
//! Exercises numeric conversion builtins with guarded tests.
const std = @import("std");

fn toU8Lossy(value: u16) u8 {
    return @truncate(value);
}

fn toI32(value: f64) i32 {
    return @intFromFloat(value);
}

fn widenU16(value: u8) u16 {
    return @intCast(value);
}

test "truncate discards high bits" {
    try std.testing.expectEqual(@as(u8, 0x34), toU8Lossy(0x1234));
}

test "intFromFloat matches floor for positive range" {
    try std.testing.expectEqual(@as(i32, 42), toI32(42.9));
}
test "intCast widens without loss" {
    try std.testing.expectEqual(@as(u16, 255), widenU16(255));
}
Run
Shell
$ zig test 03_numeric_conversions.zig
Output
Shell
All 3 tests passed.

Wrap lossy conversions in small helper functions so the intent stays readable and you can centralize assertions around shared digit logic. 10

Comptime Control & Guards

@compileError, @panic, @setEvalBranchQuota, and @inComptime give you direct control over compile-time execution; they are the safety valves that keep metaprogramming deterministic and transparent. 15 The short example below guards vector widths at compile time and raises the evaluation branch quota before computing a small Fibonacci number during analysis.

Zig
//! Demonstrates compile-time guards using @compileError and @setEvalBranchQuota.
const std = @import("std");

fn ensureVectorLength(comptime len: usize) type {
    if (len < 2) {
        @compileError("invalid vector length; expected at least 2 lanes");
    }
    return @Vector(len, u8);
}

fn boundedFib(comptime quota: u32, comptime n: u32) u64 {
    @setEvalBranchQuota(quota);
    return comptimeFib(n);
}

fn comptimeFib(comptime n: u32) u64 {
    if (n <= 1) return n;
    return comptimeFib(n - 1) + comptimeFib(n - 2);
}

test "guard accepts valid size" {
    const Vec = ensureVectorLength(4);
    const info = @typeInfo(Vec);
    try std.testing.expectEqual(@as(usize, 4), info.vector.len);
    // Uncommenting the next line triggers the compile-time guard:
    // const invalid = ensureVectorLength(1);
}

test "branch quota enables deeper recursion" {
    const result = comptime boundedFib(1024, 12);
    try std.testing.expectEqual(@as(u64, 144), result);
}
Run
Shell
$ zig test 04_comptime_guards.zig
Output
Shell
All 2 tests passed.

@compileError stops the compilation unit immediately; use it sparingly and prefer returning an error when runtime validation is cheaper. Leave a commented-out call (as in the example) to document the failure mode without breaking the build. 12

Cross-Checking Patterns

  • Drive refactors with @hasDecl and @hasField before depending on optional features from user types; this matches the defensive style introduced in Chapter 17.
  • Combine @TypeOf, @typeInfo, and @fieldParentPtr to keep diagnostics clear in validation code—the trio makes it easy to print structural information when invariants fail.
  • Remember that some builtins (like @This) depend on lexical scope; reorganizing your file can silently change their meaning, so rerun tests after every major rearrangement. 36

Notes & Caveats

  • Builtins that interact with the allocator (@alignCast, @ptrCast) still obey Zig’s aliasing rules; rely on std.mem helpers when in doubt. 3
  • @setEvalBranchQuota is global to the current compile-time execution context; keep quotas narrow to avoid masking infinite recursion. 15
  • Some experimental builtins appear in nightly builds but not in 0.15.2—pin your tooling before adopting new names.

Exercises

  • Build a diagnostic helper that prints the tag names of any union using @typeInfo.union. 17
  • Extend the numeric conversions example to emit a human-readable diff between bit patterns before and after truncation. fmt.zig
  • Write a compile-time guard that rejects structs lacking a name field, then integrate it into a generic formatter pipeline. 36

Alternatives & Edge Cases

  • Prefer higher-level std helpers when a builtin duplicates existing behavior—the standard library often wraps edge cases for you. 43
  • Reflection against anonymous structs can produce compiler-generated names; cache them in your own metadata if user-facing logs need stability. 12
  • When interfacing with C, remember that some builtins (e.g. @ptrCast) can affect calling conventions; double-check the ABI section before deploying. 33

Help make this chapter better.

Found a typo, rough edge, or missing explanation? Open an issue or propose a small improvement on GitHub.