Skip to content

Appendix A. Zig Syntax Summary

This appendix is a quick map of Zig syntax. It is not a grammar. The full Zig grammar is part of the official language reference. Zig 0.16 also keeps the language small enough...

This appendix is a quick map of Zig syntax. It is not a grammar. The full Zig grammar is part of the official language reference. Zig 0.16 also keeps the language small enough that the grammar remains practical to read directly.

A.1 Source Files

A Zig source file is a container. It contains declarations.

const std = @import("std");

const max_count = 100;

fn add(a: i32, b: i32) i32 {
    return a + b;
}

pub fn main() void {
    std.debug.print("{d}\n", .{add(1, 2)});
}

Declarations at file scope are order-independent.

pub fn main() void {
    f();
}

fn f() void {}

A.2 Comments

// ordinary comment

/// documentation comment for the next declaration

//! documentation comment for the current container

Zig has no block comments.

A.3 Names

const name = value;
var count: usize = 0;

A name may be public.

pub const version = 1;
pub fn run() void {}

A name may use @"..." when it is not a normal identifier.

const @"type" = 123;

A.4 Imports

const std = @import("std");
const math = @import("math.zig");

An imported file is a struct-like namespace.

math.add(1, 2);

A.5 Constants and Variables

const x = 10;
var y: i32 = 20;

A const binding cannot be assigned again.

const x = 1;
// x = 2; // error

A var binding can be assigned.

var n: i32 = 0;
n = n + 1;

A.6 Basic Types

bool
void
noreturn
type
comptime_int
comptime_float

u8   i8
u16  i16
u32  i32
u64  i64
u128 i128
usize isize

f16
f32
f64
f80
f128

Integer types may also have explicit bit widths.

const small: u3 = 5;
const signed: i7 = -12;

A.7 Literals

const a = 123;
const b = 0xff;
const c = 0b1010;
const d = 1.25;
const e = true;
const f = false;
const g = null;
const h = undefined;

Character and string literals:

const ch = 'A';
const s = "hello";
const nl = '\n';

Multiline strings use \\.

const text =
    \\first line
    \\second line
;

A.8 Arrays

const a = [_]u8{ 1, 2, 3 };
const b: [3]u8 = .{ 1, 2, 3 };

Indexing:

const x = a[0];

Length:

const n = a.len;

Sentinel array:

const msg: [5:0]u8 = "hello".*;

A.9 Slices

const a = [_]u8{ 1, 2, 3, 4 };
const s = a[1..3];

A slice has a pointer and a length.

s.ptr
s.len

Open-ended slice:

const t = a[2..];

A.10 Strings

A string literal is a pointer to constant bytes.

const s = "hello";

Use {s} to print it.

std.debug.print("{s}\n", .{s});

Zig strings are byte sequences. Text encoding is a library concern.

A.11 Pointers

Single-item pointer:

var x: i32 = 10;
const p: *i32 = &x;
p.* = 20;

Pointer to constant data:

const p: *const i32 = &x;

Many-item pointer:

const p: [*]u8 = buffer.ptr;

Optional pointer:

var p: ?*Node = null;

A.12 Structs

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

Initialization:

const p = Point{ .x = 10, .y = 20 };

Field access:

const x = p.x;

Default field value:

const User = struct {
    id: u64,
    active: bool = true,
};

Methods are functions inside a struct.

const Point = struct {
    x: i32,
    y: i32,

    fn zero() Point {
        return .{ .x = 0, .y = 0 };
    }
};

A.13 Enums

const Color = enum {
    red,
    green,
    blue,
};

Use:

const c = Color.red;

Inferred enum value:

const c: Color = .red;

Enum with integer tag type:

const Mode = enum(u8) {
    read = 1,
    write = 2,
};

A.14 Unions

Plain union:

const Value = union {
    i: i32,
    f: f64,
};

Tagged union:

const Token = union(enum) {
    number: i64,
    name: []const u8,
    eof,
};

Switch on a tagged union:

switch (tok) {
    .number => |n| useNumber(n),
    .name => |s| useName(s),
    .eof => return,
}

A.15 Optionals

var x: ?i32 = null;
x = 10;

Unwrap with if.

if (x) |value| {
    std.debug.print("{d}\n", .{value});
}

Use orelse.

const value = x orelse 0;

Force unwrap:

const value = x.?;

Use force unwrap only when null would be a programmer error.

A.16 Errors

Error set:

const ParseError = error{
    Empty,
    InvalidDigit,
};

Error union:

fn parse() ParseError!i32 {
    return error.Empty;
}

Return success:

return 123;

Propagate error:

const n = try parse();

Handle error:

const n = parse() catch 0;

A.17 Functions

fn add(a: i32, b: i32) i32 {
    return a + b;
}

No return value:

fn clear() void {}

Error return:

fn read() !usize {
    return error.EndOfStream;
}

Compile-time parameter:

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

A.18 Blocks

A block groups statements.

{
    const x = 1;
    _ = x;
}

A labeled block can yield a value.

const x = blk: {
    const a = 10;
    break :blk a + 1;
};

A.19 if

if (x > 0) {
    positive();
} else {
    nonPositive();
}

if is an expression.

const sign = if (x < 0) -1 else 1;

Optional capture:

if (maybe) |value| {
    use(value);
} else {
    missing();
}

Error union capture:

if (parse()) |value| {
    use(value);
} else |err| {
    useError(err);
}

A.20 switch

switch (x) {
    0 => zero(),
    1 => one(),
    else => other(),
}

Multiple values:

switch (ch) {
    'a', 'e', 'i', 'o', 'u' => vowel(),
    else => consonant(),
}

Range:

switch (n) {
    0...9 => digit(),
    else => other(),
}

Capture:

switch (tok) {
    .number => |n| use(n),
    else => {},
}

A.21 while

var i: usize = 0;
while (i < 10) {
    i += 1;
}

Continue expression:

var i: usize = 0;
while (i < 10) : (i += 1) {
    use(i);
}

Optional loop:

while (next()) |item| {
    use(item);
}

Error union loop:

while (next()) |item| {
    use(item);
} else |err| {
    useError(err);
}

A.22 for

for (items) |item| {
    use(item);
}

Index:

for (items, 0..) |item, i| {
    use(i, item);
}

Mutable pointer iteration:

for (&items) |*item| {
    item.* += 1;
}

Multiple sequences:

for (a, b) |x, y| {
    use(x, y);
}

A.23 break and continue

while (true) {
    break;
}
while (condition()) {
    continue;
}

Labeled loop:

outer: while (true) {
    while (true) {
        break :outer;
    }
}

A.24 defer and errdefer

defer runs at scope exit.

{
    lock();
    defer unlock();

    work();
}

errdefer runs only when the scope returns an error.

fn create() !*Thing {
    const p = try allocThing();
    errdefer freeThing(p);

    try initThing(p);
    return p;
}

A.25 comptime

A comptime value is known during compilation.

fn Vec(comptime T: type, comptime n: usize) type {
    return struct {
        data: [n]T,
    };
}

Use:

const V3 = Vec(f32, 3);

Compile-time block:

comptime {
    _ = @import("std");
}

A.26 Inline Loops

inline for (.{ u8, u16, u32 }) |T| {
    _ = T;
}

inline unrolls the loop at compile time.

A.27 Anonymous Struct and Tuple Literals

Struct literal with known type:

const p: Point = .{ .x = 1, .y = 2 };

Tuple literal:

const args = .{ 1, "hello", true };

Format arguments are commonly passed this way.

std.debug.print("{d} {s}\n", .{ 10, "ok" });

A.28 Operators

Arithmetic:

+  -  *  /  %

Assignment:

=  +=  -=  *=  /=  %=

Comparison:

==  !=  <  <=  >  >=

Boolean:

and  or  !

Bitwise:

&  |  ^  ~  <<  >>

Pointer and field:

&x
p.*
x.y

Optional and error:

x orelse y
try f()
f() catch y

A.29 Builtin Functions

Builtin functions begin with @.

@import("std")
@This()
@TypeOf(x)
@sizeOf(T)
@alignOf(T)
@intCast(x)
@as(T, x)
@ptrCast(p)
@alignCast(p)
@panic("message")

A builtin is part of the language, not a normal library function.

A.30 Tests

const std = @import("std");

test "addition" {
    try std.testing.expect(1 + 1 == 2);
}

Run tests:

zig test file.zig

A test block may use any code allowed in a function body.

A.31 unreachable

switch (x) {
    0 => zero(),
    1 => one(),
    else => unreachable,
}

unreachable states that control cannot reach that point. Reaching it is a bug.

A.32 undefined

var x: i32 = undefined;

undefined gives a value no defined contents. It is used when the program will write the value before reading it.

var buf: [1024]u8 = undefined;

Reading undefined memory is a bug.

A.33 pub, extern, export, inline, noinline

pub fn f() void {}
extern fn puts(s: [*:0]const u8) c_int;
export fn add(a: i32, b: i32) i32 {
    return a + b;
}
inline fn small() void {}
noinline fn large() void {}

A.34 Container Declarations

A container may be a file, struct, enum, union, or opaque type.

const S = struct {
    const Self = @This();

    value: i32,

    fn get(self: Self) i32 {
        return self.value;
    }
};

Declarations inside a container are accessed with dot syntax.

S.get

A.35 Opaque Types

const Handle = opaque {};

An opaque type has unknown layout. It is useful for handles from C or private implementation details.

A.36 Packed and Extern Layout

Packed struct:

const Flags = packed struct {
    a: bool,
    b: bool,
    c: u6,
};

Extern struct:

const CPoint = extern struct {
    x: c_int,
    y: c_int,
};

Use extern for C ABI layout. Use packed for bit-level layout.

A.37 Common Program Shape

const std = @import("std");

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    var list = std.ArrayList(u8).init(allocator);
    defer list.deinit();

    try list.append('A');

    std.debug.print("{c}\n", .{list.items[0]});
}

This shows the usual parts: import, entry point, allocator, cleanup, error propagation, and printing.