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.
//! 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);
}
$ zig test 01_struct_introspection.zigAll 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
//! 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);
}
$ zig test 02_parent_ptr_lookup.zigAll 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.
//! 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));
}
$ zig test 03_numeric_conversions.zigAll 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.
//! 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);
}
$ zig test 04_comptime_guards.zigAll 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
@hasDecland@hasFieldbefore depending on optional features from user types; this matches the defensive style introduced in Chapter 17. - Combine
@TypeOf,@typeInfo, and@fieldParentPtrto 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 onstd.memhelpers when in doubt. 3 @setEvalBranchQuotais 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
namefield, 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