⚡ Zig Guide LiveUnofficialbut fully verified
✓ Zig 0.17.0-dev.1516+8a4b5424dWhat's newOn an older Zig?

A Parser

The lexer produced a flat list. 1 + 2 * 3 is five tokens in a row, and a row has no shape: nothing in it says which operation happens first. The parser’s job is to find the shape, and the shape is a tree.

The tree for 1 + 2 * 3 has + at the top, because it is the last thing done. Its right operand is the whole of 2 * 3. Reading a tree is reading inwards-out, so the deepest node runs first, which means precedence is depth.

That reframes the problem usefully. You do not need a rule that says * binds tighter than +. You need * to end up deeper. And there is a way to write a parser where that happens without anything ever consulting a table.

The trick

Write one function per precedence level, loosest first, and have each one ask the next-tighter level for its operands.

comparison  ->  sum   ( "<" | "==" sum )*
sum         ->  product   ( "+" | "-" product )*
product     ->  unary   ( "*" | "/" unary )*
unary       ->  "-" unary | primary
primary     ->  number | name | "(" comparison ")"

sum cannot see a *. By the time it looks at the token stream, product has already consumed any multiplication and handed back a finished subtree. So the multiplication is inside the addition’s operand, which is exactly what “binds tighter” means. The call chain is the precedence table.

This is recursive descent, and it is what most real compilers use, because it is the only parsing technique you can read.

The program

const std = @import("std");
const lex = @import("lexer.zig");

pub const Kind = enum {
    number,
    variable,
    binary,
    unary,
    let,
    assign,
    print,
    @"if",
    @"while",
    block,
    fn_decl,
    call,
    ret,
};

/// Children are a linked list rather than a slice, so the whole tree lives in
/// one flat array and a node is a fixed size. `first` starts the list, `next`
/// continues it.
pub const Node = struct {
    kind: Kind,
    op: lex.Kind = .eof,
    value: i64 = 0,
    name: []const u8 = "",
    /// Operand, condition, or the value being assigned.
    a: ?u32 = null,
    /// Second operand, or the body of an if/while.
    b: ?u32 = null,
    /// The else branch.
    c: ?u32 = null,
    first: ?u32 = null,
    next: ?u32 = null,
};

/// Written out rather than inferred. These functions are mutually recursive
/// (`primary` re-enters `expression` for a parenthesised group), and inferred
/// error sets cannot close a cycle: the compiler reports a dependency loop.
pub const Error = error{ UnexpectedToken, TooManyNodes, TooManyTokens };

pub const Parser = struct {
    tokens: []const lex.Token,
    pos: usize = 0,
    nodes: []Node,
    used: u32 = 0,

    fn peek(p: *Parser) lex.Kind {
        return p.tokens[p.pos].kind;
    }

    fn accept(p: *Parser, kind: lex.Kind) bool {
        if (p.peek() != kind) return false;
        p.pos += 1;
        return true;
    }

    fn expect(p: *Parser, kind: lex.Kind) Error!lex.Token {
        if (p.peek() != kind) return error.UnexpectedToken;
        defer p.pos += 1;
        return p.tokens[p.pos];
    }

    fn add(p: *Parser, node: Node) Error!u32 {
        if (p.used == p.nodes.len) return error.TooManyNodes;
        p.nodes[p.used] = node;
        p.used += 1;
        return p.used - 1;
    }

    // Each level consumes its own operators and defers to the next one for the
    // operands. That call order *is* the precedence: `product` binds tighter
    // than `sum` because `sum` asks it for both sides before looking at `+`.

    pub fn expression(p: *Parser) Error!u32 {
        return p.comparison();
    }

    fn comparison(p: *Parser) Error!u32 {
        var left = try p.sum();
        while (p.peek() == .lt or p.peek() == .eq) {
            const op = p.peek();
            p.pos += 1;
            const right = try p.sum();
            left = try p.add(.{ .kind = .binary, .op = op, .a = left, .b = right });
        }
        return left;
    }

    fn sum(p: *Parser) Error!u32 {
        var left = try p.product();
        while (p.peek() == .plus or p.peek() == .minus) {
            const op = p.peek();
            p.pos += 1;
            const right = try p.product();
            left = try p.add(.{ .kind = .binary, .op = op, .a = left, .b = right });
        }
        return left;
    }

    fn product(p: *Parser) Error!u32 {
        var left = try p.unary();
        while (p.peek() == .star or p.peek() == .slash) {
            const op = p.peek();
            p.pos += 1;
            const right = try p.unary();
            left = try p.add(.{ .kind = .binary, .op = op, .a = left, .b = right });
        }
        return left;
    }

    fn unary(p: *Parser) Error!u32 {
        if (p.accept(.minus)) {
            return p.add(.{ .kind = .unary, .op = .minus, .a = try p.unary() });
        }
        return p.primary();
    }

    fn primary(p: *Parser) Error!u32 {
        const token = p.tokens[p.pos];
        switch (token.kind) {
            .number => {
                p.pos += 1;
                return p.add(.{ .kind = .number, .value = token.value });
            },
            .identifier => {
                p.pos += 1;
                // One token of lookahead decides between a name and a call.
                // This is the only place the expression grammar needs it.
                if (p.peek() != .lparen) {
                    return p.add(.{ .kind = .variable, .name = token.text });
                }
                p.pos += 1;
                const node = try p.add(.{ .kind = .call, .name = token.text });
                var last: ?u32 = null;
                while (p.peek() != .rparen) {
                    const arg = try p.expression();
                    if (last) |l| p.nodes[l].next = arg else p.nodes[node].first = arg;
                    last = arg;
                    if (!p.accept(.comma)) break;
                }
                _ = try p.expect(.rparen);
                return node;
            },
            // Parentheses need no precedence rule of their own. They re-enter
            // at the loosest level, which is what "override the grouping" means.
            .lparen => {
                p.pos += 1;
                const inner = try p.expression();
                _ = try p.expect(.rparen);
                return inner;
            },
            else => return error.UnexpectedToken,
        }
    }

    pub fn statement(p: *Parser) Error!u32 {
        if (p.accept(.kw_let)) {
            const name = try p.expect(.identifier);
            _ = try p.expect(.assign);
            const value = try p.expression();
            _ = try p.expect(.semi);
            return p.add(.{ .kind = .let, .name = name.text, .a = value });
        }
        if (p.accept(.kw_print)) {
            const value = try p.expression();
            _ = try p.expect(.semi);
            return p.add(.{ .kind = .print, .a = value });
        }
        if (p.accept(.kw_if)) {
            _ = try p.expect(.lparen);
            const cond = try p.expression();
            _ = try p.expect(.rparen);
            const then = try p.block();
            const other: ?u32 = if (p.accept(.kw_else)) try p.block() else null;
            return p.add(.{ .kind = .@"if", .a = cond, .b = then, .c = other });
        }
        if (p.accept(.kw_fn)) {
            const name = try p.expect(.identifier);
            _ = try p.expect(.lparen);
            const node = try p.add(.{ .kind = .fn_decl, .name = name.text });
            var last: ?u32 = null;
            while (p.peek() == .identifier) {
                const param = try p.expect(.identifier);
                const child = try p.add(.{ .kind = .variable, .name = param.text });
                if (last) |l| p.nodes[l].next = child else p.nodes[node].first = child;
                last = child;
                if (!p.accept(.comma)) break;
            }
            _ = try p.expect(.rparen);
            p.nodes[node].b = try p.block();
            return node;
        }
        if (p.accept(.kw_return)) {
            const value: ?u32 = if (p.peek() == .semi) null else try p.expression();
            _ = try p.expect(.semi);
            return p.add(.{ .kind = .ret, .a = value });
        }
        if (p.accept(.kw_while)) {
            _ = try p.expect(.lparen);
            const cond = try p.expression();
            _ = try p.expect(.rparen);
            const body = try p.block();
            return p.add(.{ .kind = .@"while", .a = cond, .b = body });
        }
        const name = try p.expect(.identifier);
        _ = try p.expect(.assign);
        const value = try p.expression();
        _ = try p.expect(.semi);
        return p.add(.{ .kind = .assign, .name = name.text, .a = value });
    }

    fn block(p: *Parser) Error!u32 {
        _ = try p.expect(.lbrace);
        const node = try p.add(.{ .kind = .block });
        try p.statementsUntil(node, .rbrace);
        _ = try p.expect(.rbrace);
        return node;
    }

    fn statementsUntil(p: *Parser, parent: u32, stop: lex.Kind) Error!void {
        var last: ?u32 = null;
        while (p.peek() != stop and p.peek() != .eof) {
            const child = try p.statement();
            if (last) |l| p.nodes[l].next = child else p.nodes[parent].first = child;
            last = child;
        }
    }

    pub fn program(p: *Parser) Error!u32 {
        const node = try p.add(.{ .kind = .block });
        try p.statementsUntil(node, .eof);
        return node;
    }
};

/// Print the tree as an S-expression, which is the shape with the syntax
/// removed: nesting is the only thing left, so precedence is visible.
pub fn show(nodes: []const Node, index: u32, out: *std.Io.Writer) !void {
    const node = nodes[index];
    switch (node.kind) {
        .number => try out.print("{d}", .{node.value}),
        .variable => try out.print("{s}", .{node.name}),
        .unary => {
            try out.writeAll("(neg ");
            try show(nodes, node.a.?, out);
            try out.writeAll(")");
        },
        .binary => {
            const symbol = switch (node.op) {
                .plus => "+",
                .minus => "-",
                .star => "*",
                .slash => "/",
                .lt => "<",
                .eq => "==",
                else => "?",
            };
            try out.print("({s} ", .{symbol});
            try show(nodes, node.a.?, out);
            try out.writeAll(" ");
            try show(nodes, node.b.?, out);
            try out.writeAll(")");
        },
        .let, .assign => {
            try out.print("({s} {s} ", .{ if (node.kind == .let) "let" else "set", node.name });
            try show(nodes, node.a.?, out);
            try out.writeAll(")");
        },
        .print => {
            try out.writeAll("(print ");
            try show(nodes, node.a.?, out);
            try out.writeAll(")");
        },
        .@"if" => {
            try out.writeAll("(if ");
            try show(nodes, node.a.?, out);
            try out.writeAll(" ");
            try show(nodes, node.b.?, out);
            if (node.c) |else_branch| {
                try out.writeAll(" ");
                try show(nodes, else_branch, out);
            }
            try out.writeAll(")");
        },
        .@"while" => {
            try out.writeAll("(while ");
            try show(nodes, node.a.?, out);
            try out.writeAll(" ");
            try show(nodes, node.b.?, out);
            try out.writeAll(")");
        },
        .call => {
            try out.print("(call {s}", .{node.name});
            var arg = node.first;
            while (arg) |a| : (arg = nodes[a].next) {
                try out.writeAll(" ");
                try show(nodes, a, out);
            }
            try out.writeAll(")");
        },
        .ret => {
            try out.writeAll("(return");
            if (node.a) |value| {
                try out.writeAll(" ");
                try show(nodes, value, out);
            }
            try out.writeAll(")");
        },
        .fn_decl => {
            try out.print("(fn {s} (", .{node.name});
            var param = node.first;
            var first = true;
            while (param) |pa| : (param = nodes[pa].next) {
                if (!first) try out.writeAll(" ");
                try out.print("{s}", .{nodes[pa].name});
                first = false;
            }
            try out.writeAll(") ");
            try show(nodes, node.b.?, out);
            try out.writeAll(")");
        },
        .block => {
            try out.writeAll("(block");
            var child = node.first;
            while (child) |c| : (child = nodes[c].next) {
                try out.writeAll(" ");
                try show(nodes, c, out);
            }
            try out.writeAll(")");
        },
    }
}

pub fn parse(source: []const u8, tokens: []lex.Token, nodes: []Node) !struct { root: u32, used: u32 } {
    const stream = try lex.tokenize(source, tokens);
    var p: Parser = .{ .tokens = stream, .nodes = nodes };
    return .{ .root = try p.program(), .used = p.used };
}

pub fn main(init: std.process.Init) !void {
    var buf: [2048]u8 = undefined;
    var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &buf);
    const out = &stdout_writer.interface;

    var tokens: [128]lex.Token = undefined;
    var nodes: [128]Node = undefined;

    // Precedence, with the syntax removed. Nothing consults a table: `*` ends
    // up deeper than `+` because `sum` called `product` first.
    try out.writeAll("precedence\n");
    for ([_][]const u8{ "1 + 2 * 3;", "(1 + 2) * 3;", "1 - 2 - 3;", "0 - -4;" }) |line| {
        const stream = try lex.tokenize(line, &tokens);
        var p: Parser = .{ .tokens = stream, .nodes = &nodes };
        const root = try p.expression();
        try out.print("  {s: <14} ", .{line});
        try show(&nodes, root, out);
        try out.writeAll("\n");
    }

    const source =
        \\let total = 0;
        \\while (total < 10) {
        \\  total = total + 3;
        \\}
        \\print total;
    ;

    const tree = try parse(source, &tokens, &nodes);
    try out.print("\nthe program, as a tree ({d} nodes)\n  ", .{tree.used});
    try show(&nodes, tree.root, out);
    try out.writeAll("\n");

    try out.flush();
}

What just happened

1 + 2 * 3 became (+ 1 (* 2 3)) and (1 + 2) * 3 became (* (+ 1 2) 3). Same five tokens in the first case as a naive left-to-right reading would give, and a completely different tree.

Parentheses needed no precedence rule. primary sees a ( and re-enters at the loosest level, then expects a ). That is the whole implementation, and it is why grouping works for any expression: it restarts the chain.

1 - 2 - 3 became (- (- 1 2) 3), not (- 1 (- 2 3)). Subtraction is left-associative, and the loop is what makes it so: each level consumes operators in a while, folding the result so far into the left side. Had it recursed on the right instead, the same grammar would produce the other tree and the answer would be -4 rather than 2. Right-associative operators, like exponentiation in languages that have it, are written exactly that way on purpose.

The whole tree is one flat array. A node holds indices, not pointers, and children hang off a first/next linked list. So the parser allocates nothing beyond the array it was handed, a node is a fixed size, and the tree can be copied or thrown away in one move. Real compilers do this too, for the cache behaviour as much as the simplicity.

The error set is written out rather than inferred. These functions are mutually recursive, since primary calls back into expression for a parenthesised group, and Zig cannot infer an error set around a cycle. The first version of this snippet failed to compile with dependency loop with length 6, which names the cycle rather than pointing at one function.

Check yourself

The grammar says while (cond) { ... } with the parentheses required. Given recursive descent, could the language drop them and accept while cond { ... }?

Yes, and many languages do. The parentheses are not doing any parsing work here: the condition ends where the { begins, and expression stops there on its own because { is not an operator it recognises. What the parentheses buy is a better error message when the condition is malformed, and a grammar that stays unambiguous if you later add a construct where an expression could be followed by a brace that means something else. Rust requires the braces and forbids the parentheses for exactly this reason.

If you have written C

The shape is identical, and this is one of the places where a C implementation and a Zig one differ least. The nodes would be a struct Node with a kind enum, a union of payloads, and struct Node * children.

Two differences worth naming. Using indices rather than pointers is a choice in both languages, and in C it is the one you make when you want to realloc the node array without invalidating everything pointing into it. Here it is also what lets the whole tree live in a fixed array with no allocator at all.

And C’s union payload would be untagged in practice, with the kind field telling you which member is live and nothing checking that you agreed. That is the classic source of a compiler bug that only shows up on one node type. Zig has tagged unions for exactly this, and the version above sidesteps the question by giving every node the same fields, which is fine at ten node kinds and would not be at fifty.

Next: an interpreter, which walks this tree and produces an answer.