// SPDX-License-Identifier: MIT
// Copyright (c) 2015-2020 Zig Contributors
// This file is part of [zig](https://ziglang.org/), which is MIT licensed.
// The MIT license requires this copyright notice to be included in all copies
// and substantial portions of the software.
const std = @import("../std.zig");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const ast = std.zig.ast;
const Node = ast.Node;
const Tree = ast.Tree;
const AstError = ast.Error;
const TokenIndex = ast.TokenIndex;
const NodeIndex = ast.NodeIndex;
const Token = std.zig.Token;

pub const Error = error{ParseError} || Allocator.Error;

/// Result should be freed with tree.deinit() when there are
/// no more references to any of the tokens or nodes.
pub fn parse(gpa: *Allocator, source: []const u8) Allocator.Error!*Tree {
    var token_ids = std.ArrayList(Token.Id).init(gpa);
    defer token_ids.deinit();
    var token_locs = std.ArrayList(Token.Loc).init(gpa);
    defer token_locs.deinit();

    // Empirically, the zig std lib has an 8:1 ratio of source bytes to token count.
    const estimated_token_count = source.len / 8;
    try token_ids.ensureCapacity(estimated_token_count);
    try token_locs.ensureCapacity(estimated_token_count);

    var tokenizer = std.zig.Tokenizer.init(source);
    while (true) {
        const token = tokenizer.next();
        try token_ids.append(token.id);
        try token_locs.append(token.loc);
        if (token.id == .Eof) break;
    }

    var parser: Parser = .{
        .source = source,
        .arena = std.heap.ArenaAllocator.init(gpa),
        .gpa = gpa,
        .token_ids = token_ids.items,
        .token_locs = token_locs.items,
        .errors = .{},
        .tok_i = 0,
    };
    defer parser.errors.deinit(gpa);
    errdefer parser.arena.deinit();

    while (token_ids.items[parser.tok_i] == .LineComment) parser.tok_i += 1;

    const root_node = try parser.parseRoot();

    const tree = try parser.arena.allocator.create(Tree);
    tree.* = .{
        .gpa = gpa,
        .source = source,
        .token_ids = token_ids.toOwnedSlice(),
        .token_locs = token_locs.toOwnedSlice(),
        .errors = parser.errors.toOwnedSlice(gpa),
        .root_node = root_node,
        .arena = parser.arena.state,
    };
    return tree;
}

/// Represents in-progress parsing, will be converted to an ast.Tree after completion.
const Parser = struct {
    arena: std.heap.ArenaAllocator,
    gpa: *Allocator,
    source: []const u8,
    token_ids: []const Token.Id,
    token_locs: []const Token.Loc,
    tok_i: TokenIndex,
    errors: std.ArrayListUnmanaged(AstError),

    /// Root <- skip ContainerMembers eof
    fn parseRoot(p: *Parser) Allocator.Error!*Node.Root {
        const decls = try parseContainerMembers(p, true);
        defer p.gpa.free(decls);

        // parseContainerMembers will try to skip as much
        // invalid tokens as it can so this can only be the EOF
        const eof_token = p.eatToken(.Eof).?;

        const decls_len = @intCast(NodeIndex, decls.len);
        const node = try Node.Root.create(&p.arena.allocator, decls_len, eof_token);
        std.mem.copy(*Node, node.decls(), decls);

        return node;
    }

    /// ContainerMembers
    ///     <- TestDecl ContainerMembers
    ///      / TopLevelComptime ContainerMembers
    ///      / KEYWORD_pub? TopLevelDecl ContainerMembers
    ///      / ContainerField COMMA ContainerMembers
    ///      / ContainerField
    ///      /
    fn parseContainerMembers(p: *Parser, top_level: bool) ![]*Node {
        var list = std.ArrayList(*Node).init(p.gpa);
        defer list.deinit();

        var field_state: union(enum) {
            /// no fields have been seen
            none,
            /// currently parsing fields
            seen,
            /// saw fields and then a declaration after them.
            /// payload is first token of previous declaration.
            end: TokenIndex,
            /// ther was a declaration between fields, don't report more errors
            err,
        } = .none;

        while (true) {
            if (try p.parseContainerDocComments()) |node| {
                try list.append(node);
                continue;
            }

            const doc_comments = try p.parseDocComment();

            if (p.parseTestDecl() catch |err| switch (err) {
                error.OutOfMemory => return error.OutOfMemory,
                error.ParseError => {
                    p.findNextContainerMember();
                    continue;
                },
            }) |node| {
                if (field_state == .seen) {
                    field_state = .{ .end = node.firstToken() };
                }
                node.cast(Node.TestDecl).?.doc_comments = doc_comments;
                try list.append(node);
                continue;
            }

            if (p.parseTopLevelComptime() catch |err| switch (err) {
                error.OutOfMemory => return error.OutOfMemory,
                error.ParseError => {
                    p.findNextContainerMember();
                    continue;
                },
            }) |node| {
                if (field_state == .seen) {
                    field_state = .{ .end = node.firstToken() };
                }
                node.cast(Node.Comptime).?.doc_comments = doc_comments;
                try list.append(node);
                continue;
            }

            const visib_token = p.eatToken(.Keyword_pub);

            if (p.parseTopLevelDecl(doc_comments, visib_token) catch |err| switch (err) {
                error.OutOfMemory => return error.OutOfMemory,
                error.ParseError => {
                    p.findNextContainerMember();
                    continue;
                },
            }) |node| {
                if (field_state == .seen) {
                    field_state = .{ .end = visib_token orelse node.firstToken() };
                }
                try list.append(node);
                continue;
            }

            if (visib_token != null) {
                try p.errors.append(p.gpa, .{
                    .ExpectedPubItem = .{ .token = p.tok_i },
                });
                // ignore this pub
                continue;
            }

            if (p.parseContainerField() catch |err| switch (err) {
                error.OutOfMemory => return error.OutOfMemory,
                error.ParseError => {
                    // attempt to recover
                    p.findNextContainerMember();
                    continue;
                },
            }) |node| {
                switch (field_state) {
                    .none => field_state = .seen,
                    .err, .seen => {},
                    .end => |tok| {
                        try p.errors.append(p.gpa, .{
                            .DeclBetweenFields = .{ .token = tok },
                        });
                        // continue parsing, error will be reported later
                        field_state = .err;
                    },
                }

                const field = node.cast(Node.ContainerField).?;
                field.doc_comments = doc_comments;
                try list.append(node);
                const comma = p.eatToken(.Comma) orelse {
                    // try to continue parsing
                    const index = p.tok_i;
                    p.findNextContainerMember();
                    const next = p.token_ids[p.tok_i];
                    switch (next) {
                        .Eof => {
                            // no invalid tokens were found
                            if (index == p.tok_i) break;

                            // Invalid tokens, add error and exit
                            try p.errors.append(p.gpa, .{
                                .ExpectedToken = .{ .token = index, .expected_id = .Comma },
                            });
                            break;
                        },
                        else => {
                            if (next == .RBrace) {
                                if (!top_level) break;
                                _ = p.nextToken();
                            }

                            // add error and continue
                            try p.errors.append(p.gpa, .{
                                .ExpectedToken = .{ .token = index, .expected_id = .Comma },
                            });
                            continue;
                        },
                    }
                };
                if (try p.parseAppendedDocComment(comma)) |appended_comment|
                    field.doc_comments = appended_comment;
                continue;
            }

            // Dangling doc comment
            if (doc_comments != null) {
                try p.errors.append(p.gpa, .{
                    .UnattachedDocComment = .{ .token = doc_comments.?.firstToken() },
                });
            }

            const next = p.token_ids[p.tok_i];
            switch (next) {
                .Eof => break,
                .Keyword_comptime => {
                    _ = p.nextToken();
                    try p.errors.append(p.gpa, .{
                        .ExpectedBlockOrField = .{ .token = p.tok_i },
                    });
                },
                else => {
                    const index = p.tok_i;
                    if (next == .RBrace) {
                        if (!top_level) break;
                        _ = p.nextToken();
                    }

                    // this was likely not supposed to end yet,
                    // try to find the next declaration
                    p.findNextContainerMember();
                    try p.errors.append(p.gpa, .{
                        .ExpectedContainerMembers = .{ .token = index },
                    });
                },
            }
        }

        return list.toOwnedSlice();
    }

    /// Attempts to find next container member by searching for certain tokens
    fn findNextContainerMember(p: *Parser) void {
        var level: u32 = 0;
        while (true) {
            const tok = p.nextToken();
            switch (p.token_ids[tok]) {
                // any of these can start a new top level declaration
                .Keyword_test,
                .Keyword_comptime,
                .Keyword_pub,
                .Keyword_export,
                .Keyword_extern,
                .Keyword_inline,
                .Keyword_noinline,
                .Keyword_usingnamespace,
                .Keyword_threadlocal,
                .Keyword_const,
                .Keyword_var,
                .Keyword_fn,
                .Identifier,
                => {
                    if (level == 0) {
                        p.putBackToken(tok);
                        return;
                    }
                },
                .Comma, .Semicolon => {
                    // this decl was likely meant to end here
                    if (level == 0) {
                        return;
                    }
                },
                .LParen, .LBracket, .LBrace => level += 1,
                .RParen, .RBracket => {
                    if (level != 0) level -= 1;
                },
                .RBrace => {
                    if (level == 0) {
                        // end of container, exit
                        p.putBackToken(tok);
                        return;
                    }
                    level -= 1;
                },
                .Eof => {
                    p.putBackToken(tok);
                    return;
                },
                else => {},
            }
        }
    }

    /// Attempts to find the next statement by searching for a semicolon
    fn findNextStmt(p: *Parser) void {
        var level: u32 = 0;
        while (true) {
            const tok = p.nextToken();
            switch (p.token_ids[tok]) {
                .LBrace => level += 1,
                .RBrace => {
                    if (level == 0) {
                        p.putBackToken(tok);
                        return;
                    }
                    level -= 1;
                },
                .Semicolon => {
                    if (level == 0) {
                        return;
                    }
                },
                .Eof => {
                    p.putBackToken(tok);
                    return;
                },
                else => {},
            }
        }
    }

    /// Eat a multiline container doc comment
    fn parseContainerDocComments(p: *Parser) !?*Node {
        if (p.eatToken(.ContainerDocComment)) |first_line| {
            while (p.eatToken(.ContainerDocComment)) |_| {}
            const node = try p.arena.allocator.create(Node.DocComment);
            node.* = .{ .first_line = first_line };
            return &node.base;
        }
        return null;
    }

    /// TestDecl <- KEYWORD_test STRINGLITERALSINGLE Block
    fn parseTestDecl(p: *Parser) !?*Node {
        const test_token = p.eatToken(.Keyword_test) orelse return null;
        const name_node = try p.expectNode(parseStringLiteralSingle, .{
            .ExpectedStringLiteral = .{ .token = p.tok_i },
        });
        const block_node = (try p.parseBlock(null)) orelse {
            try p.errors.append(p.gpa, .{ .ExpectedLBrace = .{ .token = p.tok_i } });
            return error.ParseError;
        };

        const test_node = try p.arena.allocator.create(Node.TestDecl);
        test_node.* = .{
            .doc_comments = null,
            .test_token = test_token,
            .name = name_node,
            .body_node = block_node,
        };
        return &test_node.base;
    }

    /// TopLevelComptime <- KEYWORD_comptime BlockExpr
    fn parseTopLevelComptime(p: *Parser) !?*Node {
        const tok = p.eatToken(.Keyword_comptime) orelse return null;
        const lbrace = p.eatToken(.LBrace) orelse {
            p.putBackToken(tok);
            return null;
        };
        p.putBackToken(lbrace);
        const block_node = try p.expectNode(parseBlockExpr, .{
            .ExpectedLabelOrLBrace = .{ .token = p.tok_i },
        });

        const comptime_node = try p.arena.allocator.create(Node.Comptime);
        comptime_node.* = .{
            .doc_comments = null,
            .comptime_token = tok,
            .expr = block_node,
        };
        return &comptime_node.base;
    }

    /// TopLevelDecl
    ///     <- (KEYWORD_export / KEYWORD_extern STRINGLITERALSINGLE? / (KEYWORD_inline / KEYWORD_noinline))? FnProto (SEMICOLON / Block)
    ///      / (KEYWORD_export / KEYWORD_extern STRINGLITERALSINGLE?)? KEYWORD_threadlocal? VarDecl
    ///      / KEYWORD_usingnamespace Expr SEMICOLON
    fn parseTopLevelDecl(p: *Parser, doc_comments: ?*Node.DocComment, visib_token: ?TokenIndex) !?*Node {
        var lib_name: ?*Node = null;
        const extern_export_inline_token = blk: {
            if (p.eatToken(.Keyword_export)) |token| break :blk token;
            if (p.eatToken(.Keyword_extern)) |token| {
                lib_name = try p.parseStringLiteralSingle();
                break :blk token;
            }
            if (p.eatToken(.Keyword_inline)) |token| break :blk token;
            if (p.eatToken(.Keyword_noinline)) |token| break :blk token;
            break :blk null;
        };

        if (try p.parseFnProto(.top_level, .{
            .doc_comments = doc_comments,
            .visib_token = visib_token,
            .extern_export_inline_token = extern_export_inline_token,
            .lib_name = lib_name,
        })) |node| {
            return node;
        }

        if (extern_export_inline_token) |token| {
            if (p.token_ids[token] == .Keyword_inline or
                p.token_ids[token] == .Keyword_noinline)
            {
                try p.errors.append(p.gpa, .{
                    .ExpectedFn = .{ .token = p.tok_i },
                });
                return error.ParseError;
            }
        }

        const thread_local_token = p.eatToken(.Keyword_threadlocal);

        if (try p.parseVarDecl(.{
            .doc_comments = doc_comments,
            .visib_token = visib_token,
            .thread_local_token = thread_local_token,
            .extern_export_token = extern_export_inline_token,
            .lib_name = lib_name,
        })) |node| {
            return node;
        }

        if (thread_local_token != null) {
            try p.errors.append(p.gpa, .{
                .ExpectedVarDecl = .{ .token = p.tok_i },
            });
            // ignore this and try again;
            return error.ParseError;
        }

        if (extern_export_inline_token) |token| {
            try p.errors.append(p.gpa, .{
                .ExpectedVarDeclOrFn = .{ .token = p.tok_i },
            });
            // ignore this and try again;
            return error.ParseError;
        }

        const use_token = p.eatToken(.Keyword_usingnamespace) orelse return null;
        const expr = try p.expectNode(parseExpr, .{
            .ExpectedExpr = .{ .token = p.tok_i },
        });
        const semicolon_token = try p.expectToken(.Semicolon);

        const node = try p.arena.allocator.create(Node.Use);
        node.* = .{
            .doc_comments = doc_comments orelse try p.parseAppendedDocComment(semicolon_token),
            .visib_token = visib_token,
            .use_token = use_token,
            .expr = expr,
            .semicolon_token = semicolon_token,
        };

        return &node.base;
    }

    /// FnProto <- KEYWORD_fn IDENTIFIER? LPAREN ParamDeclList RPAREN ByteAlign? LinkSection? EXCLAMATIONMARK? (Keyword_anytype / TypeExpr)
    fn parseFnProto(p: *Parser, level: enum { top_level, as_type }, fields: struct {
        doc_comments: ?*Node.DocComment = null,
        visib_token: ?TokenIndex = null,
        extern_export_inline_token: ?TokenIndex = null,
        lib_name: ?*Node = null,
    }) !?*Node {
        // TODO: Remove once extern/async fn rewriting is
        var is_async: ?void = null;
        var is_extern_prototype: ?void = null;
        const cc_token: ?TokenIndex = blk: {
            if (p.eatToken(.Keyword_extern)) |token| {
                is_extern_prototype = {};
                break :blk token;
            }
            if (p.eatToken(.Keyword_async)) |token| {
                is_async = {};
                break :blk token;
            }
            break :blk null;
        };
        const fn_token = p.eatToken(.Keyword_fn) orelse {
            if (cc_token) |token|
                p.putBackToken(token);
            return null;
        };
        const name_token = p.eatToken(.Identifier);
        const lparen = try p.expectToken(.LParen);
        const params = try p.parseParamDeclList();
        defer p.gpa.free(params);
        const var_args_token = p.eatToken(.Ellipsis3);
        const rparen = try p.expectToken(.RParen);
        const align_expr = try p.parseByteAlign();
        const section_expr = try p.parseLinkSection();
        const callconv_expr = try p.parseCallconv();
        const exclamation_token = p.eatToken(.Bang);

        const return_type_expr = (try p.parseAnyType()) orelse
            try p.expectNodeRecoverable(parseTypeExpr, .{
            // most likely the user forgot to specify the return type.
            // Mark return type as invalid and try to continue.
            .ExpectedReturnType = .{ .token = p.tok_i },
        });

        // TODO https://github.com/ziglang/zig/issues/3750
        const R = Node.FnProto.ReturnType;
        const return_type = if (return_type_expr == null)
            R{ .Invalid = rparen }
        else if (exclamation_token != null)
            R{ .InferErrorSet = return_type_expr.? }
        else
            R{ .Explicit = return_type_expr.? };

        const body_node: ?*Node = switch (level) {
            .top_level => blk: {
                if (p.eatToken(.Semicolon)) |_| {
                    break :blk null;
                }
                const body_block = (try p.parseBlock(null)) orelse {
                    // Since parseBlock only return error.ParseError on
                    // a missing '}' we can assume this function was
                    // supposed to end here.
                    try p.errors.append(p.gpa, .{ .ExpectedSemiOrLBrace = .{ .token = p.tok_i } });
                    break :blk null;
                };
                break :blk body_block;
            },
            .as_type => null,
        };

        const fn_proto_node = try Node.FnProto.create(&p.arena.allocator, .{
            .params_len = params.len,
            .fn_token = fn_token,
            .return_type = return_type,
        }, .{
            .doc_comments = fields.doc_comments,
            .visib_token = fields.visib_token,
            .name_token = name_token,
            .var_args_token = var_args_token,
            .extern_export_inline_token = fields.extern_export_inline_token,
            .body_node = body_node,
            .lib_name = fields.lib_name,
            .align_expr = align_expr,
            .section_expr = section_expr,
            .callconv_expr = callconv_expr,
            .is_extern_prototype = is_extern_prototype,
            .is_async = is_async,
        });
        std.mem.copy(Node.FnProto.ParamDecl, fn_proto_node.params(), params);

        return &fn_proto_node.base;
    }

    /// VarDecl <- (KEYWORD_const / KEYWORD_var) IDENTIFIER (COLON TypeExpr)? ByteAlign? LinkSection? (EQUAL Expr)? SEMICOLON
    fn parseVarDecl(p: *Parser, fields: struct {
        doc_comments: ?*Node.DocComment = null,
        visib_token: ?TokenIndex = null,
        thread_local_token: ?TokenIndex = null,
        extern_export_token: ?TokenIndex = null,
        lib_name: ?*Node = null,
        comptime_token: ?TokenIndex = null,
    }) !?*Node {
        const mut_token = p.eatToken(.Keyword_const) orelse
            p.eatToken(.Keyword_var) orelse
            return null;

        const name_token = try p.expectToken(.Identifier);
        const type_node = if (p.eatToken(.Colon) != null)
            try p.expectNode(parseTypeExpr, .{
                .ExpectedTypeExpr = .{ .token = p.tok_i },
            })
        else
            null;
        const align_node = try p.parseByteAlign();
        const section_node = try p.parseLinkSection();
        const eq_token = p.eatToken(.Equal);
        const init_node = if (eq_token != null) blk: {
            break :blk try p.expectNode(parseExpr, .{
                .ExpectedExpr = .{ .token = p.tok_i },
            });
        } else null;
        const semicolon_token = try p.expectToken(.Semicolon);

        const doc_comments = fields.doc_comments orelse try p.parseAppendedDocComment(semicolon_token);

        const node = try Node.VarDecl.create(&p.arena.allocator, .{
            .mut_token = mut_token,
            .name_token = name_token,
            .semicolon_token = semicolon_token,
        }, .{
            .doc_comments = doc_comments,
            .visib_token = fields.visib_token,
            .thread_local_token = fields.thread_local_token,
            .eq_token = eq_token,
            .comptime_token = fields.comptime_token,
            .extern_export_token = fields.extern_export_token,
            .lib_name = fields.lib_name,
            .type_node = type_node,
            .align_node = align_node,
            .section_node = section_node,
            .init_node = init_node,
        });
        return &node.base;
    }

    /// ContainerField <- KEYWORD_comptime? IDENTIFIER (COLON TypeExpr ByteAlign?)? (EQUAL Expr)?
    fn parseContainerField(p: *Parser) !?*Node {
        const comptime_token = p.eatToken(.Keyword_comptime);
        const name_token = p.eatToken(.Identifier) orelse {
            if (comptime_token) |t| p.putBackToken(t);
            return null;
        };

        var align_expr: ?*Node = null;
        var type_expr: ?*Node = null;
        if (p.eatToken(.Colon)) |_| {
            if (p.eatToken(.Keyword_anytype) orelse p.eatToken(.Keyword_var)) |anytype_tok| {
                const node = try p.arena.allocator.create(Node.OneToken);
                node.* = .{
                    .base = .{ .tag = .AnyType },
                    .token = anytype_tok,
                };
                type_expr = &node.base;
            } else {
                type_expr = try p.expectNode(parseTypeExpr, .{
                    .ExpectedTypeExpr = .{ .token = p.tok_i },
                });
                align_expr = try p.parseByteAlign();
            }
        }

        const value_expr = if (p.eatToken(.Equal)) |_|
            try p.expectNode(parseExpr, .{
                .ExpectedExpr = .{ .token = p.tok_i },
            })
        else
            null;

        const node = try p.arena.allocator.create(Node.ContainerField);
        node.* = .{
            .doc_comments = null,
            .comptime_token = comptime_token,
            .name_token = name_token,
            .type_expr = type_expr,
            .value_expr = value_expr,
            .align_expr = align_expr,
        };
        return &node.base;
    }

    /// Statement
    ///     <- KEYWORD_comptime? VarDecl
    ///      / KEYWORD_comptime BlockExprStatement
    ///      / KEYWORD_nosuspend BlockExprStatement
    ///      / KEYWORD_suspend (SEMICOLON / BlockExprStatement)
    ///      / KEYWORD_defer BlockExprStatement
    ///      / KEYWORD_errdefer Payload? BlockExprStatement
    ///      / IfStatement
    ///      / LabeledStatement
    ///      / SwitchExpr
    ///      / AssignExpr SEMICOLON
    fn parseStatement(p: *Parser) Error!?*Node {
        const comptime_token = p.eatToken(.Keyword_comptime);

        if (try p.parseVarDecl(.{
            .comptime_token = comptime_token,
        })) |node| {
            return node;
        }

        if (comptime_token) |token| {
            const block_expr = try p.expectNode(parseBlockExprStatement, .{
                .ExpectedBlockOrAssignment = .{ .token = p.tok_i },
            });

            const node = try p.arena.allocator.create(Node.Comptime);
            node.* = .{
                .doc_comments = null,
                .comptime_token = token,
                .expr = block_expr,
            };
            return &node.base;
        }

        if (p.eatToken(.Keyword_nosuspend)) |nosuspend_token| {
            const block_expr = try p.expectNode(parseBlockExprStatement, .{
                .ExpectedBlockOrAssignment = .{ .token = p.tok_i },
            });

            const node = try p.arena.allocator.create(Node.Nosuspend);
            node.* = .{
                .nosuspend_token = nosuspend_token,
                .expr = block_expr,
            };
            return &node.base;
        }

        if (p.eatToken(.Keyword_suspend)) |suspend_token| {
            const semicolon = p.eatToken(.Semicolon);

            const body_node = if (semicolon == null) blk: {
                break :blk try p.expectNode(parseBlockExprStatement, .{
                    .ExpectedBlockOrExpression = .{ .token = p.tok_i },
                });
            } else null;

            const node = try p.arena.allocator.create(Node.Suspend);
            node.* = .{
                .suspend_token = suspend_token,
                .body = body_node,
            };
            return &node.base;
        }

        const defer_token = p.eatToken(.Keyword_defer) orelse p.eatToken(.Keyword_errdefer);
        if (defer_token) |token| {
            const payload = if (p.token_ids[token] == .Keyword_errdefer)
                try p.parsePayload()
            else
                null;
            const expr_node = try p.expectNode(parseBlockExprStatement, .{
                .ExpectedBlockOrExpression = .{ .token = p.tok_i },
            });
            const node = try p.arena.allocator.create(Node.Defer);
            node.* = .{
                .defer_token = token,
                .expr = expr_node,
                .payload = payload,
            };
            return &node.base;
        }

        if (try p.parseIfStatement()) |node| return node;
        if (try p.parseLabeledStatement()) |node| return node;
        if (try p.parseSwitchExpr()) |node| return node;
        if (try p.parseAssignExpr()) |node| {
            _ = try p.expectTokenRecoverable(.Semicolon);
            return node;
        }

        return null;
    }

    /// IfStatement
    ///     <- IfPrefix BlockExpr ( KEYWORD_else Payload? Statement )?
    ///      / IfPrefix AssignExpr ( SEMICOLON / KEYWORD_else Payload? Statement )
    fn parseIfStatement(p: *Parser) !?*Node {
        const if_node = (try p.parseIfPrefix()) orelse return null;
        const if_prefix = if_node.cast(Node.If).?;

        const block_expr = (try p.parseBlockExpr());
        const assign_expr = if (block_expr == null)
            try p.expectNode(parseAssignExpr, .{
                .ExpectedBlockOrAssignment = .{ .token = p.tok_i },
            })
        else
            null;

        const semicolon = if (assign_expr != null) p.eatToken(.Semicolon) else null;

        const else_node = if (semicolon == null) blk: {
            const else_token = p.eatToken(.Keyword_else) orelse break :blk null;
            const payload = try p.parsePayload();
            const else_body = try p.expectNode(parseStatement, .{
                .InvalidToken = .{ .token = p.tok_i },
            });

            const node = try p.arena.allocator.create(Node.Else);
            node.* = .{
                .else_token = else_token,
                .payload = payload,
                .body = else_body,
            };

            break :blk node;
        } else null;

        if (block_expr) |body| {
            if_prefix.body = body;
            if_prefix.@"else" = else_node;
            return if_node;
        }

        if (assign_expr) |body| {
            if_prefix.body = body;
            if (semicolon != null) return if_node;
            if (else_node != null) {
                if_prefix.@"else" = else_node;
                return if_node;
            }
            try p.errors.append(p.gpa, .{
                .ExpectedSemiOrElse = .{ .token = p.tok_i },
            });
        }

        return if_node;
    }

    /// LabeledStatement <- BlockLabel? (Block / LoopStatement)
    fn parseLabeledStatement(p: *Parser) !?*Node {
        var colon: TokenIndex = undefined;
        const label_token = p.parseBlockLabel(&colon);

        if (try p.parseBlock(label_token)) |node| return node;

        if (try p.parseLoopStatement()) |node| {
            if (node.cast(Node.For)) |for_node| {
                for_node.label = label_token;
            } else if (node.cast(Node.While)) |while_node| {
                while_node.label = label_token;
            } else unreachable;
            return node;
        }

        if (label_token != null) {
            try p.errors.append(p.gpa, .{
                .ExpectedLabelable = .{ .token = p.tok_i },
            });
            return error.ParseError;
        }

        return null;
    }

    /// LoopStatement <- KEYWORD_inline? (ForStatement / WhileStatement)
    fn parseLoopStatement(p: *Parser) !?*Node {
        const inline_token = p.eatToken(.Keyword_inline);

        if (try p.parseForStatement()) |node| {
            node.cast(Node.For).?.inline_token = inline_token;
            return node;
        }

        if (try p.parseWhileStatement()) |node| {
            node.cast(Node.While).?.inline_token = inline_token;
            return node;
        }
        if (inline_token == null) return null;

        // If we've seen "inline", there should have been a "for" or "while"
        try p.errors.append(p.gpa, .{
            .ExpectedInlinable = .{ .token = p.tok_i },
        });
        return error.ParseError;
    }

    /// ForStatement
    ///     <- ForPrefix BlockExpr ( KEYWORD_else Statement )?
    ///      / ForPrefix AssignExpr ( SEMICOLON / KEYWORD_else Statement )
    fn parseForStatement(p: *Parser) !?*Node {
        const node = (try p.parseForPrefix()) orelse return null;
        const for_prefix = node.cast(Node.For).?;

        if (try p.parseBlockExpr()) |block_expr_node| {
            for_prefix.body = block_expr_node;

            if (p.eatToken(.Keyword_else)) |else_token| {
                const statement_node = try p.expectNode(parseStatement, .{
                    .InvalidToken = .{ .token = p.tok_i },
                });

                const else_node = try p.arena.allocator.create(Node.Else);
                else_node.* = .{
                    .else_token = else_token,
                    .payload = null,
                    .body = statement_node,
                };
                for_prefix.@"else" = else_node;

                return node;
            }

            return node;
        }

        for_prefix.body = try p.expectNode(parseAssignExpr, .{
            .ExpectedBlockOrAssignment = .{ .token = p.tok_i },
        });

        if (p.eatToken(.Semicolon) != null) return node;

        if (p.eatToken(.Keyword_else)) |else_token| {
            const statement_node = try p.expectNode(parseStatement, .{
                .ExpectedStatement = .{ .token = p.tok_i },
            });

            const else_node = try p.arena.allocator.create(Node.Else);
            else_node.* = .{
                .else_token = else_token,
                .payload = null,
                .body = statement_node,
            };
            for_prefix.@"else" = else_node;
            return node;
        }

        try p.errors.append(p.gpa, .{
            .ExpectedSemiOrElse = .{ .token = p.tok_i },
        });

        return node;
    }

    /// WhileStatement
    ///     <- WhilePrefix BlockExpr ( KEYWORD_else Payload? Statement )?
    ///      / WhilePrefix AssignExpr ( SEMICOLON / KEYWORD_else Payload? Statement )
    fn parseWhileStatement(p: *Parser) !?*Node {
        const node = (try p.parseWhilePrefix()) orelse return null;
        const while_prefix = node.cast(Node.While).?;

        if (try p.parseBlockExpr()) |block_expr_node| {
            while_prefix.body = block_expr_node;

            if (p.eatToken(.Keyword_else)) |else_token| {
                const payload = try p.parsePayload();

                const statement_node = try p.expectNode(parseStatement, .{
                    .InvalidToken = .{ .token = p.tok_i },
                });

                const else_node = try p.arena.allocator.create(Node.Else);
                else_node.* = .{
                    .else_token = else_token,
                    .payload = payload,
                    .body = statement_node,
                };
                while_prefix.@"else" = else_node;

                return node;
            }

            return node;
        }

        while_prefix.body = try p.expectNode(parseAssignExpr, .{
            .ExpectedBlockOrAssignment = .{ .token = p.tok_i },
        });

        if (p.eatToken(.Semicolon) != null) return node;

        if (p.eatToken(.Keyword_else)) |else_token| {
            const payload = try p.parsePayload();

            const statement_node = try p.expectNode(parseStatement, .{
                .ExpectedStatement = .{ .token = p.tok_i },
            });

            const else_node = try p.arena.allocator.create(Node.Else);
            else_node.* = .{
                .else_token = else_token,
                .payload = payload,
                .body = statement_node,
            };
            while_prefix.@"else" = else_node;
            return node;
        }

        try p.errors.append(p.gpa, .{
            .ExpectedSemiOrElse = .{ .token = p.tok_i },
        });

        return node;
    }

    /// BlockExprStatement
    ///     <- BlockExpr
    ///      / AssignExpr SEMICOLON
    fn parseBlockExprStatement(p: *Parser) !?*Node {
        if (try p.parseBlockExpr()) |node| return node;
        if (try p.parseAssignExpr()) |node| {
            _ = try p.expectTokenRecoverable(.Semicolon);
            return node;
        }
        return null;
    }

    /// BlockExpr <- BlockLabel? Block
    fn parseBlockExpr(p: *Parser) Error!?*Node {
        var colon: TokenIndex = undefined;
        const label_token = p.parseBlockLabel(&colon);
        const block_node = (try p.parseBlock(label_token)) orelse {
            if (label_token) |label| {
                p.putBackToken(label + 1); // ":"
                p.putBackToken(label); // IDENTIFIER
            }
            return null;
        };
        return block_node;
    }

    /// AssignExpr <- Expr (AssignOp Expr)?
    fn parseAssignExpr(p: *Parser) !?*Node {
        return p.parseBinOpExpr(parseAssignOp, parseExpr, .Once);
    }

    /// Expr <- KEYWORD_try* BoolOrExpr
    fn parseExpr(p: *Parser) Error!?*Node {
        return p.parsePrefixOpExpr(parseTry, parseBoolOrExpr);
    }

    /// BoolOrExpr <- BoolAndExpr (KEYWORD_or BoolAndExpr)*
    fn parseBoolOrExpr(p: *Parser) !?*Node {
        return p.parseBinOpExpr(
            SimpleBinOpParseFn(.Keyword_or, .BoolOr),
            parseBoolAndExpr,
            .Infinitely,
        );
    }

    /// BoolAndExpr <- CompareExpr (KEYWORD_and CompareExpr)*
    fn parseBoolAndExpr(p: *Parser) !?*Node {
        return p.parseBinOpExpr(
            SimpleBinOpParseFn(.Keyword_and, .BoolAnd),
            parseCompareExpr,
            .Infinitely,
        );
    }

    /// CompareExpr <- BitwiseExpr (CompareOp BitwiseExpr)?
    fn parseCompareExpr(p: *Parser) !?*Node {
        return p.parseBinOpExpr(parseCompareOp, parseBitwiseExpr, .Once);
    }

    /// BitwiseExpr <- BitShiftExpr (BitwiseOp BitShiftExpr)*
    fn parseBitwiseExpr(p: *Parser) !?*Node {
        return p.parseBinOpExpr(parseBitwiseOp, parseBitShiftExpr, .Infinitely);
    }

    /// BitShiftExpr <- AdditionExpr (BitShiftOp AdditionExpr)*
    fn parseBitShiftExpr(p: *Parser) !?*Node {
        return p.parseBinOpExpr(parseBitShiftOp, parseAdditionExpr, .Infinitely);
    }

    /// AdditionExpr <- MultiplyExpr (AdditionOp MultiplyExpr)*
    fn parseAdditionExpr(p: *Parser) !?*Node {
        return p.parseBinOpExpr(parseAdditionOp, parseMultiplyExpr, .Infinitely);
    }

    /// MultiplyExpr <- PrefixExpr (MultiplyOp PrefixExpr)*
    fn parseMultiplyExpr(p: *Parser) !?*Node {
        return p.parseBinOpExpr(parseMultiplyOp, parsePrefixExpr, .Infinitely);
    }

    /// PrefixExpr <- PrefixOp* PrimaryExpr
    fn parsePrefixExpr(p: *Parser) !?*Node {
        return p.parsePrefixOpExpr(parsePrefixOp, parsePrimaryExpr);
    }

    /// PrimaryExpr
    ///     <- AsmExpr
    ///      / IfExpr
    ///      / KEYWORD_break BreakLabel? Expr?
    ///      / KEYWORD_comptime Expr
    ///      / KEYWORD_nosuspend Expr
    ///      / KEYWORD_continue BreakLabel?
    ///      / KEYWORD_resume Expr
    ///      / KEYWORD_return Expr?
    ///      / BlockLabel? LoopExpr
    ///      / Block
    ///      / CurlySuffixExpr
    fn parsePrimaryExpr(p: *Parser) !?*Node {
        if (try p.parseAsmExpr()) |node| return node;
        if (try p.parseIfExpr()) |node| return node;

        if (p.eatToken(.Keyword_break)) |token| {
            const label = try p.parseBreakLabel();
            const expr_node = try p.parseExpr();
            const node = try Node.ControlFlowExpression.create(&p.arena.allocator, .{
                .tag = .Break,
                .ltoken = token,
            }, .{
                .label = label,
                .rhs = expr_node,
            });
            return &node.base;
        }

        if (p.eatToken(.Keyword_comptime)) |token| {
            const expr_node = try p.expectNode(parseExpr, .{
                .ExpectedExpr = .{ .token = p.tok_i },
            });
            const node = try p.arena.allocator.create(Node.Comptime);
            node.* = .{
                .doc_comments = null,
                .comptime_token = token,
                .expr = expr_node,
            };
            return &node.base;
        }

        if (p.eatToken(.Keyword_nosuspend)) |token| {
            const expr_node = try p.expectNode(parseExpr, .{
                .ExpectedExpr = .{ .token = p.tok_i },
            });
            const node = try p.arena.allocator.create(Node.Nosuspend);
            node.* = .{
                .nosuspend_token = token,
                .expr = expr_node,
            };
            return &node.base;
        }

        if (p.eatToken(.Keyword_continue)) |token| {
            const label = try p.parseBreakLabel();
            const node = try Node.ControlFlowExpression.create(&p.arena.allocator, .{
                .tag = .Continue,
                .ltoken = token,
            }, .{
                .label = label,
                .rhs = null,
            });
            return &node.base;
        }

        if (p.eatToken(.Keyword_resume)) |token| {
            const expr_node = try p.expectNode(parseExpr, .{
                .ExpectedExpr = .{ .token = p.tok_i },
            });
            const node = try p.arena.allocator.create(Node.SimplePrefixOp);
            node.* = .{
                .base = .{ .tag = .Resume },
                .op_token = token,
                .rhs = expr_node,
            };
            return &node.base;
        }

        if (p.eatToken(.Keyword_return)) |token| {
            const expr_node = try p.parseExpr();
            const node = try Node.ControlFlowExpression.create(&p.arena.allocator, .{
                .tag = .Return,
                .ltoken = token,
            }, .{
                .rhs = expr_node,
            });
            return &node.base;
        }

        var colon: TokenIndex = undefined;
        const label = p.parseBlockLabel(&colon);
        if (try p.parseLoopExpr()) |node| {
            if (node.cast(Node.For)) |for_node| {
                for_node.label = label;
            } else if (node.cast(Node.While)) |while_node| {
                while_node.label = label;
            } else unreachable;
            return node;
        }
        if (label) |token| {
            p.putBackToken(token + 1); // ":"
            p.putBackToken(token); // IDENTIFIER
        }

        if (try p.parseBlock(null)) |node| return node;
        if (try p.parseCurlySuffixExpr()) |node| return node;

        return null;
    }

    /// IfExpr <- IfPrefix Expr (KEYWORD_else Payload? Expr)?
    fn parseIfExpr(p: *Parser) !?*Node {
        return p.parseIf(parseExpr);
    }

    /// Block <- LBRACE Statement* RBRACE
    fn parseBlock(p: *Parser, label_token: ?TokenIndex) !?*Node {
        const lbrace = p.eatToken(.LBrace) orelse return null;

        var statements = std.ArrayList(*Node).init(p.gpa);
        defer statements.deinit();

        while (true) {
            const statement = (p.parseStatement() catch |err| switch (err) {
                error.OutOfMemory => return error.OutOfMemory,
                error.ParseError => {
                    // try to skip to the next statement
                    p.findNextStmt();
                    continue;
                },
            }) orelse break;
            try statements.append(statement);
        }

        const rbrace = try p.expectToken(.RBrace);

        const statements_len = @intCast(NodeIndex, statements.items.len);

        if (label_token) |label| {
            const block_node = try Node.LabeledBlock.alloc(&p.arena.allocator, statements_len);
            block_node.* = .{
                .label = label,
                .lbrace = lbrace,
                .statements_len = statements_len,
                .rbrace = rbrace,
            };
            std.mem.copy(*Node, block_node.statements(), statements.items);
            return &block_node.base;
        } else {
            const block_node = try Node.Block.alloc(&p.arena.allocator, statements_len);
            block_node.* = .{
                .lbrace = lbrace,
                .statements_len = statements_len,
                .rbrace = rbrace,
            };
            std.mem.copy(*Node, block_node.statements(), statements.items);
            return &block_node.base;
        }
    }

    /// LoopExpr <- KEYWORD_inline? (ForExpr / WhileExpr)
    fn parseLoopExpr(p: *Parser) !?*Node {
        const inline_token = p.eatToken(.Keyword_inline);

        if (try p.parseForExpr()) |node| {
            node.cast(Node.For).?.inline_token = inline_token;
            return node;
        }

        if (try p.parseWhileExpr()) |node| {
            node.cast(Node.While).?.inline_token = inline_token;
            return node;
        }

        if (inline_token == null) return null;

        // If we've seen "inline", there should have been a "for" or "while"
        try p.errors.append(p.gpa, .{
            .ExpectedInlinable = .{ .token = p.tok_i },
        });
        return error.ParseError;
    }

    /// ForExpr <- ForPrefix Expr (KEYWORD_else Expr)?
    fn parseForExpr(p: *Parser) !?*Node {
        const node = (try p.parseForPrefix()) orelse return null;
        const for_prefix = node.cast(Node.For).?;

        const body_node = try p.expectNode(parseExpr, .{
            .ExpectedExpr = .{ .token = p.tok_i },
        });
        for_prefix.body = body_node;

        if (p.eatToken(.Keyword_else)) |else_token| {
            const body = try p.expectNode(parseExpr, .{
                .ExpectedExpr = .{ .token = p.tok_i },
            });

            const else_node = try p.arena.allocator.create(Node.Else);
            else_node.* = .{
                .else_token = else_token,
                .payload = null,
                .body = body,
            };

            for_prefix.@"else" = else_node;
        }

        return node;
    }

    /// WhileExpr <- WhilePrefix Expr (KEYWORD_else Payload? Expr)?
    fn parseWhileExpr(p: *Parser) !?*Node {
        const node = (try p.parseWhilePrefix()) orelse return null;
        const while_prefix = node.cast(Node.While).?;

        const body_node = try p.expectNode(parseExpr, .{
            .ExpectedExpr = .{ .token = p.tok_i },
        });
        while_prefix.body = body_node;

        if (p.eatToken(.Keyword_else)) |else_token| {
            const payload = try p.parsePayload();
            const body = try p.expectNode(parseExpr, .{
                .ExpectedExpr = .{ .token = p.tok_i },
            });

            const else_node = try p.arena.allocator.create(Node.Else);
            else_node.* = .{
                .else_token = else_token,
                .payload = payload,
                .body = body,
            };

            while_prefix.@"else" = else_node;
        }

        return node;
    }

    /// CurlySuffixExpr <- TypeExpr InitList?
    fn parseCurlySuffixExpr(p: *Parser) !?*Node {
        const lhs = (try p.parseTypeExpr()) orelse return null;
        const suffix_op = (try p.parseInitList(lhs)) orelse return lhs;
        return suffix_op;
    }

    /// InitList
    ///     <- LBRACE FieldInit (COMMA FieldInit)* COMMA? RBRACE
    ///      / LBRACE Expr (COMMA Expr)* COMMA? RBRACE
    ///      / LBRACE RBRACE
    fn parseInitList(p: *Parser, lhs: *Node) !?*Node {
        const lbrace = p.eatToken(.LBrace) orelse return null;
        var init_list = std.ArrayList(*Node).init(p.gpa);
        defer init_list.deinit();

        if (try p.parseFieldInit()) |field_init| {
            try init_list.append(field_init);
            while (p.eatToken(.Comma)) |_| {
                const next = (try p.parseFieldInit()) orelse break;
                try init_list.append(next);
            }
            const node = try Node.StructInitializer.alloc(&p.arena.allocator, init_list.items.len);
            node.* = .{
                .lhs = lhs,
                .rtoken = try p.expectToken(.RBrace),
                .list_len = init_list.items.len,
            };
            std.mem.copy(*Node, node.list(), init_list.items);
            return &node.base;
        }

        if (try p.parseExpr()) |expr| {
            try init_list.append(expr);
            while (p.eatToken(.Comma)) |_| {
                const next = (try p.parseExpr()) orelse break;
                try init_list.append(next);
            }
            const node = try Node.ArrayInitializer.alloc(&p.arena.allocator, init_list.items.len);
            node.* = .{
                .lhs = lhs,
                .rtoken = try p.expectToken(.RBrace),
                .list_len = init_list.items.len,
            };
            std.mem.copy(*Node, node.list(), init_list.items);
            return &node.base;
        }

        const node = try p.arena.allocator.create(Node.StructInitializer);
        node.* = .{
            .lhs = lhs,
            .rtoken = try p.expectToken(.RBrace),
            .list_len = 0,
        };
        return &node.base;
    }

    /// InitList
    ///     <- LBRACE FieldInit (COMMA FieldInit)* COMMA? RBRACE
    ///      / LBRACE Expr (COMMA Expr)* COMMA? RBRACE
    ///      / LBRACE RBRACE
    fn parseAnonInitList(p: *Parser, dot: TokenIndex) !?*Node {
        const lbrace = p.eatToken(.LBrace) orelse return null;
        var init_list = std.ArrayList(*Node).init(p.gpa);
        defer init_list.deinit();

        if (try p.parseFieldInit()) |field_init| {
            try init_list.append(field_init);
            while (p.eatToken(.Comma)) |_| {
                const next = (try p.parseFieldInit()) orelse break;
                try init_list.append(next);
            }
            const node = try Node.StructInitializerDot.alloc(&p.arena.allocator, init_list.items.len);
            node.* = .{
                .dot = dot,
                .rtoken = try p.expectToken(.RBrace),
                .list_len = init_list.items.len,
            };
            std.mem.copy(*Node, node.list(), init_list.items);
            return &node.base;
        }

        if (try p.parseExpr()) |expr| {
            try init_list.append(expr);
            while (p.eatToken(.Comma)) |_| {
                const next = (try p.parseExpr()) orelse break;
                try init_list.append(next);
            }
            const node = try Node.ArrayInitializerDot.alloc(&p.arena.allocator, init_list.items.len);
            node.* = .{
                .dot = dot,
                .rtoken = try p.expectToken(.RBrace),
                .list_len = init_list.items.len,
            };
            std.mem.copy(*Node, node.list(), init_list.items);
            return &node.base;
        }

        const node = try p.arena.allocator.create(Node.StructInitializerDot);
        node.* = .{
            .dot = dot,
            .rtoken = try p.expectToken(.RBrace),
            .list_len = 0,
        };
        return &node.base;
    }

    /// TypeExpr <- PrefixTypeOp* ErrorUnionExpr
    fn parseTypeExpr(p: *Parser) Error!?*Node {
        return p.parsePrefixOpExpr(parsePrefixTypeOp, parseErrorUnionExpr);
    }

    /// ErrorUnionExpr <- SuffixExpr (EXCLAMATIONMARK TypeExpr)?
    fn parseErrorUnionExpr(p: *Parser) !?*Node {
        const suffix_expr = (try p.parseSuffixExpr()) orelse return null;

        if (try SimpleBinOpParseFn(.Bang, .ErrorUnion)(p)) |node| {
            const error_union = node.castTag(.ErrorUnion).?;
            const type_expr = try p.expectNode(parseTypeExpr, .{
                .ExpectedTypeExpr = .{ .token = p.tok_i },
            });
            error_union.lhs = suffix_expr;
            error_union.rhs = type_expr;
            return node;
        }

        return suffix_expr;
    }

    /// SuffixExpr
    ///     <- KEYWORD_async PrimaryTypeExpr SuffixOp* FnCallArguments
    ///      / PrimaryTypeExpr (SuffixOp / FnCallArguments)*
    fn parseSuffixExpr(p: *Parser) !?*Node {
        const maybe_async = p.eatToken(.Keyword_async);
        if (maybe_async) |async_token| {
            const token_fn = p.eatToken(.Keyword_fn);
            if (token_fn != null) {
                // TODO: remove this hack when async fn rewriting is
                // HACK: If we see the keyword `fn`, then we assume that
                //       we are parsing an async fn proto, and not a call.
                //       We therefore put back all tokens consumed by the async
                //       prefix...
                p.putBackToken(token_fn.?);
                p.putBackToken(async_token);
                return p.parsePrimaryTypeExpr();
            }
            var res = try p.expectNode(parsePrimaryTypeExpr, .{
                .ExpectedPrimaryTypeExpr = .{ .token = p.tok_i },
            });

            while (try p.parseSuffixOp(res)) |node| {
                res = node;
            }

            const params = (try p.parseFnCallArguments()) orelse {
                try p.errors.append(p.gpa, .{
                    .ExpectedParamList = .{ .token = p.tok_i },
                });
                // ignore this, continue parsing
                return res;
            };
            defer p.gpa.free(params.list);
            const node = try Node.Call.alloc(&p.arena.allocator, params.list.len);
            node.* = .{
                .lhs = res,
                .params_len = params.list.len,
                .async_token = async_token,
                .rtoken = params.rparen,
            };
            std.mem.copy(*Node, node.params(), params.list);
            return &node.base;
        }
        if (try p.parsePrimaryTypeExpr()) |expr| {
            var res = expr;

            while (true) {
                if (try p.parseSuffixOp(res)) |node| {
                    res = node;
                    continue;
                }
                if (try p.parseFnCallArguments()) |params| {
                    defer p.gpa.free(params.list);
                    const call = try Node.Call.alloc(&p.arena.allocator, params.list.len);
                    call.* = .{
                        .lhs = res,
                        .params_len = params.list.len,
                        .async_token = null,
                        .rtoken = params.rparen,
                    };
                    std.mem.copy(*Node, call.params(), params.list);
                    res = &call.base;
                    continue;
                }
                break;
            }
            return res;
        }

        return null;
    }

    /// PrimaryTypeExpr
    ///     <- BUILTINIDENTIFIER FnCallArguments
    ///      / CHAR_LITERAL
    ///      / ContainerDecl
    ///      / DOT IDENTIFIER
    ///      / ErrorSetDecl
    ///      / FLOAT
    ///      / FnProto
    ///      / GroupedExpr
    ///      / LabeledTypeExpr
    ///      / IDENTIFIER
    ///      / IfTypeExpr
    ///      / INTEGER
    ///      / KEYWORD_comptime TypeExpr
    ///      / KEYWORD_error DOT IDENTIFIER
    ///      / KEYWORD_false
    ///      / KEYWORD_null
    ///      / KEYWORD_anyframe
    ///      / KEYWORD_true
    ///      / KEYWORD_undefined
    ///      / KEYWORD_unreachable
    ///      / STRINGLITERAL
    ///      / SwitchExpr
    fn parsePrimaryTypeExpr(p: *Parser) !?*Node {
        if (try p.parseBuiltinCall()) |node| return node;
        if (p.eatToken(.CharLiteral)) |token| {
            const node = try p.arena.allocator.create(Node.OneToken);
            node.* = .{
                .base = .{ .tag = .CharLiteral },
                .token = token,
            };
            return &node.base;
        }
        if (try p.parseContainerDecl()) |node| return node;
        if (try p.parseAnonLiteral()) |node| return node;
        if (try p.parseErrorSetDecl()) |node| return node;
        if (try p.parseFloatLiteral()) |node| return node;
        if (try p.parseFnProto(.as_type, .{})) |node| return node;
        if (try p.parseGroupedExpr()) |node| return node;
        if (try p.parseLabeledTypeExpr()) |node| return node;
        if (try p.parseIdentifier()) |node| return node;
        if (try p.parseIfTypeExpr()) |node| return node;
        if (try p.parseIntegerLiteral()) |node| return node;
        if (p.eatToken(.Keyword_comptime)) |token| {
            const expr = (try p.parseTypeExpr()) orelse return null;
            const node = try p.arena.allocator.create(Node.Comptime);
            node.* = .{
                .doc_comments = null,
                .comptime_token = token,
                .expr = expr,
            };
            return &node.base;
        }
        if (p.eatToken(.Keyword_error)) |token| {
            const period = try p.expectTokenRecoverable(.Period);
            const identifier = try p.expectNodeRecoverable(parseIdentifier, .{
                .ExpectedIdentifier = .{ .token = p.tok_i },
            });
            const global_error_set = try p.createLiteral(.ErrorType, token);
            if (period == null or identifier == null) return global_error_set;

            const node = try p.arena.allocator.create(Node.SimpleInfixOp);
            node.* = .{
                .base = Node{ .tag = .Period },
                .op_token = period.?,
                .lhs = global_error_set,
                .rhs = identifier.?,
            };
            return &node.base;
        }
        if (p.eatToken(.Keyword_false)) |token| return p.createLiteral(.BoolLiteral, token);
        if (p.eatToken(.Keyword_null)) |token| return p.createLiteral(.NullLiteral, token);
        if (p.eatToken(.Keyword_anyframe)) |token| {
            const node = try p.arena.allocator.create(Node.AnyFrameType);
            node.* = .{
                .anyframe_token = token,
                .result = null,
            };
            return &node.base;
        }
        if (p.eatToken(.Keyword_true)) |token| return p.createLiteral(.BoolLiteral, token);
        if (p.eatToken(.Keyword_undefined)) |token| return p.createLiteral(.UndefinedLiteral, token);
        if (p.eatToken(.Keyword_unreachable)) |token| return p.createLiteral(.Unreachable, token);
        if (try p.parseStringLiteral()) |node| return node;
        if (try p.parseSwitchExpr()) |node| return node;

        return null;
    }

    /// ContainerDecl <- (KEYWORD_extern / KEYWORD_packed)? ContainerDeclAuto
    fn parseContainerDecl(p: *Parser) !?*Node {
        const layout_token = p.eatToken(.Keyword_extern) orelse
            p.eatToken(.Keyword_packed);

        const node = (try p.parseContainerDeclAuto()) orelse {
            if (layout_token) |token|
                p.putBackToken(token);
            return null;
        };
        node.cast(Node.ContainerDecl).?.*.layout_token = layout_token;
        return node;
    }

    /// ErrorSetDecl <- KEYWORD_error LBRACE IdentifierList RBRACE
    fn parseErrorSetDecl(p: *Parser) !?*Node {
        const error_token = p.eatToken(.Keyword_error) orelse return null;
        if (p.eatToken(.LBrace) == null) {
            // Might parse as `KEYWORD_error DOT IDENTIFIER` later in PrimaryTypeExpr, so don't error
            p.putBackToken(error_token);
            return null;
        }
        const decls = try p.parseErrorTagList();
        defer p.gpa.free(decls);
        const rbrace = try p.expectToken(.RBrace);

        const node = try Node.ErrorSetDecl.alloc(&p.arena.allocator, decls.len);
        node.* = .{
            .error_token = error_token,
            .decls_len = decls.len,
            .rbrace_token = rbrace,
        };
        std.mem.copy(*Node, node.decls(), decls);
        return &node.base;
    }

    /// GroupedExpr <- LPAREN Expr RPAREN
    fn parseGroupedExpr(p: *Parser) !?*Node {
        const lparen = p.eatToken(.LParen) orelse return null;
        const expr = try p.expectNode(parseExpr, .{
            .ExpectedExpr = .{ .token = p.tok_i },
        });
        const rparen = try p.expectToken(.RParen);

        const node = try p.arena.allocator.create(Node.GroupedExpression);
        node.* = .{
            .lparen = lparen,
            .expr = expr,
            .rparen = rparen,
        };
        return &node.base;
    }

    /// IfTypeExpr <- IfPrefix TypeExpr (KEYWORD_else Payload? TypeExpr)?
    fn parseIfTypeExpr(p: *Parser) !?*Node {
        return p.parseIf(parseTypeExpr);
    }

    /// LabeledTypeExpr
    ///     <- BlockLabel Block
    ///      / BlockLabel? LoopTypeExpr
    fn parseLabeledTypeExpr(p: *Parser) !?*Node {
        var colon: TokenIndex = undefined;
        const label = p.parseBlockLabel(&colon);

        if (label) |label_token| {
            if (try p.parseBlock(label_token)) |node| return node;
        }

        if (try p.parseLoopTypeExpr()) |node| {
            switch (node.tag) {
                .For => node.cast(Node.For).?.label = label,
                .While => node.cast(Node.While).?.label = label,
                else => unreachable,
            }
            return node;
        }

        if (label) |token| {
            p.putBackToken(colon);
            p.putBackToken(token);
        }
        return null;
    }

    /// LoopTypeExpr <- KEYWORD_inline? (ForTypeExpr / WhileTypeExpr)
    fn parseLoopTypeExpr(p: *Parser) !?*Node {
        const inline_token = p.eatToken(.Keyword_inline);

        if (try p.parseForTypeExpr()) |node| {
            node.cast(Node.For).?.inline_token = inline_token;
            return node;
        }

        if (try p.parseWhileTypeExpr()) |node| {
            node.cast(Node.While).?.inline_token = inline_token;
            return node;
        }

        if (inline_token == null) return null;

        // If we've seen "inline", there should have been a "for" or "while"
        try p.errors.append(p.gpa, .{
            .ExpectedInlinable = .{ .token = p.tok_i },
        });
        return error.ParseError;
    }

    /// ForTypeExpr <- ForPrefix TypeExpr (KEYWORD_else TypeExpr)?
    fn parseForTypeExpr(p: *Parser) !?*Node {
        const node = (try p.parseForPrefix()) orelse return null;
        const for_prefix = node.cast(Node.For).?;

        const type_expr = try p.expectNode(parseTypeExpr, .{
            .ExpectedTypeExpr = .{ .token = p.tok_i },
        });
        for_prefix.body = type_expr;

        if (p.eatToken(.Keyword_else)) |else_token| {
            const else_expr = try p.expectNode(parseTypeExpr, .{
                .ExpectedTypeExpr = .{ .token = p.tok_i },
            });

            const else_node = try p.arena.allocator.create(Node.Else);
            else_node.* = .{
                .else_token = else_token,
                .payload = null,
                .body = else_expr,
            };

            for_prefix.@"else" = else_node;
        }

        return node;
    }

    /// WhileTypeExpr <- WhilePrefix TypeExpr (KEYWORD_else Payload? TypeExpr)?
    fn parseWhileTypeExpr(p: *Parser) !?*Node {
        const node = (try p.parseWhilePrefix()) orelse return null;
        const while_prefix = node.cast(Node.While).?;

        const type_expr = try p.expectNode(parseTypeExpr, .{
            .ExpectedTypeExpr = .{ .token = p.tok_i },
        });
        while_prefix.body = type_expr;

        if (p.eatToken(.Keyword_else)) |else_token| {
            const payload = try p.parsePayload();

            const else_expr = try p.expectNode(parseTypeExpr, .{
                .ExpectedTypeExpr = .{ .token = p.tok_i },
            });

            const else_node = try p.arena.allocator.create(Node.Else);
            else_node.* = .{
                .else_token = else_token,
                .payload = null,
                .body = else_expr,
            };

            while_prefix.@"else" = else_node;
        }

        return node;
    }

    /// SwitchExpr <- KEYWORD_switch LPAREN Expr RPAREN LBRACE SwitchProngList RBRACE
    fn parseSwitchExpr(p: *Parser) !?*Node {
        const switch_token = p.eatToken(.Keyword_switch) orelse return null;
        _ = try p.expectToken(.LParen);
        const expr_node = try p.expectNode(parseExpr, .{
            .ExpectedExpr = .{ .token = p.tok_i },
        });
        _ = try p.expectToken(.RParen);
        _ = try p.expectToken(.LBrace);
        const cases = try p.parseSwitchProngList();
        defer p.gpa.free(cases);
        const rbrace = try p.expectToken(.RBrace);

        const node = try Node.Switch.alloc(&p.arena.allocator, cases.len);
        node.* = .{
            .switch_token = switch_token,
            .expr = expr_node,
            .cases_len = cases.len,
            .rbrace = rbrace,
        };
        std.mem.copy(*Node, node.cases(), cases);
        return &node.base;
    }

    /// AsmExpr <- KEYWORD_asm KEYWORD_volatile? LPAREN Expr AsmOutput? RPAREN
    /// AsmOutput <- COLON AsmOutputList AsmInput?
    /// AsmInput <- COLON AsmInputList AsmClobbers?
    /// AsmClobbers <- COLON StringList
    /// StringList <- (STRINGLITERAL COMMA)* STRINGLITERAL?
    fn parseAsmExpr(p: *Parser) !?*Node {
        const asm_token = p.eatToken(.Keyword_asm) orelse return null;
        const volatile_token = p.eatToken(.Keyword_volatile);
        _ = try p.expectToken(.LParen);
        const template = try p.expectNode(parseExpr, .{
            .ExpectedExpr = .{ .token = p.tok_i },
        });

        var arena_outputs: []Node.Asm.Output = &[0]Node.Asm.Output{};
        var arena_inputs: []Node.Asm.Input = &[0]Node.Asm.Input{};
        var arena_clobbers: []*Node = &[0]*Node{};

        if (p.eatToken(.Colon) != null) {
            const outputs = try p.parseAsmOutputList();
            defer p.gpa.free(outputs);
            arena_outputs = try p.arena.allocator.dupe(Node.Asm.Output, outputs);

            if (p.eatToken(.Colon) != null) {
                const inputs = try p.parseAsmInputList();
                defer p.gpa.free(inputs);
                arena_inputs = try p.arena.allocator.dupe(Node.Asm.Input, inputs);

                if (p.eatToken(.Colon) != null) {
                    const clobbers = try ListParseFn(*Node, parseStringLiteral)(p);
                    defer p.gpa.free(clobbers);
                    arena_clobbers = try p.arena.allocator.dupe(*Node, clobbers);
                }
            }
        }

        const node = try p.arena.allocator.create(Node.Asm);
        node.* = .{
            .asm_token = asm_token,
            .volatile_token = volatile_token,
            .template = template,
            .outputs = arena_outputs,
            .inputs = arena_inputs,
            .clobbers = arena_clobbers,
            .rparen = try p.expectToken(.RParen),
        };

        return &node.base;
    }

    /// DOT IDENTIFIER
    fn parseAnonLiteral(p: *Parser) !?*Node {
        const dot = p.eatToken(.Period) orelse return null;

        // anon enum literal
        if (p.eatToken(.Identifier)) |name| {
            const node = try p.arena.allocator.create(Node.EnumLiteral);
            node.* = .{
                .dot = dot,
                .name = name,
            };
            return &node.base;
        }

        if (try p.parseAnonInitList(dot)) |node| {
            return node;
        }

        p.putBackToken(dot);
        return null;
    }

    /// AsmOutputItem <- LBRACKET IDENTIFIER RBRACKET STRINGLITERAL LPAREN (MINUSRARROW TypeExpr / IDENTIFIER) RPAREN
    fn parseAsmOutputItem(p: *Parser) !?Node.Asm.Output {
        const lbracket = p.eatToken(.LBracket) orelse return null;
        const name = try p.expectNode(parseIdentifier, .{
            .ExpectedIdentifier = .{ .token = p.tok_i },
        });
        _ = try p.expectToken(.RBracket);

        const constraint = try p.expectNode(parseStringLiteral, .{
            .ExpectedStringLiteral = .{ .token = p.tok_i },
        });

        _ = try p.expectToken(.LParen);
        const kind: Node.Asm.Output.Kind = blk: {
            if (p.eatToken(.Arrow) != null) {
                const return_ident = try p.expectNode(parseTypeExpr, .{
                    .ExpectedTypeExpr = .{ .token = p.tok_i },
                });
                break :blk .{ .Return = return_ident };
            }
            const variable = try p.expectNode(parseIdentifier, .{
                .ExpectedIdentifier = .{ .token = p.tok_i },
            });
            break :blk .{ .Variable = variable.castTag(.Identifier).? };
        };
        const rparen = try p.expectToken(.RParen);

        return Node.Asm.Output{
            .lbracket = lbracket,
            .symbolic_name = name,
            .constraint = constraint,
            .kind = kind,
            .rparen = rparen,
        };
    }

    /// AsmInputItem <- LBRACKET IDENTIFIER RBRACKET STRINGLITERAL LPAREN Expr RPAREN
    fn parseAsmInputItem(p: *Parser) !?Node.Asm.Input {
        const lbracket = p.eatToken(.LBracket) orelse return null;
        const name = try p.expectNode(parseIdentifier, .{
            .ExpectedIdentifier = .{ .token = p.tok_i },
        });
        _ = try p.expectToken(.RBracket);

        const constraint = try p.expectNode(parseStringLiteral, .{
            .ExpectedStringLiteral = .{ .token = p.tok_i },
        });

        _ = try p.expectToken(.LParen);
        const expr = try p.expectNode(parseExpr, .{
            .ExpectedExpr = .{ .token = p.tok_i },
        });
        const rparen = try p.expectToken(.RParen);

        return Node.Asm.Input{
            .lbracket = lbracket,
            .symbolic_name = name,
            .constraint = constraint,
            .expr = expr,
            .rparen = rparen,
        };
    }

    /// BreakLabel <- COLON IDENTIFIER
    fn parseBreakLabel(p: *Parser) !?TokenIndex {
        _ = p.eatToken(.Colon) orelse return null;
        const ident = try p.expectToken(.Identifier);
        return ident;
    }

    /// BlockLabel <- IDENTIFIER COLON
    fn parseBlockLabel(p: *Parser, colon_token: *TokenIndex) ?TokenIndex {
        const identifier = p.eatToken(.Identifier) orelse return null;
        if (p.eatToken(.Colon)) |colon| {
            colon_token.* = colon;
            return identifier;
        }
        p.putBackToken(identifier);
        return null;
    }

    /// FieldInit <- DOT IDENTIFIER EQUAL Expr
    fn parseFieldInit(p: *Parser) !?*Node {
        const period_token = p.eatToken(.Period) orelse return null;
        const name_token = p.eatToken(.Identifier) orelse {
            // Because of anon literals `.{` is also valid.
            p.putBackToken(period_token);
            return null;
        };
        const eq_token = p.eatToken(.Equal) orelse {
            // `.Name` may also be an enum literal, which is a later rule.
            p.putBackToken(name_token);
            p.putBackToken(period_token);
            return null;
        };
        const expr_node = try p.expectNode(parseExpr, .{
            .ExpectedExpr = .{ .token = p.tok_i },
        });

        const node = try p.arena.allocator.create(Node.FieldInitializer);
        node.* = .{
            .period_token = period_token,
            .name_token = name_token,
            .expr = expr_node,
        };
        return &node.base;
    }

    /// WhileContinueExpr <- COLON LPAREN AssignExpr RPAREN
    fn parseWhileContinueExpr(p: *Parser) !?*Node {
        _ = p.eatToken(.Colon) orelse return null;
        _ = try p.expectToken(.LParen);
        const node = try p.expectNode(parseAssignExpr, .{
            .ExpectedExprOrAssignment = .{ .token = p.tok_i },
        });
        _ = try p.expectToken(.RParen);
        return node;
    }

    /// LinkSection <- KEYWORD_linksection LPAREN Expr RPAREN
    fn parseLinkSection(p: *Parser) !?*Node {
        _ = p.eatToken(.Keyword_linksection) orelse return null;
        _ = try p.expectToken(.LParen);
        const expr_node = try p.expectNode(parseExpr, .{
            .ExpectedExpr = .{ .token = p.tok_i },
        });
        _ = try p.expectToken(.RParen);
        return expr_node;
    }

    /// CallConv <- KEYWORD_callconv LPAREN Expr RPAREN
    fn parseCallconv(p: *Parser) !?*Node {
        _ = p.eatToken(.Keyword_callconv) orelse return null;
        _ = try p.expectToken(.LParen);
        const expr_node = try p.expectNode(parseExpr, .{
            .ExpectedExpr = .{ .token = p.tok_i },
        });
        _ = try p.expectToken(.RParen);
        return expr_node;
    }

    /// ParamDecl <- (KEYWORD_noalias / KEYWORD_comptime)? (IDENTIFIER COLON)? ParamType
    fn parseParamDecl(p: *Parser) !?Node.FnProto.ParamDecl {
        const doc_comments = try p.parseDocComment();
        const noalias_token = p.eatToken(.Keyword_noalias);
        const comptime_token = if (noalias_token == null) p.eatToken(.Keyword_comptime) else null;
        const name_token = blk: {
            const identifier = p.eatToken(.Identifier) orelse break :blk null;
            if (p.eatToken(.Colon) != null) break :blk identifier;
            p.putBackToken(identifier); // ParamType may also be an identifier
            break :blk null;
        };
        const param_type = (try p.parseParamType()) orelse {
            // Only return cleanly if no keyword, identifier, or doc comment was found
            if (noalias_token == null and
                comptime_token == null and
                name_token == null and
                doc_comments == null)
            {
                return null;
            }
            try p.errors.append(p.gpa, .{
                .ExpectedParamType = .{ .token = p.tok_i },
            });
            return error.ParseError;
        };

        return Node.FnProto.ParamDecl{
            .doc_comments = doc_comments,
            .comptime_token = comptime_token,
            .noalias_token = noalias_token,
            .name_token = name_token,
            .param_type = param_type,
        };
    }

    /// ParamType
    ///     <- Keyword_anytype
    ///      / DOT3
    ///      / TypeExpr
    fn parseParamType(p: *Parser) !?Node.FnProto.ParamDecl.ParamType {
        // TODO cast from tuple to error union is broken
        const P = Node.FnProto.ParamDecl.ParamType;
        if (try p.parseAnyType()) |node| return P{ .any_type = node };
        if (try p.parseTypeExpr()) |node| return P{ .type_expr = node };
        return null;
    }

    /// IfPrefix <- KEYWORD_if LPAREN Expr RPAREN PtrPayload?
    fn parseIfPrefix(p: *Parser) !?*Node {
        const if_token = p.eatToken(.Keyword_if) orelse return null;
        _ = try p.expectToken(.LParen);
        const condition = try p.expectNode(parseExpr, .{
            .ExpectedExpr = .{ .token = p.tok_i },
        });
        _ = try p.expectToken(.RParen);
        const payload = try p.parsePtrPayload();

        const node = try p.arena.allocator.create(Node.If);
        node.* = .{
            .if_token = if_token,
            .condition = condition,
            .payload = payload,
            .body = undefined, // set by caller
            .@"else" = null,
        };
        return &node.base;
    }

    /// WhilePrefix <- KEYWORD_while LPAREN Expr RPAREN PtrPayload? WhileContinueExpr?
    fn parseWhilePrefix(p: *Parser) !?*Node {
        const while_token = p.eatToken(.Keyword_while) orelse return null;

        _ = try p.expectToken(.LParen);
        const condition = try p.expectNode(parseExpr, .{
            .ExpectedExpr = .{ .token = p.tok_i },
        });
        _ = try p.expectToken(.RParen);

        const payload = try p.parsePtrPayload();
        const continue_expr = try p.parseWhileContinueExpr();

        const node = try p.arena.allocator.create(Node.While);
        node.* = .{
            .label = null,
            .inline_token = null,
            .while_token = while_token,
            .condition = condition,
            .payload = payload,
            .continue_expr = continue_expr,
            .body = undefined, // set by caller
            .@"else" = null,
        };
        return &node.base;
    }

    /// ForPrefix <- KEYWORD_for LPAREN Expr RPAREN PtrIndexPayload
    fn parseForPrefix(p: *Parser) !?*Node {
        const for_token = p.eatToken(.Keyword_for) orelse return null;

        _ = try p.expectToken(.LParen);
        const array_expr = try p.expectNode(parseExpr, .{
            .ExpectedExpr = .{ .token = p.tok_i },
        });
        _ = try p.expectToken(.RParen);

        const payload = try p.expectNode(parsePtrIndexPayload, .{
            .ExpectedPayload = .{ .token = p.tok_i },
        });

        const node = try p.arena.allocator.create(Node.For);
        node.* = .{
            .label = null,
            .inline_token = null,
            .for_token = for_token,
            .array_expr = array_expr,
            .payload = payload,
            .body = undefined, // set by caller
            .@"else" = null,
        };
        return &node.base;
    }

    /// Payload <- PIPE IDENTIFIER PIPE
    fn parsePayload(p: *Parser) !?*Node {
        const lpipe = p.eatToken(.Pipe) orelse return null;
        const identifier = try p.expectNode(parseIdentifier, .{
            .ExpectedIdentifier = .{ .token = p.tok_i },
        });
        const rpipe = try p.expectToken(.Pipe);

        const node = try p.arena.allocator.create(Node.Payload);
        node.* = .{
            .lpipe = lpipe,
            .error_symbol = identifier,
            .rpipe = rpipe,
        };
        return &node.base;
    }

    /// PtrPayload <- PIPE ASTERISK? IDENTIFIER PIPE
    fn parsePtrPayload(p: *Parser) !?*Node {
        const lpipe = p.eatToken(.Pipe) orelse return null;
        const asterisk = p.eatToken(.Asterisk);
        const identifier = try p.expectNode(parseIdentifier, .{
            .ExpectedIdentifier = .{ .token = p.tok_i },
        });
        const rpipe = try p.expectToken(.Pipe);

        const node = try p.arena.allocator.create(Node.PointerPayload);
        node.* = .{
            .lpipe = lpipe,
            .ptr_token = asterisk,
            .value_symbol = identifier,
            .rpipe = rpipe,
        };
        return &node.base;
    }

    /// PtrIndexPayload <- PIPE ASTERISK? IDENTIFIER (COMMA IDENTIFIER)? PIPE
    fn parsePtrIndexPayload(p: *Parser) !?*Node {
        const lpipe = p.eatToken(.Pipe) orelse return null;
        const asterisk = p.eatToken(.Asterisk);
        const identifier = try p.expectNode(parseIdentifier, .{
            .ExpectedIdentifier = .{ .token = p.tok_i },
        });

        const index = if (p.eatToken(.Comma) == null)
            null
        else
            try p.expectNode(parseIdentifier, .{
                .ExpectedIdentifier = .{ .token = p.tok_i },
            });

        const rpipe = try p.expectToken(.Pipe);

        const node = try p.arena.allocator.create(Node.PointerIndexPayload);
        node.* = .{
            .lpipe = lpipe,
            .ptr_token = asterisk,
            .value_symbol = identifier,
            .index_symbol = index,
            .rpipe = rpipe,
        };
        return &node.base;
    }

    /// SwitchProng <- SwitchCase EQUALRARROW PtrPayload? AssignExpr
    fn parseSwitchProng(p: *Parser) !?*Node {
        const node = (try p.parseSwitchCase()) orelse return null;
        const arrow = try p.expectToken(.EqualAngleBracketRight);
        const payload = try p.parsePtrPayload();
        const expr = try p.expectNode(parseAssignExpr, .{
            .ExpectedExprOrAssignment = .{ .token = p.tok_i },
        });

        const switch_case = node.cast(Node.SwitchCase).?;
        switch_case.arrow_token = arrow;
        switch_case.payload = payload;
        switch_case.expr = expr;

        return node;
    }

    /// SwitchCase
    ///     <- SwitchItem (COMMA SwitchItem)* COMMA?
    ///      / KEYWORD_else
    fn parseSwitchCase(p: *Parser) !?*Node {
        var list = std.ArrayList(*Node).init(p.gpa);
        defer list.deinit();

        if (try p.parseSwitchItem()) |first_item| {
            try list.append(first_item);
            while (p.eatToken(.Comma) != null) {
                const next_item = (try p.parseSwitchItem()) orelse break;
                try list.append(next_item);
            }
        } else if (p.eatToken(.Keyword_else)) |else_token| {
            const else_node = try p.arena.allocator.create(Node.SwitchElse);
            else_node.* = .{
                .token = else_token,
            };
            try list.append(&else_node.base);
        } else return null;

        const node = try Node.SwitchCase.alloc(&p.arena.allocator, list.items.len);
        node.* = .{
            .items_len = list.items.len,
            .arrow_token = undefined, // set by caller
            .payload = null,
            .expr = undefined, // set by caller
        };
        std.mem.copy(*Node, node.items(), list.items);
        return &node.base;
    }

    /// SwitchItem <- Expr (DOT3 Expr)?
    fn parseSwitchItem(p: *Parser) !?*Node {
        const expr = (try p.parseExpr()) orelse return null;
        if (p.eatToken(.Ellipsis3)) |token| {
            const range_end = try p.expectNode(parseExpr, .{
                .ExpectedExpr = .{ .token = p.tok_i },
            });

            const node = try p.arena.allocator.create(Node.SimpleInfixOp);
            node.* = .{
                .base = Node{ .tag = .Range },
                .op_token = token,
                .lhs = expr,
                .rhs = range_end,
            };
            return &node.base;
        }
        return expr;
    }

    /// AssignOp
    ///     <- ASTERISKEQUAL
    ///      / SLASHEQUAL
    ///      / PERCENTEQUAL
    ///      / PLUSEQUAL
    ///      / MINUSEQUAL
    ///      / LARROW2EQUAL
    ///      / RARROW2EQUAL
    ///      / AMPERSANDEQUAL
    ///      / CARETEQUAL
    ///      / PIPEEQUAL
    ///      / ASTERISKPERCENTEQUAL
    ///      / PLUSPERCENTEQUAL
    ///      / MINUSPERCENTEQUAL
    ///      / EQUAL
    fn parseAssignOp(p: *Parser) !?*Node {
        const token = p.nextToken();
        const op: Node.Tag = switch (p.token_ids[token]) {
            .AsteriskEqual => .AssignMul,
            .SlashEqual => .AssignDiv,
            .PercentEqual => .AssignMod,
            .PlusEqual => .AssignAdd,
            .MinusEqual => .AssignSub,
            .AngleBracketAngleBracketLeftEqual => .AssignBitShiftLeft,
            .AngleBracketAngleBracketRightEqual => .AssignBitShiftRight,
            .AmpersandEqual => .AssignBitAnd,
            .CaretEqual => .AssignBitXor,
            .PipeEqual => .AssignBitOr,
            .AsteriskPercentEqual => .AssignMulWrap,
            .PlusPercentEqual => .AssignAddWrap,
            .MinusPercentEqual => .AssignSubWrap,
            .Equal => .Assign,
            else => {
                p.putBackToken(token);
                return null;
            },
        };

        const node = try p.arena.allocator.create(Node.SimpleInfixOp);
        node.* = .{
            .base = .{ .tag = op },
            .op_token = token,
            .lhs = undefined, // set by caller
            .rhs = undefined, // set by caller
        };
        return &node.base;
    }

    /// CompareOp
    ///     <- EQUALEQUAL
    ///      / EXCLAMATIONMARKEQUAL
    ///      / LARROW
    ///      / RARROW
    ///      / LARROWEQUAL
    ///      / RARROWEQUAL
    fn parseCompareOp(p: *Parser) !?*Node {
        const token = p.nextToken();
        const op: Node.Tag = switch (p.token_ids[token]) {
            .EqualEqual => .EqualEqual,
            .BangEqual => .BangEqual,
            .AngleBracketLeft => .LessThan,
            .AngleBracketRight => .GreaterThan,
            .AngleBracketLeftEqual => .LessOrEqual,
            .AngleBracketRightEqual => .GreaterOrEqual,
            else => {
                p.putBackToken(token);
                return null;
            },
        };

        return p.createInfixOp(token, op);
    }

    /// BitwiseOp
    ///     <- AMPERSAND
    ///      / CARET
    ///      / PIPE
    ///      / KEYWORD_orelse
    ///      / KEYWORD_catch Payload?
    fn parseBitwiseOp(p: *Parser) !?*Node {
        const token = p.nextToken();
        const op: Node.Tag = switch (p.token_ids[token]) {
            .Ampersand => .BitAnd,
            .Caret => .BitXor,
            .Pipe => .BitOr,
            .Keyword_orelse => .OrElse,
            .Keyword_catch => {
                const payload = try p.parsePayload();
                const node = try p.arena.allocator.create(Node.Catch);
                node.* = .{
                    .op_token = token,
                    .lhs = undefined, // set by caller
                    .rhs = undefined, // set by caller
                    .payload = payload,
                };
                return &node.base;
            },
            else => {
                p.putBackToken(token);
                return null;
            },
        };

        return p.createInfixOp(token, op);
    }

    /// BitShiftOp
    ///     <- LARROW2
    ///      / RARROW2
    fn parseBitShiftOp(p: *Parser) !?*Node {
        const token = p.nextToken();
        const op: Node.Tag = switch (p.token_ids[token]) {
            .AngleBracketAngleBracketLeft => .BitShiftLeft,
            .AngleBracketAngleBracketRight => .BitShiftRight,
            else => {
                p.putBackToken(token);
                return null;
            },
        };

        return p.createInfixOp(token, op);
    }

    /// AdditionOp
    ///     <- PLUS
    ///      / MINUS
    ///      / PLUS2
    ///      / PLUSPERCENT
    ///      / MINUSPERCENT
    fn parseAdditionOp(p: *Parser) !?*Node {
        const token = p.nextToken();
        const op: Node.Tag = switch (p.token_ids[token]) {
            .Plus => .Add,
            .Minus => .Sub,
            .PlusPlus => .ArrayCat,
            .PlusPercent => .AddWrap,
            .MinusPercent => .SubWrap,
            else => {
                p.putBackToken(token);
                return null;
            },
        };

        return p.createInfixOp(token, op);
    }

    /// MultiplyOp
    ///     <- PIPE2
    ///      / ASTERISK
    ///      / SLASH
    ///      / PERCENT
    ///      / ASTERISK2
    ///      / ASTERISKPERCENT
    fn parseMultiplyOp(p: *Parser) !?*Node {
        const token = p.nextToken();
        const op: Node.Tag = switch (p.token_ids[token]) {
            .PipePipe => .MergeErrorSets,
            .Asterisk => .Mul,
            .Slash => .Div,
            .Percent => .Mod,
            .AsteriskAsterisk => .ArrayMult,
            .AsteriskPercent => .MulWrap,
            else => {
                p.putBackToken(token);
                return null;
            },
        };

        return p.createInfixOp(token, op);
    }

    /// PrefixOp
    ///     <- EXCLAMATIONMARK
    ///      / MINUS
    ///      / TILDE
    ///      / MINUSPERCENT
    ///      / AMPERSAND
    ///      / KEYWORD_try
    ///      / KEYWORD_await
    fn parsePrefixOp(p: *Parser) !?*Node {
        const token = p.nextToken();
        switch (p.token_ids[token]) {
            .Bang => return p.allocSimplePrefixOp(.BoolNot, token),
            .Minus => return p.allocSimplePrefixOp(.Negation, token),
            .Tilde => return p.allocSimplePrefixOp(.BitNot, token),
            .MinusPercent => return p.allocSimplePrefixOp(.NegationWrap, token),
            .Ampersand => return p.allocSimplePrefixOp(.AddressOf, token),
            .Keyword_try => return p.allocSimplePrefixOp(.Try, token),
            .Keyword_await => return p.allocSimplePrefixOp(.Await, token),
            else => {
                p.putBackToken(token);
                return null;
            },
        }
    }

    fn allocSimplePrefixOp(p: *Parser, comptime tag: Node.Tag, token: TokenIndex) !?*Node {
        const node = try p.arena.allocator.create(Node.SimplePrefixOp);
        node.* = .{
            .base = .{ .tag = tag },
            .op_token = token,
            .rhs = undefined, // set by caller
        };
        return &node.base;
    }

    // TODO: ArrayTypeStart is either an array or a slice, but const/allowzero only work on
    //       pointers. Consider updating this rule:
    //       ...
    //       / ArrayTypeStart
    //       / SliceTypeStart (ByteAlign / KEYWORD_const / KEYWORD_volatile / KEYWORD_allowzero)*
    //       / PtrTypeStart ...

    /// PrefixTypeOp
    ///     <- QUESTIONMARK
    ///      / KEYWORD_anyframe MINUSRARROW
    ///      / ArrayTypeStart (ByteAlign / KEYWORD_const / KEYWORD_volatile / KEYWORD_allowzero)*
    ///      / PtrTypeStart (KEYWORD_align LPAREN Expr (COLON INTEGER COLON INTEGER)? RPAREN / KEYWORD_const / KEYWORD_volatile / KEYWORD_allowzero)*
    fn parsePrefixTypeOp(p: *Parser) !?*Node {
        if (p.eatToken(.QuestionMark)) |token| {
            const node = try p.arena.allocator.create(Node.SimplePrefixOp);
            node.* = .{
                .base = .{ .tag = .OptionalType },
                .op_token = token,
                .rhs = undefined, // set by caller
            };
            return &node.base;
        }

        if (p.eatToken(.Keyword_anyframe)) |token| {
            const arrow = p.eatToken(.Arrow) orelse {
                p.putBackToken(token);
                return null;
            };
            const node = try p.arena.allocator.create(Node.AnyFrameType);
            node.* = .{
                .anyframe_token = token,
                .result = .{
                    .arrow_token = arrow,
                    .return_type = undefined, // set by caller
                },
            };
            return &node.base;
        }

        if (try p.parsePtrTypeStart()) |node| {
            // If the token encountered was **, there will be two nodes instead of one.
            // The attributes should be applied to the rightmost operator.
            var ptr_info = if (node.cast(Node.PtrType)) |ptr_type|
                if (p.token_ids[ptr_type.op_token] == .AsteriskAsterisk)
                    &ptr_type.rhs.cast(Node.PtrType).?.ptr_info
                else
                    &ptr_type.ptr_info
            else if (node.cast(Node.SliceType)) |slice_type|
                &slice_type.ptr_info
            else
                unreachable;

            while (true) {
                if (p.eatToken(.Keyword_align)) |align_token| {
                    const lparen = try p.expectToken(.LParen);
                    const expr_node = try p.expectNode(parseExpr, .{
                        .ExpectedExpr = .{ .token = p.tok_i },
                    });

                    // Optional bit range
                    const bit_range = if (p.eatToken(.Colon)) |_| bit_range_value: {
                        const range_start = try p.expectNode(parseIntegerLiteral, .{
                            .ExpectedIntegerLiteral = .{ .token = p.tok_i },
                        });
                        _ = try p.expectToken(.Colon);
                        const range_end = try p.expectNode(parseIntegerLiteral, .{
                            .ExpectedIntegerLiteral = .{ .token = p.tok_i },
                        });

                        break :bit_range_value ast.PtrInfo.Align.BitRange{
                            .start = range_start,
                            .end = range_end,
                        };
                    } else null;
                    _ = try p.expectToken(.RParen);

                    if (ptr_info.align_info != null) {
                        try p.errors.append(p.gpa, .{
                            .ExtraAlignQualifier = .{ .token = p.tok_i - 1 },
                        });
                        continue;
                    }

                    ptr_info.align_info = ast.PtrInfo.Align{
                        .node = expr_node,
                        .bit_range = bit_range,
                    };

                    continue;
                }
                if (p.eatToken(.Keyword_const)) |const_token| {
                    if (ptr_info.const_token != null) {
                        try p.errors.append(p.gpa, .{
                            .ExtraConstQualifier = .{ .token = p.tok_i - 1 },
                        });
                        continue;
                    }
                    ptr_info.const_token = const_token;
                    continue;
                }
                if (p.eatToken(.Keyword_volatile)) |volatile_token| {
                    if (ptr_info.volatile_token != null) {
                        try p.errors.append(p.gpa, .{
                            .ExtraVolatileQualifier = .{ .token = p.tok_i - 1 },
                        });
                        continue;
                    }
                    ptr_info.volatile_token = volatile_token;
                    continue;
                }
                if (p.eatToken(.Keyword_allowzero)) |allowzero_token| {
                    if (ptr_info.allowzero_token != null) {
                        try p.errors.append(p.gpa, .{
                            .ExtraAllowZeroQualifier = .{ .token = p.tok_i - 1 },
                        });
                        continue;
                    }
                    ptr_info.allowzero_token = allowzero_token;
                    continue;
                }
                break;
            }

            return node;
        }

        if (try p.parseArrayTypeStart()) |node| {
            if (node.cast(Node.SliceType)) |slice_type| {
                // Collect pointer qualifiers in any order, but disallow duplicates
                while (true) {
                    if (try p.parseByteAlign()) |align_expr| {
                        if (slice_type.ptr_info.align_info != null) {
                            try p.errors.append(p.gpa, .{
                                .ExtraAlignQualifier = .{ .token = p.tok_i - 1 },
                            });
                            continue;
                        }
                        slice_type.ptr_info.align_info = ast.PtrInfo.Align{
                            .node = align_expr,
                            .bit_range = null,
                        };
                        continue;
                    }
                    if (p.eatToken(.Keyword_const)) |const_token| {
                        if (slice_type.ptr_info.const_token != null) {
                            try p.errors.append(p.gpa, .{
                                .ExtraConstQualifier = .{ .token = p.tok_i - 1 },
                            });
                            continue;
                        }
                        slice_type.ptr_info.const_token = const_token;
                        continue;
                    }
                    if (p.eatToken(.Keyword_volatile)) |volatile_token| {
                        if (slice_type.ptr_info.volatile_token != null) {
                            try p.errors.append(p.gpa, .{
                                .ExtraVolatileQualifier = .{ .token = p.tok_i - 1 },
                            });
                            continue;
                        }
                        slice_type.ptr_info.volatile_token = volatile_token;
                        continue;
                    }
                    if (p.eatToken(.Keyword_allowzero)) |allowzero_token| {
                        if (slice_type.ptr_info.allowzero_token != null) {
                            try p.errors.append(p.gpa, .{
                                .ExtraAllowZeroQualifier = .{ .token = p.tok_i - 1 },
                            });
                            continue;
                        }
                        slice_type.ptr_info.allowzero_token = allowzero_token;
                        continue;
                    }
                    break;
                }
            }
            return node;
        }

        return null;
    }

    /// SuffixOp
    ///     <- LBRACKET Expr (DOT2 (Expr (COLON Expr)?)?)? RBRACKET
    ///      / DOT IDENTIFIER
    ///      / DOTASTERISK
    ///      / DOTQUESTIONMARK
    fn parseSuffixOp(p: *Parser, lhs: *Node) !?*Node {
        if (p.eatToken(.LBracket)) |_| {
            const index_expr = try p.expectNode(parseExpr, .{
                .ExpectedExpr = .{ .token = p.tok_i },
            });

            if (p.eatToken(.Ellipsis2) != null) {
                const end_expr = try p.parseExpr();
                const sentinel: ?*Node = if (p.eatToken(.Colon) != null)
                    try p.parseExpr()
                else
                    null;
                const rtoken = try p.expectToken(.RBracket);
                const node = try p.arena.allocator.create(Node.Slice);
                node.* = .{
                    .lhs = lhs,
                    .rtoken = rtoken,
                    .start = index_expr,
                    .end = end_expr,
                    .sentinel = sentinel,
                };
                return &node.base;
            }

            const rtoken = try p.expectToken(.RBracket);
            const node = try p.arena.allocator.create(Node.ArrayAccess);
            node.* = .{
                .lhs = lhs,
                .rtoken = rtoken,
                .index_expr = index_expr,
            };
            return &node.base;
        }

        if (p.eatToken(.PeriodAsterisk)) |period_asterisk| {
            const node = try p.arena.allocator.create(Node.SimpleSuffixOp);
            node.* = .{
                .base = .{ .tag = .Deref },
                .lhs = lhs,
                .rtoken = period_asterisk,
            };
            return &node.base;
        }

        if (p.eatToken(.Invalid_periodasterisks)) |period_asterisk| {
            try p.errors.append(p.gpa, .{
                .AsteriskAfterPointerDereference = .{ .token = period_asterisk },
            });
            const node = try p.arena.allocator.create(Node.SimpleSuffixOp);
            node.* = .{
                .base = .{ .tag = .Deref },
                .lhs = lhs,
                .rtoken = period_asterisk,
            };
            return &node.base;
        }

        if (p.eatToken(.Period)) |period| {
            if (try p.parseIdentifier()) |identifier| {
                const node = try p.arena.allocator.create(Node.SimpleInfixOp);
                node.* = .{
                    .base = Node{ .tag = .Period },
                    .op_token = period,
                    .lhs = lhs,
                    .rhs = identifier,
                };
                return &node.base;
            }
            if (p.eatToken(.QuestionMark)) |question_mark| {
                const node = try p.arena.allocator.create(Node.SimpleSuffixOp);
                node.* = .{
                    .base = .{ .tag = .UnwrapOptional },
                    .lhs = lhs,
                    .rtoken = question_mark,
                };
                return &node.base;
            }
            try p.errors.append(p.gpa, .{
                .ExpectedSuffixOp = .{ .token = p.tok_i },
            });
            return null;
        }

        return null;
    }

    /// FnCallArguments <- LPAREN ExprList RPAREN
    /// ExprList <- (Expr COMMA)* Expr?
    fn parseFnCallArguments(p: *Parser) !?AnnotatedParamList {
        if (p.eatToken(.LParen) == null) return null;
        const list = try ListParseFn(*Node, parseExpr)(p);
        errdefer p.gpa.free(list);
        const rparen = try p.expectToken(.RParen);
        return AnnotatedParamList{ .list = list, .rparen = rparen };
    }

    const AnnotatedParamList = struct {
        list: []*Node,
        rparen: TokenIndex,
    };

    /// ArrayTypeStart <- LBRACKET Expr? RBRACKET
    fn parseArrayTypeStart(p: *Parser) !?*Node {
        const lbracket = p.eatToken(.LBracket) orelse return null;
        const expr = try p.parseExpr();
        const sentinel = if (p.eatToken(.Colon)) |_|
            try p.expectNode(parseExpr, .{
                .ExpectedExpr = .{ .token = p.tok_i },
            })
        else
            null;
        const rbracket = try p.expectToken(.RBracket);

        if (expr) |len_expr| {
            if (sentinel) |s| {
                const node = try p.arena.allocator.create(Node.ArrayTypeSentinel);
                node.* = .{
                    .op_token = lbracket,
                    .rhs = undefined, // set by caller
                    .len_expr = len_expr,
                    .sentinel = s,
                };
                return &node.base;
            } else {
                const node = try p.arena.allocator.create(Node.ArrayType);
                node.* = .{
                    .op_token = lbracket,
                    .rhs = undefined, // set by caller
                    .len_expr = len_expr,
                };
                return &node.base;
            }
        }

        const node = try p.arena.allocator.create(Node.SliceType);
        node.* = .{
            .op_token = lbracket,
            .rhs = undefined, // set by caller
            .ptr_info = .{ .sentinel = sentinel },
        };
        return &node.base;
    }

    /// PtrTypeStart
    ///     <- ASTERISK
    ///      / ASTERISK2
    ///      / PTRUNKNOWN
    ///      / PTRC
    fn parsePtrTypeStart(p: *Parser) !?*Node {
        if (p.eatToken(.Asterisk)) |asterisk| {
            const sentinel = if (p.eatToken(.Colon)) |_|
                try p.expectNode(parseExpr, .{
                    .ExpectedExpr = .{ .token = p.tok_i },
                })
            else
                null;
            const node = try p.arena.allocator.create(Node.PtrType);
            node.* = .{
                .op_token = asterisk,
                .rhs = undefined, // set by caller
                .ptr_info = .{ .sentinel = sentinel },
            };
            return &node.base;
        }

        if (p.eatToken(.AsteriskAsterisk)) |double_asterisk| {
            const node = try p.arena.allocator.create(Node.PtrType);
            node.* = .{
                .op_token = double_asterisk,
                .rhs = undefined, // set by caller
            };

            // Special case for **, which is its own token
            const child = try p.arena.allocator.create(Node.PtrType);
            child.* = .{
                .op_token = double_asterisk,
                .rhs = undefined, // set by caller
            };
            node.rhs = &child.base;

            return &node.base;
        }
        if (p.eatToken(.LBracket)) |lbracket| {
            const asterisk = p.eatToken(.Asterisk) orelse {
                p.putBackToken(lbracket);
                return null;
            };
            if (p.eatToken(.Identifier)) |ident| {
                const token_loc = p.token_locs[ident];
                const token_slice = p.source[token_loc.start..token_loc.end];
                if (!std.mem.eql(u8, token_slice, "c")) {
                    p.putBackToken(ident);
                } else {
                    _ = try p.expectToken(.RBracket);
                    const node = try p.arena.allocator.create(Node.PtrType);
                    node.* = .{
                        .op_token = lbracket,
                        .rhs = undefined, // set by caller
                    };
                    return &node.base;
                }
            }
            const sentinel = if (p.eatToken(.Colon)) |_|
                try p.expectNode(parseExpr, .{
                    .ExpectedExpr = .{ .token = p.tok_i },
                })
            else
                null;
            _ = try p.expectToken(.RBracket);
            const node = try p.arena.allocator.create(Node.PtrType);
            node.* = .{
                .op_token = lbracket,
                .rhs = undefined, // set by caller
                .ptr_info = .{ .sentinel = sentinel },
            };
            return &node.base;
        }
        return null;
    }

    /// ContainerDeclAuto <- ContainerDeclType LBRACE ContainerMembers RBRACE
    fn parseContainerDeclAuto(p: *Parser) !?*Node {
        const container_decl_type = (try p.parseContainerDeclType()) orelse return null;
        const lbrace = try p.expectToken(.LBrace);
        const members = try p.parseContainerMembers(false);
        defer p.gpa.free(members);
        const rbrace = try p.expectToken(.RBrace);

        const members_len = @intCast(NodeIndex, members.len);
        const node = try Node.ContainerDecl.alloc(&p.arena.allocator, members_len);
        node.* = .{
            .layout_token = null,
            .kind_token = container_decl_type.kind_token,
            .init_arg_expr = container_decl_type.init_arg_expr,
            .fields_and_decls_len = members_len,
            .lbrace_token = lbrace,
            .rbrace_token = rbrace,
        };
        std.mem.copy(*Node, node.fieldsAndDecls(), members);
        return &node.base;
    }

    /// Holds temporary data until we are ready to construct the full ContainerDecl AST node.
    const ContainerDeclType = struct {
        kind_token: TokenIndex,
        init_arg_expr: Node.ContainerDecl.InitArg,
    };

    /// ContainerDeclType
    ///     <- KEYWORD_struct
    ///      / KEYWORD_enum (LPAREN Expr RPAREN)?
    ///      / KEYWORD_union (LPAREN (KEYWORD_enum (LPAREN Expr RPAREN)? / Expr) RPAREN)?
    ///      / KEYWORD_opaque
    fn parseContainerDeclType(p: *Parser) !?ContainerDeclType {
        const kind_token = p.nextToken();

        const init_arg_expr = switch (p.token_ids[kind_token]) {
            .Keyword_struct, .Keyword_opaque => Node.ContainerDecl.InitArg{ .None = {} },
            .Keyword_enum => blk: {
                if (p.eatToken(.LParen) != null) {
                    const expr = try p.expectNode(parseExpr, .{
                        .ExpectedExpr = .{ .token = p.tok_i },
                    });
                    _ = try p.expectToken(.RParen);
                    break :blk Node.ContainerDecl.InitArg{ .Type = expr };
                }
                break :blk Node.ContainerDecl.InitArg{ .None = {} };
            },
            .Keyword_union => blk: {
                if (p.eatToken(.LParen) != null) {
                    if (p.eatToken(.Keyword_enum) != null) {
                        if (p.eatToken(.LParen) != null) {
                            const expr = try p.expectNode(parseExpr, .{
                                .ExpectedExpr = .{ .token = p.tok_i },
                            });
                            _ = try p.expectToken(.RParen);
                            _ = try p.expectToken(.RParen);
                            break :blk Node.ContainerDecl.InitArg{ .Enum = expr };
                        }
                        _ = try p.expectToken(.RParen);
                        break :blk Node.ContainerDecl.InitArg{ .Enum = null };
                    }
                    const expr = try p.expectNode(parseExpr, .{
                        .ExpectedExpr = .{ .token = p.tok_i },
                    });
                    _ = try p.expectToken(.RParen);
                    break :blk Node.ContainerDecl.InitArg{ .Type = expr };
                }
                break :blk Node.ContainerDecl.InitArg{ .None = {} };
            },
            else => {
                p.putBackToken(kind_token);
                return null;
            },
        };

        return ContainerDeclType{
            .kind_token = kind_token,
            .init_arg_expr = init_arg_expr,
        };
    }

    /// ByteAlign <- KEYWORD_align LPAREN Expr RPAREN
    fn parseByteAlign(p: *Parser) !?*Node {
        _ = p.eatToken(.Keyword_align) orelse return null;
        _ = try p.expectToken(.LParen);
        const expr = try p.expectNode(parseExpr, .{
            .ExpectedExpr = .{ .token = p.tok_i },
        });
        _ = try p.expectToken(.RParen);
        return expr;
    }

    /// IdentifierList <- (IDENTIFIER COMMA)* IDENTIFIER?
    /// Only ErrorSetDecl parses an IdentifierList
    fn parseErrorTagList(p: *Parser) ![]*Node {
        return ListParseFn(*Node, parseErrorTag)(p);
    }

    /// SwitchProngList <- (SwitchProng COMMA)* SwitchProng?
    fn parseSwitchProngList(p: *Parser) ![]*Node {
        return ListParseFn(*Node, parseSwitchProng)(p);
    }

    /// AsmOutputList <- (AsmOutputItem COMMA)* AsmOutputItem?
    fn parseAsmOutputList(p: *Parser) Error![]Node.Asm.Output {
        return ListParseFn(Node.Asm.Output, parseAsmOutputItem)(p);
    }

    /// AsmInputList <- (AsmInputItem COMMA)* AsmInputItem?
    fn parseAsmInputList(p: *Parser) Error![]Node.Asm.Input {
        return ListParseFn(Node.Asm.Input, parseAsmInputItem)(p);
    }

    /// ParamDeclList <- (ParamDecl COMMA)* ParamDecl?
    fn parseParamDeclList(p: *Parser) ![]Node.FnProto.ParamDecl {
        return ListParseFn(Node.FnProto.ParamDecl, parseParamDecl)(p);
    }

    const NodeParseFn = fn (p: *Parser) Error!?*Node;

    fn ListParseFn(comptime E: type, comptime nodeParseFn: anytype) ParseFn([]E) {
        return struct {
            pub fn parse(p: *Parser) ![]E {
                var list = std.ArrayList(E).init(p.gpa);
                defer list.deinit();

                while (try nodeParseFn(p)) |item| {
                    try list.append(item);

                    switch (p.token_ids[p.tok_i]) {
                        .Comma => _ = p.nextToken(),
                        // all possible delimiters
                        .Colon, .RParen, .RBrace, .RBracket => break,
                        else => {
                            // this is likely just a missing comma,
                            // continue parsing this list and give an error
                            try p.errors.append(p.gpa, .{
                                .ExpectedToken = .{ .token = p.tok_i, .expected_id = .Comma },
                            });
                        },
                    }
                }
                return list.toOwnedSlice();
            }
        }.parse;
    }

    fn SimpleBinOpParseFn(comptime token: Token.Id, comptime op: Node.Tag) NodeParseFn {
        return struct {
            pub fn parse(p: *Parser) Error!?*Node {
                const op_token = if (token == .Keyword_and) switch (p.token_ids[p.tok_i]) {
                    .Keyword_and => p.nextToken(),
                    .Invalid_ampersands => blk: {
                        try p.errors.append(p.gpa, .{
                            .InvalidAnd = .{ .token = p.tok_i },
                        });
                        break :blk p.nextToken();
                    },
                    else => return null,
                } else p.eatToken(token) orelse return null;

                const node = try p.arena.allocator.create(Node.SimpleInfixOp);
                node.* = .{
                    .base = .{ .tag = op },
                    .op_token = op_token,
                    .lhs = undefined, // set by caller
                    .rhs = undefined, // set by caller
                };
                return &node.base;
            }
        }.parse;
    }

    // Helper parsers not included in the grammar

    fn parseBuiltinCall(p: *Parser) !?*Node {
        const token = p.eatToken(.Builtin) orelse return null;
        const params = (try p.parseFnCallArguments()) orelse {
            try p.errors.append(p.gpa, .{
                .ExpectedParamList = .{ .token = p.tok_i },
            });

            // lets pretend this was an identifier so we can continue parsing
            const node = try p.arena.allocator.create(Node.OneToken);
            node.* = .{
                .base = .{ .tag = .Identifier },
                .token = token,
            };
            return &node.base;
        };
        defer p.gpa.free(params.list);

        const node = try Node.BuiltinCall.alloc(&p.arena.allocator, params.list.len);
        node.* = .{
            .builtin_token = token,
            .params_len = params.list.len,
            .rparen_token = params.rparen,
        };
        std.mem.copy(*Node, node.params(), params.list);
        return &node.base;
    }

    fn parseErrorTag(p: *Parser) !?*Node {
        const doc_comments = try p.parseDocComment(); // no need to rewind on failure
        const token = p.eatToken(.Identifier) orelse return null;

        const node = try p.arena.allocator.create(Node.ErrorTag);
        node.* = .{
            .doc_comments = doc_comments,
            .name_token = token,
        };
        return &node.base;
    }

    fn parseIdentifier(p: *Parser) !?*Node {
        const token = p.eatToken(.Identifier) orelse return null;
        const node = try p.arena.allocator.create(Node.OneToken);
        node.* = .{
            .base = .{ .tag = .Identifier },
            .token = token,
        };
        return &node.base;
    }

    fn parseAnyType(p: *Parser) !?*Node {
        const token = p.eatToken(.Keyword_anytype) orelse
            p.eatToken(.Keyword_var) orelse return null; // TODO remove in next release cycle
        const node = try p.arena.allocator.create(Node.OneToken);
        node.* = .{
            .base = .{ .tag = .AnyType },
            .token = token,
        };
        return &node.base;
    }

    fn createLiteral(p: *Parser, tag: ast.Node.Tag, token: TokenIndex) !*Node {
        const result = try p.arena.allocator.create(Node.OneToken);
        result.* = .{
            .base = .{ .tag = tag },
            .token = token,
        };
        return &result.base;
    }

    fn parseStringLiteralSingle(p: *Parser) !?*Node {
        if (p.eatToken(.StringLiteral)) |token| {
            const node = try p.arena.allocator.create(Node.OneToken);
            node.* = .{
                .base = .{ .tag = .StringLiteral },
                .token = token,
            };
            return &node.base;
        }
        return null;
    }

    // string literal or multiline string literal
    fn parseStringLiteral(p: *Parser) !?*Node {
        if (try p.parseStringLiteralSingle()) |node| return node;

        if (p.eatToken(.MultilineStringLiteralLine)) |first_line| {
            const start_tok_i = p.tok_i;
            var tok_i = start_tok_i;
            var count: usize = 1; // including first_line
            while (true) : (tok_i += 1) {
                switch (p.token_ids[tok_i]) {
                    .LineComment => continue,
                    .MultilineStringLiteralLine => count += 1,
                    else => break,
                }
            }

            const node = try Node.MultilineStringLiteral.alloc(&p.arena.allocator, count);
            node.* = .{ .lines_len = count };
            const lines = node.lines();
            tok_i = start_tok_i;
            lines[0] = first_line;
            count = 1;
            while (true) : (tok_i += 1) {
                switch (p.token_ids[tok_i]) {
                    .LineComment => continue,
                    .MultilineStringLiteralLine => {
                        lines[count] = tok_i;
                        count += 1;
                    },
                    else => break,
                }
            }
            p.tok_i = tok_i;
            return &node.base;
        }

        return null;
    }

    fn parseIntegerLiteral(p: *Parser) !?*Node {
        const token = p.eatToken(.IntegerLiteral) orelse return null;
        const node = try p.arena.allocator.create(Node.OneToken);
        node.* = .{
            .base = .{ .tag = .IntegerLiteral },
            .token = token,
        };
        return &node.base;
    }

    fn parseFloatLiteral(p: *Parser) !?*Node {
        const token = p.eatToken(.FloatLiteral) orelse return null;
        const node = try p.arena.allocator.create(Node.OneToken);
        node.* = .{
            .base = .{ .tag = .FloatLiteral },
            .token = token,
        };
        return &node.base;
    }

    fn parseTry(p: *Parser) !?*Node {
        const token = p.eatToken(.Keyword_try) orelse return null;
        const node = try p.arena.allocator.create(Node.SimplePrefixOp);
        node.* = .{
            .base = .{ .tag = .Try },
            .op_token = token,
            .rhs = undefined, // set by caller
        };
        return &node.base;
    }

    /// IfPrefix Body (KEYWORD_else Payload? Body)?
    fn parseIf(p: *Parser, bodyParseFn: NodeParseFn) !?*Node {
        const node = (try p.parseIfPrefix()) orelse return null;
        const if_prefix = node.cast(Node.If).?;

        if_prefix.body = try p.expectNode(bodyParseFn, .{
            .InvalidToken = .{ .token = p.tok_i },
        });

        const else_token = p.eatToken(.Keyword_else) orelse return node;
        const payload = try p.parsePayload();
        const else_expr = try p.expectNode(bodyParseFn, .{
            .InvalidToken = .{ .token = p.tok_i },
        });
        const else_node = try p.arena.allocator.create(Node.Else);
        else_node.* = .{
            .else_token = else_token,
            .payload = payload,
            .body = else_expr,
        };
        if_prefix.@"else" = else_node;

        return node;
    }

    /// Eat a multiline doc comment
    fn parseDocComment(p: *Parser) !?*Node.DocComment {
        if (p.eatToken(.DocComment)) |first_line| {
            while (p.eatToken(.DocComment)) |_| {}
            const node = try p.arena.allocator.create(Node.DocComment);
            node.* = .{ .first_line = first_line };
            return node;
        }
        return null;
    }

    fn tokensOnSameLine(p: *Parser, token1: TokenIndex, token2: TokenIndex) bool {
        return std.mem.indexOfScalar(u8, p.source[p.token_locs[token1].end..p.token_locs[token2].start], '\n') == null;
    }

    /// Eat a single-line doc comment on the same line as another node
    fn parseAppendedDocComment(p: *Parser, after_token: TokenIndex) !?*Node.DocComment {
        const comment_token = p.eatToken(.DocComment) orelse return null;
        if (p.tokensOnSameLine(after_token, comment_token)) {
            const node = try p.arena.allocator.create(Node.DocComment);
            node.* = .{ .first_line = comment_token };
            return node;
        }
        p.putBackToken(comment_token);
        return null;
    }

    /// Op* Child
    fn parsePrefixOpExpr(p: *Parser, comptime opParseFn: NodeParseFn, comptime childParseFn: NodeParseFn) Error!?*Node {
        if (try opParseFn(p)) |first_op| {
            var rightmost_op = first_op;
            while (true) {
                switch (rightmost_op.tag) {
                    .AddressOf,
                    .Await,
                    .BitNot,
                    .BoolNot,
                    .OptionalType,
                    .Negation,
                    .NegationWrap,
                    .Resume,
                    .Try,
                    => {
                        if (try opParseFn(p)) |rhs| {
                            rightmost_op.cast(Node.SimplePrefixOp).?.rhs = rhs;
                            rightmost_op = rhs;
                        } else break;
                    },
                    .ArrayType => {
                        if (try opParseFn(p)) |rhs| {
                            rightmost_op.cast(Node.ArrayType).?.rhs = rhs;
                            rightmost_op = rhs;
                        } else break;
                    },
                    .ArrayTypeSentinel => {
                        if (try opParseFn(p)) |rhs| {
                            rightmost_op.cast(Node.ArrayTypeSentinel).?.rhs = rhs;
                            rightmost_op = rhs;
                        } else break;
                    },
                    .SliceType => {
                        if (try opParseFn(p)) |rhs| {
                            rightmost_op.cast(Node.SliceType).?.rhs = rhs;
                            rightmost_op = rhs;
                        } else break;
                    },
                    .PtrType => {
                        var ptr_type = rightmost_op.cast(Node.PtrType).?;
                        // If the token encountered was **, there will be two nodes
                        if (p.token_ids[ptr_type.op_token] == .AsteriskAsterisk) {
                            rightmost_op = ptr_type.rhs;
                            ptr_type = rightmost_op.cast(Node.PtrType).?;
                        }
                        if (try opParseFn(p)) |rhs| {
                            ptr_type.rhs = rhs;
                            rightmost_op = rhs;
                        } else break;
                    },
                    .AnyFrameType => {
                        const prom = rightmost_op.cast(Node.AnyFrameType).?;
                        if (try opParseFn(p)) |rhs| {
                            prom.result.?.return_type = rhs;
                            rightmost_op = rhs;
                        } else break;
                    },
                    else => unreachable,
                }
            }

            // If any prefix op existed, a child node on the RHS is required
            switch (rightmost_op.tag) {
                .AddressOf,
                .Await,
                .BitNot,
                .BoolNot,
                .OptionalType,
                .Negation,
                .NegationWrap,
                .Resume,
                .Try,
                => {
                    const prefix_op = rightmost_op.cast(Node.SimplePrefixOp).?;
                    prefix_op.rhs = try p.expectNode(childParseFn, .{
                        .InvalidToken = .{ .token = p.tok_i },
                    });
                },
                .ArrayType => {
                    const prefix_op = rightmost_op.cast(Node.ArrayType).?;
                    prefix_op.rhs = try p.expectNode(childParseFn, .{
                        .InvalidToken = .{ .token = p.tok_i },
                    });
                },
                .ArrayTypeSentinel => {
                    const prefix_op = rightmost_op.cast(Node.ArrayTypeSentinel).?;
                    prefix_op.rhs = try p.expectNode(childParseFn, .{
                        .InvalidToken = .{ .token = p.tok_i },
                    });
                },
                .PtrType => {
                    const prefix_op = rightmost_op.cast(Node.PtrType).?;
                    prefix_op.rhs = try p.expectNode(childParseFn, .{
                        .InvalidToken = .{ .token = p.tok_i },
                    });
                },
                .SliceType => {
                    const prefix_op = rightmost_op.cast(Node.SliceType).?;
                    prefix_op.rhs = try p.expectNode(childParseFn, .{
                        .InvalidToken = .{ .token = p.tok_i },
                    });
                },
                .AnyFrameType => {
                    const prom = rightmost_op.cast(Node.AnyFrameType).?;
                    prom.result.?.return_type = try p.expectNode(childParseFn, .{
                        .InvalidToken = .{ .token = p.tok_i },
                    });
                },
                else => unreachable,
            }

            return first_op;
        }

        // Otherwise, the child node is optional
        return childParseFn(p);
    }

    /// Child (Op Child)*
    /// Child (Op Child)?
    fn parseBinOpExpr(
        p: *Parser,
        opParseFn: NodeParseFn,
        childParseFn: NodeParseFn,
        chain: enum {
            Once,
            Infinitely,
        },
    ) Error!?*Node {
        var res = (try childParseFn(p)) orelse return null;

        while (try opParseFn(p)) |node| {
            const right = try p.expectNode(childParseFn, .{
                .InvalidToken = .{ .token = p.tok_i },
            });
            const left = res;
            res = node;

            if (node.castTag(.Catch)) |op| {
                op.lhs = left;
                op.rhs = right;
            } else if (node.cast(Node.SimpleInfixOp)) |op| {
                op.lhs = left;
                op.rhs = right;
            }

            switch (chain) {
                .Once => break,
                .Infinitely => continue,
            }
        }

        return res;
    }

    fn createInfixOp(p: *Parser, op_token: TokenIndex, tag: Node.Tag) !*Node {
        const node = try p.arena.allocator.create(Node.SimpleInfixOp);
        node.* = .{
            .base = Node{ .tag = tag },
            .op_token = op_token,
            .lhs = undefined, // set by caller
            .rhs = undefined, // set by caller
        };
        return &node.base;
    }

    fn eatToken(p: *Parser, id: Token.Id) ?TokenIndex {
        return if (p.token_ids[p.tok_i] == id) p.nextToken() else null;
    }

    fn expectToken(p: *Parser, id: Token.Id) Error!TokenIndex {
        return (try p.expectTokenRecoverable(id)) orelse error.ParseError;
    }

    fn expectTokenRecoverable(p: *Parser, id: Token.Id) !?TokenIndex {
        const token = p.nextToken();
        if (p.token_ids[token] != id) {
            try p.errors.append(p.gpa, .{
                .ExpectedToken = .{ .token = token, .expected_id = id },
            });
            // go back so that we can recover properly
            p.putBackToken(token);
            return null;
        }
        return token;
    }

    fn nextToken(p: *Parser) TokenIndex {
        const result = p.tok_i;
        p.tok_i += 1;
        assert(p.token_ids[result] != .LineComment);
        if (p.tok_i >= p.token_ids.len) return result;

        while (true) {
            if (p.token_ids[p.tok_i] != .LineComment) return result;
            p.tok_i += 1;
        }
    }

    fn putBackToken(p: *Parser, putting_back: TokenIndex) void {
        while (p.tok_i > 0) {
            p.tok_i -= 1;
            if (p.token_ids[p.tok_i] == .LineComment) continue;
            assert(putting_back == p.tok_i);
            return;
        }
    }

    /// TODO Delete this function. I don't like the inversion of control.
    fn expectNode(
        p: *Parser,
        parseFn: NodeParseFn,
        /// if parsing fails
        err: AstError,
    ) Error!*Node {
        return (try p.expectNodeRecoverable(parseFn, err)) orelse return error.ParseError;
    }

    /// TODO Delete this function. I don't like the inversion of control.
    fn expectNodeRecoverable(
        p: *Parser,
        parseFn: NodeParseFn,
        /// if parsing fails
        err: AstError,
    ) !?*Node {
        return (try parseFn(p)) orelse {
            try p.errors.append(p.gpa, err);
            return null;
        };
    }
};

fn ParseFn(comptime T: type) type {
    return fn (p: *Parser) Error!T;
}

test "std.zig.parser" {
    _ = @import("parser_test.zig");
}
