surt666 avatar

zig-fundamentals

This skill should be used when the user asks questions like "how does comptime work in Zig", "explai

作者 surt666|オープンソース

Zig Fundamentals

Overview

This skill provides essential Zig language concepts for experienced programmers transitioning from other languages. It focuses on Zig's distinctive features: comptime (compile-time execution), error handling, optionals, and memory control primitives.

Target Zig version: 0.15.2

Core Concepts

Comptime: Compile-Time Execution

Comptime is Zig's most distinctive feature, enabling code execution during compilation.

What it does:

  • Executes arbitrary Zig code at compile time
  • Generates code, types, and data structures before runtime
  • Eliminates need for macros, preprocessor, or code generation tools

Common use cases:

  1. Generic functions and types:

    fn max(comptime T: type, a: T, b: T) T {
        return if (a > b) a else b;
    }
    

    The comptime T: type means T is determined at compile time, creating monomorphized versions.

  2. Comptime computation:

    const array_size = comptime fibonacci(10);  // Computed at compile time
    var buffer: [array_size]u8 = undefined;
    
  3. Type introspection and generation:

    fn printFields(comptime T: type) void {
        inline for (@typeInfo(T).Struct.fields) |field| {
            std.debug.print("{s}\n", .{field.name});
        }
    }
    

Key points:

  • Any expression can be forced to compile-time with comptime keyword
  • Parameters marked comptime must be known at compile time
  • Use inline for to unroll loops at compile time
  • Access type information with @typeInfo(T)

For more detailed patterns, see references/comptime-patterns.md.

Error Handling

Zig uses explicit error handling without exceptions. Errors are values, not control flow.

Error unions:

const FileError = error{ FileNotFound, PermissionDenied };

fn readFile(path: []const u8) FileError![]u8 {
    // Returns either error or []u8
    if (file_not_exist) return error.FileNotFound;
    return file_contents;
}

The ! operator:

  • !T means "error union" - either an error or value of type T
  • anyerror!T allows any error type

Error handling patterns:

  1. try - propagate errors:

    const contents = try readFile("data.txt");
    // If error, returns immediately from current function
    
  2. catch - handle errors:

    const contents = readFile("data.txt") catch |err| {
        std.debug.print("Error: {}\n", .{err});
        return;
    };
    
  3. catch with default value:

    const contents = readFile("data.txt") catch "default content";
    
  4. errdefer - cleanup on error:

    fn allocateResource() !*Resource {
        const resource = try allocate();
        errdefer deallocate(resource);  // Only runs if function returns error
    
        try initialize(resource);
        return resource;
    }
    

Key principles:

  • Errors are compile-time known sets of values
  • No hidden control flow (no exceptions)
  • try is syntactic sugar for catch |err| return err
  • Always handle or propagate errors explicitly

Optionals

Optionals represent values that may or may not exist, replacing null pointers safely.

Syntax:

const maybe_value: ?i32 = null;  // Optional i32
const has_value: ?i32 = 42;      // Contains value

Checking and unwrapping:

  1. if unwrapping:

    if (maybe_value) |value| {
        // value is i32 here, not ?i32
        std.debug.print("Value: {}\n", .{value});
    } else {
        std.debug.print("No value\n", .{});
    }
    
  2. orelse - provide default:

    const value = maybe_value orelse 0;  // Use 0 if null
    
  3. orelse - early return:

    const value = maybe_value orelse return;  // Return if null
    
  4. .? - assert non-null (unsafe):

    const value = maybe_value.?;  // Panics if null
    

Combining with errors:

fn findUser(id: u32) !?User {
    // Can return error OR null OR User
    if (database_error) return error.DatabaseDown;
    if (user_not_found) return null;
    return user;
}

// Usage:
const user = try findUser(123) orelse return error.UserNotFound;

Key distinction from other languages:

  • null only exists for optional types (?T)
  • Regular pointers cannot be null in Zig
  • Optional pointers have same size as regular pointers (null is optimized)

Defer: Guaranteed Cleanup

defer executes code when scope exits, regardless of how it exits (return, error, etc.).

Basic usage:

fn processFile(path: []const u8) !void {
    const file = try std.fs.cwd().openFile(path, .{});
    defer file.close();  // Guaranteed to run when function exits

    // Work with file...
    // file.close() automatically called
}

Multiple defers execute in reverse order (LIFO):

defer std.debug.print("Third\n", .{});
defer std.debug.print("Second\n", .{});
defer std.debug.print("First\n", .{});
// Prints: First, Second, Third

errdefer - cleanup only on error:

fn createWidget() !*Widget {
    const widget = try allocator.create(Widget);
    errdefer allocator.destroy(widget);  // Only if function errors

    try widget.initialize();  // If this fails, widget is destroyed
    return widget;
}

Key patterns:

  • Use defer immediately after acquiring resource
  • errdefer for partial initialization cleanup
  • Defers execute in reverse order of declaration
  • Scope-based, not function-based (works in blocks)

Basic Types and Values

Integer types:

  • Explicit bit sizes: i8, u8, i16, u16, i32, u32, i64, u64, i128, u128
  • Platform-dependent: isize, usize (pointer-sized integers)
  • Arbitrary bit width: i7, u24, etc. (any bit count)

Float types:

  • f16, f32, f64, f80, f128

Boolean:

  • bool - only true or false
  • No implicit integer conversion

Arrays and slices:

const array: [5]i32 = .{ 1, 2, 3, 4, 5 };     // Fixed-size array
const slice: []const i32 = &array;             // Slice references array

Struct initialization:

const Point = struct {
    x: f32,
    y: f32,
};

const point = Point{ .x = 1.0, .y = 2.0 };

Anonymous struct literals:

const point = .{ .x = 1.0, .y = 2.0 };  // Type inferred

Undefined and initialization:

  • undefined - uninitialized memory (performance optimization)
  • Use for arrays/buffers that will be filled immediately
  • Debug builds catch usage of undefined values

Control Flow Differences

if is an expression:

const value = if (condition) 42 else 0;

switch is exhaustive:

const value: i32 = 2;
const result = switch (value) {
    1 => "one",
    2 => "two",
    3, 4, 5 => "three to five",
    else => "other",
};

while with continue expressions:

var i: usize = 0;
while (i < 10) : (i += 1) {  // Continue expression runs each iteration
    // Loop body
}

for for arrays and slices:

for (items) |item| {
    std.debug.print("{}\n", .{item});
}

// With index:
for (items, 0..) |item, i| {
    std.debug.print("{}: {}\n", .{ i, item });
}

Working with Zig Code

Reading Zig Code

When examining Zig code:

  1. Look for comptime - indicates compile-time logic
  2. Check error types - functions with ! return errors
  3. Identify optionals - ?T types may be null
  4. Find defers - locate resource cleanup
  5. Read parameter types - comptime params vs runtime params

Common Patterns

Result type (error + optional):

fn operation() !?Value {
    // Returns: error, null, or Value
}

Allocator parameter:

fn createThing(allocator: std.mem.Allocator) !Thing {
    const data = try allocator.alloc(u8, size);
    // allocator passed explicitly, no hidden allocation
}

Payload capture:

if (maybe_value) |value| {
    // Use value
}

if (result) |value| {
    // Success case
} else |err| {
    // Error case
}

Transitioning from Other Languages

From C/C++:

  • No implicit casts - use explicit conversion (@intCast, etc.)
  • No undefined behavior in safe mode (checked in Debug/ReleaseSafe)
  • No hidden control flow (no exceptions, no operator overloading)
  • Comptime replaces templates/macros

From Rust:

  • No borrow checker - manual memory management with allocators
  • Explicit allocator passing instead of implicit lifetimes
  • Error unions instead of Result<T, E> enum
  • Optionals instead of Option<T> enum

From Go/Java/Python:

  • No garbage collector - manual memory management
  • Explicit error handling, no exceptions
  • No hidden allocations - all memory explicit
  • Value types by default, not references

Additional Resources

Reference Files

For detailed examples and advanced patterns:

  • references/comptime-patterns.md - Advanced comptime techniques, type introspection, generic patterns
  • references/error-patterns.md - Comprehensive error handling patterns, error sets, custom errors

Official Documentation

Quick Reference

ConceptSyntaxPurpose
Comptime parametercomptime T: typeGeneric/compile-time value
Error union!TFunction may return error or T
Optional?TValue may be null
Unwrap optionalvalue orelse defaultProvide default if null
Try errortry exprPropagate error or unwrap value
Catch errorexpr catch handlerHandle error case
Defer cleanupdefer exprExecute when scope exits
Error defererrdefer exprExecute only on error
Type introspection@typeInfo(T)Get compile-time type info

Tips for Success

  1. Embrace explicit error handling - no hidden exceptions improves reliability
  2. Use comptime for generics - replaces C++ templates with readable code
  3. Leverage defer for cleanup - simpler than try/finally or RAII
  4. Pass allocators explicitly - makes memory allocation visible
  5. Read the standard library - idiomatic patterns and common utilities
  6. Use Debug build mode - catches undefined behavior and safety violations

When working with Zig code, focus on understanding the explicit flow: where errors originate, how memory is allocated, and what executes at compile time versus runtime.