const std = @import("std");
const Io = std.Io;
const mem = std.mem;
const Allocator = mem.Allocator;
const assert = std.debug.assert;
const big = std.math.big;

const Attribute = @import("Attribute.zig");
const Builtins = @import("Builtins.zig");
const evalBuiltin = @import("Builtins/eval.zig").eval;
const char_info = @import("char_info.zig");
const Compilation = @import("Compilation.zig");
const Diagnostics = @import("Diagnostics.zig");
const InitList = @import("InitList.zig");
const Preprocessor = @import("Preprocessor.zig");
const record_layout = @import("record_layout.zig");
const Source = @import("Source.zig");
const StringId = @import("StringInterner.zig").StringId;
const SymbolStack = @import("SymbolStack.zig");
const Symbol = SymbolStack.Symbol;
const text_literal = @import("text_literal.zig");
const Tokenizer = @import("Tokenizer.zig");
const Tree = @import("Tree.zig");
const Token = Tree.Token;
const NumberPrefix = Token.NumberPrefix;
const NumberSuffix = Token.NumberSuffix;
const TokenIndex = Tree.TokenIndex;
const Node = Tree.Node;
const TypeStore = @import("TypeStore.zig");
const Type = TypeStore.Type;
const QualType = TypeStore.QualType;
const Value = @import("Value.zig");

const NodeList = std.ArrayList(Node.Index);
const Switch = struct {
    default: ?TokenIndex = null,
    ranges: std.ArrayList(Range) = .empty,
    qt: QualType,
    comp: *const Compilation,

    const Range = struct {
        first: Value,
        last: Value,
        tok: TokenIndex,
    };

    fn add(s: *Switch, first: Value, last: Value, tok: TokenIndex) !?Range {
        for (s.ranges.items) |range| {
            if (last.compare(.gte, range.first, s.comp) and first.compare(.lte, range.last, s.comp)) {
                return range; // They overlap.
            }
        }
        try s.ranges.append(s.comp.gpa, .{
            .first = first,
            .last = last,
            .tok = tok,
        });
        return null;
    }
};

const Label = union(enum) {
    unresolved_goto: TokenIndex,
    label: TokenIndex,
};

const InitContext = enum {
    /// inits do not need to be compile-time constants
    runtime,
    /// constexpr variable, could be any scope but inits must be compile-time constants
    constexpr,
    /// static and global variables, inits must be compile-time constants
    static,
};

pub const Error = Compilation.Error || error{ParsingFailed};

/// An attribute that has been parsed but not yet validated in its context
const TentativeAttribute = struct {
    attr: Attribute,
    tok: TokenIndex,
    seen: bool = false,
};

/// How the parser handles const int decl references when it is expecting an integer
/// constant expression.
const ConstDeclFoldingMode = enum {
    /// fold const decls as if they were literals
    fold_const_decls,
    /// fold const decls as if they were literals and issue GNU extension diagnostic
    gnu_folding_extension,
    /// fold const decls as if they were literals and issue VLA diagnostic
    gnu_vla_folding_extension,
    /// folding const decls is prohibited; return an unavailable value
    no_const_decl_folding,
};

const Parser = @This();

// values from preprocessor
pp: *Preprocessor,
comp: *Compilation,
diagnostics: *Diagnostics,
tok_ids: []const Token.Id,
tok_i: TokenIndex = 0,

/// The AST being constructed.
tree: Tree,

// buffers used during compilation
syms: SymbolStack = .{},
strings: std.array_list.Aligned(u8, .@"4") = .empty,
labels: std.ArrayList(Label) = .empty,
list_buf: NodeList = .empty,
decl_buf: NodeList = .empty,
/// Function type parameters, also used for generic selection association
/// duplicate checking.
param_buf: std.ArrayList(Type.Func.Param) = .empty,
/// Enum type fields.
enum_buf: std.ArrayList(Type.Enum.Field) = .empty,
/// Record type fields.
record_buf: std.ArrayList(Type.Record.Field) = .empty,
/// Attributes that have been parsed but not yet validated or applied.
attr_buf: std.MultiArrayList(TentativeAttribute) = .empty,
/// Used to store validated attributes before they are applied to types.
attr_application_buf: std.ArrayList(Attribute) = .empty,
/// type name -> variable name location for tentative definitions (top-level defs with thus-far-incomplete types)
/// e.g. `struct Foo bar;` where `struct Foo` is not defined yet.
/// The key is the StringId of `Foo` and the value is the TokenIndex of `bar`
/// Items are removed if the type is subsequently completed with a definition.
/// We only store the first tentative definition that uses a given type because this map is only used
/// for issuing an error message, and correcting the first error for a type will fix all of them for that type.
tentative_defs: std.AutoHashMapUnmanaged(StringId, TokenIndex) = .empty,

// configuration and miscellaneous info
no_eval: bool = false,
in_macro: bool = false,
extension_suppressed: bool = false,
contains_address_of_label: bool = false,
label_count: u32 = 0,
const_decl_folding: ConstDeclFoldingMode = .fold_const_decls,
/// location of first computed goto in function currently being parsed
/// if a computed goto is used, the function must contain an
/// address-of-label expression (tracked with contains_address_of_label)
computed_goto_tok: ?TokenIndex = null,

/// __auto_type may only be used with a single declarator. Keep track of the name
/// so that it is not used in its own initializer.
auto_type_decl_name: StringId = .empty,

init_context: InitContext = .runtime,

/// Various variables that are different for each function.
func: struct {
    /// null if not in function, will always be plain func
    qt: ?QualType = null,
    name: TokenIndex = 0,
    ident: ?Result = null,
    pretty_ident: ?Result = null,
} = .{},

/// Various variables that are different for each record.
record: struct {
    // invalid means we're not parsing a record
    kind: Token.Id = .invalid,
    flexible_field: ?TokenIndex = null,
    start: usize = 0,

    fn addField(r: @This(), p: *Parser, name: StringId, tok: TokenIndex) Error!void {
        var i = p.record_members.items.len;
        while (i > r.start) {
            i -= 1;
            if (p.record_members.items[i].name == name) {
                try p.err(tok, .duplicate_member, .{p.tokSlice(tok)});
                try p.err(p.record_members.items[i].tok, .previous_definition, .{});
                break;
            }
        }
        try p.record_members.append(p.comp.gpa, .{ .name = name, .tok = tok });
    }

    fn addFieldsFromAnonymous(r: @This(), p: *Parser, record_ty: Type.Record) Error!void {
        for (record_ty.fields) |f| {
            if (f.name_tok == 0) {
                if (f.qt.getRecord(p.comp)) |field_record_ty| {
                    try r.addFieldsFromAnonymous(p, field_record_ty);
                }
            } else {
                try r.addField(p, f.name, f.name_tok);
            }
        }
    }
} = .{},
record_members: std.ArrayList(struct { tok: TokenIndex, name: StringId }) = .empty,

@"switch": ?*Switch = null,
in_loop: bool = false,
pragma_pack: ?u8 = null,
string_ids: struct {
    declspec_id: StringId,
    main_id: StringId,
    file: StringId,
    jmp_buf: StringId,
    sigjmp_buf: StringId,
    ucontext_t: StringId,
},

/// Checks codepoint for various pedantic warnings
/// Returns true if diagnostic issued
fn checkIdentifierCodepointWarnings(p: *Parser, codepoint: u21, loc: Source.Location) Compilation.Error!bool {
    assert(codepoint >= 0x80);

    const prev_total = p.diagnostics.total;
    var sf = std.heap.stackFallback(1024, p.comp.gpa);
    var allocating: Io.Writer.Allocating = .init(sf.get());
    defer allocating.deinit();

    if (!char_info.isC99IdChar(codepoint)) {
        const diagnostic: Diagnostic = .c99_compat;
        try p.diagnostics.add(.{
            .kind = diagnostic.kind,
            .text = diagnostic.fmt,
            .extension = diagnostic.extension,
            .opt = diagnostic.opt,
            .location = loc.expand(p.comp),
        });
    }
    if (char_info.isInvisible(codepoint)) {
        const diagnostic: Diagnostic = .unicode_zero_width;
        p.formatArgs(&allocating.writer, diagnostic.fmt, .{Codepoint.init(codepoint)}) catch return error.OutOfMemory;

        try p.diagnostics.add(.{
            .kind = diagnostic.kind,
            .text = allocating.written(),
            .extension = diagnostic.extension,
            .opt = diagnostic.opt,
            .location = loc.expand(p.comp),
        });
    }
    if (char_info.homoglyph(codepoint)) |resembles| {
        const diagnostic: Diagnostic = .unicode_homoglyph;
        p.formatArgs(&allocating.writer, diagnostic.fmt, .{ Codepoint.init(codepoint), resembles }) catch return error.OutOfMemory;

        try p.diagnostics.add(.{
            .kind = diagnostic.kind,
            .text = allocating.written(),
            .extension = diagnostic.extension,
            .opt = diagnostic.opt,
            .location = loc.expand(p.comp),
        });
    }
    return p.diagnostics.total != prev_total;
}

/// Issues diagnostics for the current extended identifier token
/// Return value indicates whether the token should be considered an identifier
/// true means consider the token to actually be an identifier
/// false means it is not
fn validateExtendedIdentifier(p: *Parser) !bool {
    assert(p.tok_ids[p.tok_i] == .extended_identifier);

    const slice = p.tokSlice(p.tok_i);
    const view = std.unicode.Utf8View.init(slice) catch {
        try p.err(p.tok_i, .invalid_utf8, .{});
        return error.FatalError;
    };
    var it = view.iterator();

    var valid_identifier = true;
    var warned = false;
    var len: usize = 0;
    var invalid_char: u21 = undefined;
    var loc = p.pp.tokens.items(.loc)[p.tok_i];

    var normalized = true;
    var last_canonical_class: char_info.CanonicalCombiningClass = .not_reordered;
    const standard = p.comp.langopts.standard;
    while (it.nextCodepoint()) |codepoint| {
        defer {
            len += 1;
            loc.byte_offset += std.unicode.utf8CodepointSequenceLength(codepoint) catch unreachable;
        }
        if (codepoint == '$') {
            warned = true;
            if (p.comp.langopts.dollars_in_identifiers) {
                const diagnostic: Diagnostic = .dollar_in_identifier_extension;
                try p.diagnostics.add(.{
                    .kind = diagnostic.kind,
                    .text = diagnostic.fmt,
                    .extension = diagnostic.extension,
                    .opt = diagnostic.opt,
                    .location = loc.expand(p.comp),
                });
            }
        }

        if (codepoint <= 0x7F) continue;
        if (!valid_identifier) continue;

        const allowed = standard.codepointAllowedInIdentifier(codepoint, len == 0);
        if (!allowed) {
            invalid_char = codepoint;
            valid_identifier = false;
            continue;
        }

        if (!warned) {
            warned = try p.checkIdentifierCodepointWarnings(codepoint, loc);
        }

        // Check NFC normalization.
        if (!normalized) continue;
        const canonical_class = char_info.getCanonicalClass(codepoint);
        if (@intFromEnum(last_canonical_class) > @intFromEnum(canonical_class) and
            canonical_class != .not_reordered)
        {
            normalized = false;
            try p.err(p.tok_i, .identifier_not_normalized, .{slice});
            continue;
        }
        if (char_info.isNormalized(codepoint) != .yes) {
            normalized = false;
            try p.err(p.tok_i, .identifier_not_normalized, .{Normalized.init(slice)});
        }
        last_canonical_class = canonical_class;
    }

    if (!valid_identifier) {
        if (len == 1) {
            try p.err(p.tok_i, .unexpected_character, .{Codepoint.init(invalid_char)});
            return false;
        } else {
            try p.err(p.tok_i, .invalid_identifier_start_char, .{Codepoint.init(invalid_char)});
        }
    }

    return true;
}

fn eatIdentifier(p: *Parser) !?TokenIndex {
    switch (p.tok_ids[p.tok_i]) {
        .identifier => {},
        .extended_identifier => {
            if (!try p.validateExtendedIdentifier()) {
                p.tok_i += 1;
                return null;
            }
        },
        else => return null,
    }
    p.tok_i += 1;

    // Handle illegal '$' characters in identifiers
    if (!p.comp.langopts.dollars_in_identifiers) {
        if (p.tok_ids[p.tok_i] == .invalid and p.tokSlice(p.tok_i)[0] == '$') {
            try p.err(p.tok_i, .dollars_in_identifiers, .{});
            p.tok_i += 1;
            return error.ParsingFailed;
        }
    }

    return p.tok_i - 1;
}

fn expectIdentifier(p: *Parser) Error!TokenIndex {
    const actual = p.tok_ids[p.tok_i];
    if (actual != .identifier and actual != .extended_identifier) {
        return p.errExpectedToken(.identifier, actual);
    }

    return (try p.eatIdentifier()) orelse error.ParsingFailed;
}

fn eatToken(p: *Parser, id: Token.Id) ?TokenIndex {
    assert(id != .identifier and id != .extended_identifier); // use eatIdentifier
    if (p.tok_ids[p.tok_i] == id) {
        defer p.tok_i += 1;
        return p.tok_i;
    } else return null;
}

fn expectToken(p: *Parser, expected: Token.Id) Error!TokenIndex {
    assert(expected != .identifier and expected != .extended_identifier); // use expectIdentifier
    const actual = p.tok_ids[p.tok_i];
    if (actual != expected) return p.errExpectedToken(expected, actual);
    defer p.tok_i += 1;
    return p.tok_i;
}

pub fn tokSlice(p: *Parser, tok: TokenIndex) []const u8 {
    if (p.tok_ids[tok].lexeme()) |some| return some;
    const loc = p.pp.tokens.items(.loc)[tok];
    var tmp_tokenizer: Tokenizer = .{
        .buf = p.comp.getSource(loc.id).buf,
        .langopts = p.comp.langopts,
        .index = loc.byte_offset,
        .source = .generated,
        .splice_locs = &.{},
    };
    const res = tmp_tokenizer.next();
    return tmp_tokenizer.buf[res.start..res.end];
}

fn expectClosing(p: *Parser, opening: TokenIndex, id: Token.Id) Error!void {
    _ = p.expectToken(id) catch |e| {
        if (e == error.ParsingFailed) {
            try p.err(opening, switch (id) {
                .r_paren => .to_match_paren,
                .r_brace => .to_match_brace,
                .r_bracket => .to_match_brace,
                else => unreachable,
            }, .{});
        }
        return e;
    };
}

pub const Diagnostic = @import("Parser/Diagnostic.zig");

pub fn err(p: *Parser, tok_i: TokenIndex, diagnostic: Diagnostic, args: anytype) Compilation.Error!void {
    if (p.extension_suppressed) {
        if (diagnostic.extension and diagnostic.kind == .off) return;
    }
    if (diagnostic.suppress_version) |some| if (p.comp.langopts.standard.atLeast(some)) return;
    if (diagnostic.suppress_unless_version) |some| if (!p.comp.langopts.standard.atLeast(some)) return;
    if (p.diagnostics.effectiveKind(diagnostic) == .off) return;

    var sf = std.heap.stackFallback(1024, p.comp.gpa);
    var allocating: Io.Writer.Allocating = .init(sf.get());
    defer allocating.deinit();

    p.formatArgs(&allocating.writer, diagnostic.fmt, args) catch return error.OutOfMemory;

    const tok = p.pp.tokens.get(tok_i);
    var loc = tok.loc;
    if (tok_i != 0 and tok.id == .eof) {
        // if the token is EOF, point at the end of the previous token instead
        const prev = p.pp.tokens.get(tok_i - 1);
        loc = prev.loc;
        loc.byte_offset += @intCast(p.tokSlice(tok_i - 1).len);
    }
    try p.diagnostics.addWithLocation(p.comp, .{
        .kind = diagnostic.kind,
        .text = allocating.written(),
        .opt = diagnostic.opt,
        .extension = diagnostic.extension,
        .location = loc.expand(p.comp),
    }, p.pp.expansionSlice(tok_i), true);
}

fn formatArgs(p: *Parser, w: *Io.Writer, fmt: []const u8, args: anytype) !void {
    var i: usize = 0;
    inline for (std.meta.fields(@TypeOf(args))) |arg_info| {
        const arg = @field(args, arg_info.name);
        i += switch (@TypeOf(arg)) {
            []const u8 => try Diagnostics.formatString(w, fmt[i..], arg),
            Tree.Token.Id => try formatTokenId(w, fmt[i..], arg),
            QualType => try p.formatQualType(w, fmt[i..], arg),
            text_literal.Ascii => try arg.format(w, fmt[i..]),
            Result => try p.formatResult(w, fmt[i..], arg),
            *Result => try p.formatResult(w, fmt[i..], arg.*),
            Enumerator, *Enumerator => try p.formatResult(w, fmt[i..], .{
                .node = undefined,
                .val = arg.val,
                .qt = arg.qt,
            }),
            Codepoint => try arg.format(w, fmt[i..]),
            Normalized => try arg.format(w, fmt[i..]),
            Escaped => try arg.format(w, fmt[i..]),
            else => switch (@typeInfo(@TypeOf(arg))) {
                .int, .comptime_int => try Diagnostics.formatInt(w, fmt[i..], arg),
                .pointer => try Diagnostics.formatString(w, fmt[i..], arg),
                else => comptime unreachable,
            },
        };
    }
    try w.writeAll(fmt[i..]);
}

fn formatTokenId(w: *Io.Writer, fmt: []const u8, tok_id: Tree.Token.Id) !usize {
    const i = Diagnostics.templateIndex(w, fmt, "{tok_id}");
    try w.writeAll(tok_id.symbol());
    return i;
}

fn formatQualType(p: *Parser, w: *Io.Writer, fmt: []const u8, qt: QualType) !usize {
    const i = Diagnostics.templateIndex(w, fmt, "{qt}");
    try w.writeByte('\'');
    try qt.print(p.comp, w);
    try w.writeByte('\'');

    if (qt.isC23Auto()) return i;
    if (qt.get(p.comp, .vector)) |vector_ty| {
        try w.print(" (vector of {d} '", .{vector_ty.len});
        try vector_ty.elem.printDesugared(p.comp, w);
        try w.writeAll("' values)");
    } else if (qt.shouldDesugar(p.comp)) {
        try w.writeAll(" (aka '");
        try qt.printDesugared(p.comp, w);
        try w.writeAll("')");
    }
    return i;
}

fn formatResult(p: *Parser, w: *Io.Writer, fmt: []const u8, res: Result) !usize {
    const i = Diagnostics.templateIndex(w, fmt, "{value}");
    switch (res.val.opt_ref) {
        .none => try w.writeAll("(none)"),
        .null => try w.writeAll("nullptr_t"),
        else => if (try res.val.print(res.qt, p.comp, w)) |nested| switch (nested) {
            .pointer => |ptr| {
                const ptr_node: Node.Index = @enumFromInt(ptr.node);
                const decl_name = p.tree.tokSlice(ptr_node.tok(&p.tree));
                try ptr.offset.printPointer(decl_name, p.comp, w);
            },
        },
    }
    return i;
}

const Normalized = struct {
    str: []const u8,

    fn init(str: []const u8) Normalized {
        return .{ .str = str };
    }

    pub fn format(ctx: Normalized, w: *Io.Writer, fmt: []const u8) !usize {
        const i = Diagnostics.templateIndex(w, fmt, "{normalized}");
        var it: std.unicode.Utf8Iterator = .{
            .bytes = ctx.str,
            .i = 0,
        };
        while (it.nextCodepoint()) |codepoint| {
            if (codepoint < 0x7F) {
                try w.writeByte(@intCast(codepoint));
            } else if (codepoint < 0xFFFF) {
                try w.writeAll("\\u");
                try w.printInt(codepoint, 16, .upper, .{
                    .fill = '0',
                    .width = 4,
                });
            } else {
                try w.writeAll("\\U");
                try w.printInt(codepoint, 16, .upper, .{
                    .fill = '0',
                    .width = 8,
                });
            }
        }
        return i;
    }
};

const Codepoint = struct {
    codepoint: u21,

    fn init(codepoint: u21) Codepoint {
        return .{ .codepoint = codepoint };
    }

    pub fn format(ctx: Codepoint, w: *Io.Writer, fmt: []const u8) !usize {
        const i = Diagnostics.templateIndex(w, fmt, "{codepoint}");
        try w.print("{X:0>4}", .{ctx.codepoint});
        return i;
    }
};

const Escaped = struct {
    str: []const u8,

    fn init(str: []const u8) Escaped {
        return .{ .str = str };
    }

    pub fn format(ctx: Escaped, w: *Io.Writer, fmt: []const u8) !usize {
        const i = Diagnostics.templateIndex(w, fmt, "{s}");
        try std.zig.stringEscape(ctx.str, w);
        return i;
    }
};

pub fn todo(p: *Parser, msg: []const u8) Error {
    try p.err(p.tok_i, .todo, .{msg});
    return error.ParsingFailed;
}

pub fn removeNull(p: *Parser, str: Value) !Value {
    const strings_top = p.strings.items.len;
    defer p.strings.items.len = strings_top;
    {
        const bytes = p.comp.interner.get(str.ref()).bytes;
        try p.strings.appendSlice(p.comp.gpa, bytes[0 .. bytes.len - 1]);
    }
    return Value.intern(p.comp, .{ .bytes = p.strings.items[strings_top..] });
}

pub fn errValueChanged(p: *Parser, tok_i: TokenIndex, diagnostic: Diagnostic, res: Result, old_val: Value, int_qt: QualType) !void {
    const zero_str = if (res.val.isZero(p.comp)) "non-zero " else "";
    const old_res: Result = .{
        .node = undefined,
        .val = old_val,
        .qt = res.qt,
    };
    const new_res: Result = .{
        .node = undefined,
        .val = res.val,
        .qt = int_qt,
    };
    try p.err(tok_i, diagnostic, .{ res.qt, int_qt, zero_str, old_res, new_res });
}

fn checkDeprecatedUnavailable(p: *Parser, ty: QualType, usage_tok: TokenIndex, decl_tok: TokenIndex) !void {
    if (ty.getAttribute(p.comp, .@"error")) |@"error"| {
        const msg_str = p.comp.interner.get(@"error".msg.ref()).bytes;
        try p.err(usage_tok, .error_attribute, .{ p.tokSlice(@"error".__name_tok), Escaped.init(msg_str) });
    }
    if (ty.getAttribute(p.comp, .warning)) |warning| {
        const msg_str = p.comp.interner.get(warning.msg.ref()).bytes;
        try p.err(usage_tok, .warning_attribute, .{ p.tokSlice(warning.__name_tok), Escaped.init(msg_str) });
    }
    if (ty.getAttribute(p.comp, .unavailable)) |unavailable| {
        try p.errDeprecated(usage_tok, .unavailable, unavailable.msg);
        try p.err(unavailable.__name_tok, .unavailable_note, .{p.tokSlice(decl_tok)});
        return error.ParsingFailed;
    }
    if (ty.getAttribute(p.comp, .deprecated)) |deprecated| {
        try p.errDeprecated(usage_tok, .deprecated_declarations, deprecated.msg);
        try p.err(deprecated.__name_tok, .deprecated_note, .{p.tokSlice(decl_tok)});
    }
}

fn errDeprecated(p: *Parser, tok_i: TokenIndex, diagnostic: Diagnostic, msg: ?Value) Compilation.Error!void {
    const colon_str: []const u8 = if (msg != null) ": " else "";
    const msg_str: []const u8 = if (msg) |m| p.comp.interner.get(m.ref()).bytes else "";
    return p.err(tok_i, diagnostic, .{ p.tokSlice(tok_i), colon_str, Escaped.init(msg_str) });
}

fn addNode(p: *Parser, node: Tree.Node) Allocator.Error!Node.Index {
    if (p.in_macro) return undefined;
    return p.tree.addNode(node);
}

fn errExpectedToken(p: *Parser, expected: Token.Id, actual: Token.Id) Error {
    switch (actual) {
        .invalid => try p.err(p.tok_i, .expected_invalid, .{expected}),
        .eof => try p.err(p.tok_i, .expected_eof, .{expected}),
        else => try p.err(p.tok_i, .expected_token, .{ expected, actual }),
    }
    return error.ParsingFailed;
}

fn addList(p: *Parser, nodes: []const Node.Index) Allocator.Error!Tree.Node.Range {
    if (p.in_macro) return Tree.Node.Range{ .start = 0, .end = 0 };
    const start: u32 = @intCast(p.data.items.len);
    try p.data.appendSlice(nodes);
    const end: u32 = @intCast(p.data.items.len);
    return Tree.Node.Range{ .start = start, .end = end };
}

/// Recursively sets the defintion field of `tentative_decl` to `definition`.
pub fn setTentativeDeclDefinition(p: *Parser, tentative_decl: Node.Index, definition: Node.Index) void {
    const node_data = &p.tree.nodes.items(.data)[@intFromEnum(tentative_decl)];
    switch (p.tree.nodes.items(.tag)[@intFromEnum(tentative_decl)]) {
        .fn_proto => {},
        .variable => {},
        else => return,
    }

    const prev: Node.OptIndex = @enumFromInt(node_data[2]);

    node_data[2] = @intFromEnum(definition);
    if (prev.unpack()) |some| {
        p.setTentativeDeclDefinition(some, definition);
    }
}

/// Clears the defintion field of declarations that were not defined so that
/// the field always contains a _def if present.
fn clearNonTentativeDefinitions(p: *Parser) void {
    const tags = p.tree.nodes.items(.tag);
    const data = p.tree.nodes.items(.data);
    for (p.tree.root_decls.items) |root_decl| {
        switch (tags[@intFromEnum(root_decl)]) {
            .fn_proto => {
                const node_data = &data[@intFromEnum(root_decl)];
                if (node_data[2] != @intFromEnum(Node.OptIndex.null)) {
                    if (tags[node_data[2]] != .fn_def) {
                        node_data[2] = @intFromEnum(Node.OptIndex.null);
                    }
                }
            },
            .variable => {
                const node_data = &data[@intFromEnum(root_decl)];
                if (node_data[2] != @intFromEnum(Node.OptIndex.null)) {
                    if (tags[node_data[2]] != .variable_def) {
                        node_data[2] = @intFromEnum(Node.OptIndex.null);
                    }
                }
            },
            else => {},
        }
    }
}

fn findLabel(p: *Parser, name: []const u8) ?TokenIndex {
    for (p.labels.items) |item| {
        switch (item) {
            .label => |l| if (mem.eql(u8, p.tokSlice(l), name)) return l,
            .unresolved_goto => {},
        }
    }
    return null;
}

fn nodeIs(p: *Parser, node: Node.Index, comptime tag: std.meta.Tag(Tree.Node)) bool {
    return p.getNode(node, tag) != null;
}

pub fn getDecayedStringLiteral(p: *Parser, node: Node.Index) ?Value {
    var cur = node;
    while (true) {
        switch (cur.get(&p.tree)) {
            .paren_expr => |un| cur = un.operand,
            .string_literal_expr => return p.tree.value_map.get(cur),
            .cast => |cast| switch (cast.kind) {
                .no_op, .bitcast, .array_to_pointer => cur = cast.operand,
                else => return null,
            },
            else => return null,
        }
    }
}

fn getNode(p: *Parser, node: Node.Index, comptime tag: std.meta.Tag(Tree.Node)) ?@FieldType(Node, @tagName(tag)) {
    loop: switch (node.get(&p.tree)) {
        .paren_expr => |un| continue :loop un.operand.get(&p.tree),
        tag => |data| return data,
        else => return null,
    }
}

fn pragma(p: *Parser) Compilation.Error!bool {
    var found_pragma = false;
    while (p.eatToken(.keyword_pragma)) |_| {
        found_pragma = true;

        const name_tok = p.tok_i;
        const name = p.tokSlice(name_tok);

        const end_idx = mem.indexOfScalarPos(Token.Id, p.tok_ids, p.tok_i, .nl).?;
        const pragma_len = @as(TokenIndex, @intCast(end_idx)) - p.tok_i;
        defer p.tok_i += pragma_len + 1; // skip past .nl as well
        if (p.comp.getPragma(name)) |prag| {
            try prag.parserCB(p, p.tok_i);
        }
    }
    return found_pragma;
}

/// Issue errors for top-level definitions whose type was never completed.
fn diagnoseIncompleteDefinitions(p: *Parser) !void {
    @branchHint(.cold);

    for (p.decl_buf.items) |decl_index| {
        const node = decl_index.get(&p.tree);
        const forward = switch (node) {
            .struct_forward_decl, .union_forward_decl, .enum_forward_decl => |forward| forward,
            else => continue,
        };

        const decl_type_name = switch (forward.container_qt.base(p.comp).type) {
            .@"struct", .@"union" => |record_ty| record_ty.name,
            .@"enum" => |enum_ty| enum_ty.name,
            else => unreachable,
        };

        const tentative_def_tok = p.tentative_defs.get(decl_type_name) orelse continue;
        try p.err(tentative_def_tok, .tentative_definition_incomplete, .{forward.container_qt});
        try p.err(forward.name_or_kind_tok, .forward_declaration_here, .{forward.container_qt});
    }
}

/// root : (decl | assembly ';' | staticAssert)*
pub fn parse(pp: *Preprocessor) Compilation.Error!Tree {
    const gpa = pp.comp.gpa;
    assert(pp.linemarkers == .none);
    pp.comp.pragmaEvent(.before_parse);

    const expected_implicit_typedef_max = 7;
    try pp.tokens.ensureUnusedCapacity(gpa, expected_implicit_typedef_max);

    var p: Parser = .{
        .pp = pp,
        .comp = pp.comp,
        .diagnostics = pp.diagnostics,
        .tree = .{
            .comp = pp.comp,
            .tokens = undefined, // Set after implicit typedefs
        },
        .tok_ids = pp.tokens.items(.id),
        .string_ids = .{
            .declspec_id = try pp.comp.internString("__declspec"),
            .main_id = try pp.comp.internString("main"),
            .file = try pp.comp.internString("FILE"),
            .jmp_buf = try pp.comp.internString("jmp_buf"),
            .sigjmp_buf = try pp.comp.internString("sigjmp_buf"),
            .ucontext_t = try pp.comp.internString("ucontext_t"),
        },
    };
    errdefer p.tree.deinit();
    defer {
        p.labels.deinit(gpa);
        p.strings.deinit(gpa);
        p.syms.deinit(gpa);
        p.list_buf.deinit(gpa);
        p.decl_buf.deinit(gpa);
        p.param_buf.deinit(gpa);
        p.enum_buf.deinit(gpa);
        p.record_buf.deinit(gpa);
        p.record_members.deinit(gpa);
        p.attr_buf.deinit(gpa);
        p.attr_application_buf.deinit(gpa);
        p.tentative_defs.deinit(gpa);
    }

    try p.syms.pushScope(&p);
    defer p.syms.popScope();

    {
        if (p.comp.langopts.hasChar8_T()) {
            try p.addImplicitTypedef("char8_t", .uchar);
        }
        try p.addImplicitTypedef("__int128_t", .int128);
        try p.addImplicitTypedef("__uint128_t", .uint128);

        try p.addImplicitTypedef("__builtin_ms_va_list", .char_pointer);

        const va_list_qt = pp.comp.type_store.va_list;
        try p.addImplicitTypedef("__builtin_va_list", va_list_qt);
        pp.comp.type_store.va_list = try va_list_qt.decay(pp.comp);

        try p.addImplicitTypedef("__NSConstantString", pp.comp.type_store.ns_constant_string);

        if (p.comp.float80Type()) |float80_ty| {
            try p.addImplicitTypedef("__float80", float80_ty);
        }

        // Set here so that the newly generated tokens are included.
        p.tree.tokens = p.pp.tokens.slice();
    }
    const implicit_typedef_count = p.decl_buf.items.len;
    assert(implicit_typedef_count <= expected_implicit_typedef_max);

    while (p.eatToken(.eof) == null) {
        if (try p.pragma()) continue;
        if (try p.parseOrNextDecl(staticAssert)) continue;
        if (try p.parseOrNextDecl(decl)) continue;
        if (p.eatToken(.keyword_extension)) |_| {
            const saved_extension = p.extension_suppressed;
            defer p.extension_suppressed = saved_extension;
            p.extension_suppressed = true;

            if (try p.parseOrNextDecl(decl)) continue;
            switch (p.tok_ids[p.tok_i]) {
                .semicolon => p.tok_i += 1,
                .keyword_static_assert,
                .keyword_c23_static_assert,
                .keyword_pragma,
                .keyword_extension,
                .keyword_asm,
                .keyword_asm1,
                .keyword_asm2,
                => {},
                else => try p.err(p.tok_i, .expected_external_decl, .{}),
            }
            continue;
        }
        if (p.assembly(.global) catch |er| switch (er) {
            error.ParsingFailed => {
                p.nextExternDecl();
                continue;
            },
            else => |e| return e,
        }) |node| {
            try p.decl_buf.append(gpa, node);
            continue;
        }
        if (p.eatToken(.semicolon)) |tok| {
            try p.err(tok, .extra_semi, .{});
            const empty = try p.tree.addNode(.{ .empty_decl = .{
                .semicolon = tok,
            } });
            try p.decl_buf.append(gpa, empty);
            continue;
        }
        try p.err(p.tok_i, .expected_external_decl, .{});
        p.nextExternDecl();
    }
    if (p.tentative_defs.count() > 0) {
        try p.diagnoseIncompleteDefinitions();
    }

    p.tree.root_decls = p.decl_buf;
    p.decl_buf = .empty;

    if (p.tree.root_decls.items.len == implicit_typedef_count) {
        try p.err(p.tok_i - 1, .empty_translation_unit, .{});
    }
    pp.comp.pragmaEvent(.after_parse);

    p.clearNonTentativeDefinitions();

    return p.tree;
}

fn addImplicitTypedef(p: *Parser, name: []const u8, qt: QualType) !void {
    const gpa = p.comp.gpa;
    const start = p.comp.generated_buf.items.len;
    try p.comp.generated_buf.ensureUnusedCapacity(gpa, name.len + 1);
    p.comp.generated_buf.appendSliceAssumeCapacity(name);
    p.comp.generated_buf.appendAssumeCapacity('\n');

    const name_tok: u32 = @intCast(p.pp.tokens.len);
    p.pp.tokens.appendAssumeCapacity(.{ .id = .identifier, .loc = .{
        .id = .generated,
        .byte_offset = @intCast(start),
        .line = p.pp.generated_line,
    } });
    p.pp.generated_line += 1;

    const node = try p.addNode(.{
        .typedef = .{
            .name_tok = name_tok,
            .qt = qt,
            .implicit = true,
        },
    });

    const interned_name = try p.comp.internString(name);
    const typedef_qt = (try p.comp.type_store.put(gpa, .{ .typedef = .{
        .base = qt,
        .name = interned_name,
        .decl_node = node,
    } })).withQualifiers(qt);
    try p.syms.defineTypedef(p, interned_name, typedef_qt, name_tok, node);
    try p.decl_buf.append(gpa, node);
}

fn skipToPragmaSentinel(p: *Parser) void {
    while (true) : (p.tok_i += 1) {
        if (p.tok_ids[p.tok_i] == .nl) return;
        if (p.tok_ids[p.tok_i] == .eof) {
            p.tok_i -= 1;
            return;
        }
    }
}

fn parseOrNextDecl(p: *Parser, comptime func: fn (*Parser) Error!bool) Compilation.Error!bool {
    return func(p) catch |er| switch (er) {
        error.ParsingFailed => {
            p.nextExternDecl();
            return true;
        },
        else => |e| return e,
    };
}

fn nextExternDecl(p: *Parser) void {
    var parens: u32 = 0;
    while (true) : (p.tok_i += 1) {
        switch (p.tok_ids[p.tok_i]) {
            .l_paren, .l_brace, .l_bracket => parens += 1,
            .r_paren, .r_brace, .r_bracket => parens -|= 1,
            .keyword_typedef,
            .keyword_extern,
            .keyword_static,
            .keyword_auto,
            .keyword_register,
            .keyword_thread_local,
            .keyword_c23_thread_local,
            .keyword_inline,
            .keyword_inline1,
            .keyword_inline2,
            .keyword_noreturn,
            .keyword_void,
            .keyword_bool,
            .keyword_c23_bool,
            .keyword_char,
            .keyword_short,
            .keyword_int,
            .keyword_long,
            .keyword_signed,
            .keyword_signed1,
            .keyword_signed2,
            .keyword_unsigned,
            .keyword_float,
            .keyword_double,
            .keyword_complex,
            .keyword_atomic,
            .keyword_enum,
            .keyword_struct,
            .keyword_union,
            .keyword_alignas,
            .keyword_c23_alignas,
            .identifier,
            .extended_identifier,
            .keyword_typeof,
            .keyword_typeof1,
            .keyword_typeof2,
            .keyword_typeof_unqual,
            .keyword_extension,
            .keyword_bit_int,
            => if (parens == 0) return,
            .keyword_pragma => p.skipToPragmaSentinel(),
            .eof => return,
            .semicolon => if (parens == 0) {
                p.tok_i += 1;
                return;
            },
            else => {},
        }
    }
}

fn skipTo(p: *Parser, id: Token.Id) void {
    var parens: u32 = 0;
    while (true) : (p.tok_i += 1) {
        if (p.tok_ids[p.tok_i] == id and parens == 0) {
            p.tok_i += 1;
            return;
        }
        switch (p.tok_ids[p.tok_i]) {
            .l_paren, .l_brace, .l_bracket => parens += 1,
            .r_paren, .r_brace, .r_bracket => if (parens != 0) {
                parens -= 1;
            },
            .keyword_pragma => p.skipToPragmaSentinel(),
            .eof => return,
            else => {},
        }
    }
}

/// Called after a typedef is defined
fn typedefDefined(p: *Parser, name: StringId, ty: QualType) void {
    if (name == p.string_ids.file) {
        p.comp.type_store.file = ty;
    } else if (name == p.string_ids.jmp_buf) {
        p.comp.type_store.jmp_buf = ty;
    } else if (name == p.string_ids.sigjmp_buf) {
        p.comp.type_store.sigjmp_buf = ty;
    } else if (name == p.string_ids.ucontext_t) {
        p.comp.type_store.ucontext_t = ty;
    }
}

// ====== declarations ======

/// decl
///  : declSpec (initDeclarator ( ',' initDeclarator)*)? ';'
///  | declSpec declarator decl* compoundStmt
fn decl(p: *Parser) Error!bool {
    const gpa = p.comp.gpa;
    _ = try p.pragma();
    const first_tok = p.tok_i;
    const attr_buf_top = p.attr_buf.len;
    defer p.attr_buf.len = attr_buf_top;

    try p.attributeSpecifier();

    var decl_spec = (try p.declSpec()) orelse blk: {
        if (p.func.qt != null) {
            p.tok_i = first_tok;
            return false;
        }
        switch (p.tok_ids[first_tok]) {
            .asterisk, .l_paren => {},
            .identifier, .extended_identifier => switch (p.tok_ids[first_tok + 1]) {
                .identifier, .extended_identifier => {
                    // The most likely reason for `identifier identifier` is
                    // an unknown type name.
                    try p.err(p.tok_i, .unknown_type_name, .{p.tokSlice(p.tok_i)});
                    p.tok_i += 1;
                    break :blk DeclSpec{ .qt = .invalid };
                },
                else => {},
            },
            else => if (p.tok_i != first_tok) {
                try p.err(p.tok_i, .expected_ident_or_l_paren, .{});
                return error.ParsingFailed;
            } else return false,
        }
        var builder: TypeStore.Builder = .{ .parser = p };
        break :blk DeclSpec{ .qt = try builder.finish() };
    };
    if (decl_spec.noreturn) |tok| {
        const attr = Attribute{ .tag = .noreturn, .args = .{ .noreturn = .{} }, .syntax = .keyword };
        try p.attr_buf.append(gpa, .{ .attr = attr, .tok = tok });
    }

    var decl_node = try p.tree.addNode(.{ .empty_decl = .{
        .semicolon = first_tok,
    } });
    var init_d = (try p.initDeclarator(&decl_spec, attr_buf_top, decl_node)) orelse {
        _ = try p.expectToken(.semicolon);

        missing_decl: {
            if (decl_spec.qt.type(p.comp) == .typeof) {
                // we follow GCC and clang's behavior here
                try p.err(first_tok, .missing_declaration, .{});
                return true;
            }
            switch (decl_spec.qt.base(p.comp).type) {
                .@"enum" => break :missing_decl,
                .@"struct", .@"union" => |record_ty| if (!record_ty.isAnonymous(p.comp)) break :missing_decl,
                else => {},
            }

            try p.err(first_tok, .missing_declaration, .{});
            return true;
        }

        const attrs = p.attr_buf.items(.attr)[attr_buf_top..];
        const toks = p.attr_buf.items(.tok)[attr_buf_top..];
        for (attrs, toks) |attr, tok| {
            try p.err(tok, .ignored_record_attr, .{
                @tagName(attr.tag), @tagName(decl_spec.qt.base(p.comp).type),
            });
        }
        return true;
    };

    // Check for function definition.
    if (init_d.d.declarator_type == .func and init_d.initializer == null) fn_def: {
        switch (p.tok_ids[p.tok_i]) {
            .comma, .semicolon => break :fn_def,
            .l_brace => {},
            else => if (init_d.d.old_style_func == null) {
                try p.err(p.tok_i - 1, .expected_fn_body, .{});
                return true;
            },
        }
        if (p.func.qt != null) try p.err(p.tok_i, .func_not_in_root, .{});

        const interned_declarator_name = try p.comp.internString(p.tokSlice(init_d.d.name));
        try p.syms.defineSymbol(p, interned_declarator_name, init_d.d.qt, init_d.d.name, decl_node, .{}, false);
        const func = p.func;
        p.func = .{
            .qt = init_d.d.qt,
            .name = init_d.d.name,
        };
        defer p.func = func;

        // Check return type of 'main' function.
        if (interned_declarator_name == p.string_ids.main_id) {
            const func_ty = init_d.d.qt.get(p.comp, .func).?;
            const int_ty = func_ty.return_type.get(p.comp, .int);
            if (int_ty == null or int_ty.? != .int) {
                try p.err(init_d.d.name, .main_return_type, .{});
            }
        }

        try p.syms.pushScope(p);
        defer p.syms.popScope();

        // Collect old style parameter declarations.
        if (init_d.d.old_style_func != null) {
            const param_buf_top = p.param_buf.items.len;
            defer p.param_buf.items.len = param_buf_top;

            // We cannot refer to the function type here because the pointer to
            // type_store.extra might get invalidated while parsing the param decls.
            const func_qt = init_d.d.qt.base(p.comp).qt;
            const params_len = func_qt.get(p.comp, .func).?.params.len;

            const new_params = try p.param_buf.addManyAsSlice(gpa, params_len);
            for (new_params) |*new_param| {
                new_param.name = .empty;
            }

            param_loop: while (true) {
                const param_decl_spec = (try p.declSpec()) orelse break;
                if (p.eatToken(.semicolon)) |semi| {
                    try p.err(semi, .missing_declaration, .{});
                    continue :param_loop;
                }

                while (true) {
                    const attr_buf_top_declarator = p.attr_buf.len;
                    defer p.attr_buf.len = attr_buf_top_declarator;

                    var param_d = (try p.declarator(param_decl_spec.qt, .param)) orelse {
                        try p.err(first_tok, .missing_declaration, .{});
                        _ = try p.expectToken(.semicolon);
                        continue :param_loop;
                    };
                    try p.attributeSpecifier();

                    if (param_d.qt.hasIncompleteSize(p.comp)) {
                        if (param_d.qt.is(p.comp, .void)) {
                            try p.err(param_d.name, .invalid_void_param, .{});
                        } else {
                            try p.err(param_d.name, .parameter_incomplete_ty, .{param_d.qt});
                        }
                    } else {
                        // Decay params declared as functions or arrays to pointer.
                        param_d.qt = try param_d.qt.decay(p.comp);
                    }

                    const attributed_qt = try Attribute.applyParameterAttributes(p, param_d.qt, attr_buf_top_declarator, .alignas_on_param);

                    try param_decl_spec.validateParam(p);
                    const param_node = try p.addNode(.{
                        .param = .{
                            .name_tok = param_d.name,
                            .qt = attributed_qt,
                            .storage_class = switch (param_decl_spec.storage_class) {
                                .none => .auto,
                                .register => .register,
                                else => .auto, // Error reported in `validateParam`
                            },
                        },
                    });

                    const name_str = p.tokSlice(param_d.name);
                    const interned_name = try p.comp.internString(name_str);
                    try p.syms.defineParam(p, interned_name, attributed_qt, param_d.name, param_node);

                    // find and correct parameter types
                    for (func_qt.get(p.comp, .func).?.params, new_params) |param, *new_param| {
                        if (param.name == interned_name) {
                            new_param.* = .{
                                .qt = attributed_qt,
                                .name = param.name,
                                .node = .pack(param_node),
                                .name_tok = param.name_tok,
                            };
                            break;
                        }
                    } else {
                        try p.err(param_d.name, .parameter_missing, .{name_str});
                    }

                    if (p.eatToken(.comma) == null) break;
                }
                _ = try p.expectToken(.semicolon);
            }

            const func_ty = func_qt.get(p.comp, .func).?;
            for (func_ty.params, new_params) |param, *new_param| {
                if (new_param.name == .empty) {
                    try p.err(param.name_tok, .param_not_declared, .{param.name.lookup(p.comp)});
                    new_param.* = .{
                        .name = param.name,
                        .name_tok = param.name_tok,
                        .node = param.node,
                        .qt = .int,
                    };
                }
            }
            // Update the functio type to contain the declared parameters.
            p.func.qt = try p.comp.type_store.put(gpa, .{ .func = .{
                .kind = .normal,
                .params = new_params,
                .return_type = func_ty.return_type,
            } });
        } else if (init_d.d.qt.get(p.comp, .func)) |func_ty| {
            for (func_ty.params) |param| {
                if (param.name == .empty) {
                    try p.err(param.name_tok, .omitting_parameter_name, .{});
                    continue;
                }

                // bypass redefinition check to avoid duplicate errors
                try p.syms.define(gpa, .{
                    .kind = .def,
                    .name = param.name,
                    .tok = param.name_tok,
                    .qt = param.qt,
                    .val = .{},
                    .node = param.node,
                });
                if (param.qt.isInvalid()) continue;

                if (param.qt.get(p.comp, .pointer)) |pointer_ty| {
                    if (pointer_ty.decayed) |decayed_qt| {
                        if (decayed_qt.get(p.comp, .array)) |array_ty| {
                            if (array_ty.len == .unspecified_variable) {
                                try p.err(param.name_tok, .unbound_vla, .{});
                            }
                        }
                    }
                }
                if (param.qt.hasIncompleteSize(p.comp) and !param.qt.is(p.comp, .void)) {
                    try p.err(param.name_tok, .parameter_incomplete_ty, .{param.qt});
                }
            }
        }

        const body = (try p.compoundStmt(true, null)) orelse {
            assert(init_d.d.old_style_func != null);
            try p.err(p.tok_i, .expected_fn_body, .{});
            return true;
        };

        try decl_spec.validateFnDef(p);
        try p.tree.setNode(.{ .function = .{
            .name_tok = init_d.d.name,
            .@"inline" = decl_spec.@"inline" != null,
            .static = decl_spec.storage_class == .static,
            .qt = p.func.qt.?,
            .body = body,
            .definition = null,
        } }, @intFromEnum(decl_node));

        try p.decl_buf.append(gpa, decl_node);

        // check gotos
        if (func.qt == null) {
            for (p.labels.items) |item| {
                if (item == .unresolved_goto)
                    try p.err(item.unresolved_goto, .undeclared_label, .{p.tokSlice(item.unresolved_goto)});
            }
            if (p.computed_goto_tok) |goto_tok| {
                if (!p.contains_address_of_label) try p.err(goto_tok, .invalid_computed_goto, .{});
            }
            p.labels.items.len = 0;
            p.label_count = 0;
            p.contains_address_of_label = false;
            p.computed_goto_tok = null;
        }
        return true;
    }

    // Declare all variable/typedef declarators.
    var warned_auto = false;
    while (true) {
        if (init_d.d.old_style_func) |tok_i| try p.err(tok_i, .invalid_old_style_params, .{});

        if (decl_spec.storage_class == .typedef) {
            try decl_spec.validateDecl(p);
            try p.tree.setNode(.{ .typedef = .{
                .name_tok = init_d.d.name,
                .qt = init_d.d.qt,
                .implicit = false,
            } }, @intFromEnum(decl_node));
        } else if (init_d.d.declarator_type == .func or init_d.d.qt.is(p.comp, .func)) {
            try decl_spec.validateFnDecl(p);
            try p.tree.setNode(.{ .function = .{
                .name_tok = init_d.d.name,
                .qt = init_d.d.qt,
                .static = decl_spec.storage_class == .static,
                .@"inline" = decl_spec.@"inline" != null,
                .body = null,
                .definition = null,
            } }, @intFromEnum(decl_node));
        } else {
            try decl_spec.validateDecl(p);
            var node_qt = init_d.d.qt;
            if (p.func.qt == null and decl_spec.storage_class != .@"extern") {
                if (node_qt.get(p.comp, .array)) |array_ty| {
                    if (array_ty.len == .incomplete) {
                        // Create tentative array node with fixed type.
                        node_qt = try p.comp.type_store.put(gpa, .{ .array = .{
                            .elem = array_ty.elem,
                            .len = .{ .fixed = 1 },
                        } });
                    }
                }
            }

            try p.tree.setNode(.{
                .variable = .{
                    .name_tok = init_d.d.name,
                    .qt = node_qt,
                    .thread_local = decl_spec.thread_local != null,
                    .implicit = false,
                    .storage_class = switch (decl_spec.storage_class) {
                        .auto => .auto,
                        .register => .register,
                        .static => .static,
                        .@"extern" => if (init_d.initializer == null) .@"extern" else .auto,
                        else => .auto, // Error reported in `validate`
                    },
                    .initializer = if (init_d.initializer) |some| some.node else null,
                    .definition = null,
                },
            }, @intFromEnum(decl_node));
        }
        try p.decl_buf.append(gpa, decl_node);

        const interned_name = try p.comp.internString(p.tokSlice(init_d.d.name));
        if (decl_spec.storage_class == .typedef) {
            const typedef_qt = if (init_d.d.qt.isInvalid())
                init_d.d.qt
            else
                (try p.comp.type_store.put(gpa, .{ .typedef = .{
                    .base = init_d.d.qt,
                    .name = interned_name,
                    .decl_node = decl_node,
                } })).withQualifiers(init_d.d.qt);
            try p.syms.defineTypedef(p, interned_name, typedef_qt, init_d.d.name, decl_node);
            p.typedefDefined(interned_name, typedef_qt);
        } else if (init_d.initializer) |init| {
            // TODO validate global variable/constexpr initializer comptime known
            try p.syms.defineSymbol(
                p,
                interned_name,
                init_d.d.qt,
                init_d.d.name,
                decl_node,
                if (init_d.d.qt.@"const" or decl_spec.constexpr != null) init.val else .{},
                decl_spec.constexpr != null,
            );
        } else if (init_d.d.qt.is(p.comp, .func)) {
            try p.syms.declareSymbol(p, interned_name, init_d.d.qt, init_d.d.name, decl_node);
        } else if (p.func.qt != null and decl_spec.storage_class != .@"extern") {
            try p.syms.defineSymbol(p, interned_name, init_d.d.qt, init_d.d.name, decl_node, .{}, false);
        } else {
            try p.syms.declareSymbol(p, interned_name, init_d.d.qt, init_d.d.name, decl_node);
        }

        if (p.eatToken(.comma) == null) break;

        const attr_buf_top_declarator = p.attr_buf.len;
        defer p.attr_buf.len = attr_buf_top_declarator;

        try p.attributeSpecifierGnu();

        if (!warned_auto) {
            // TODO these are warnings in clang
            if (decl_spec.auto_type) |tok_i| {
                try p.err(tok_i, .auto_type_requires_single_declarator, .{});
                warned_auto = true;
            }
            if (decl_spec.c23_auto) |tok_i| {
                try p.err(tok_i, .c23_auto_single_declarator, .{});
                warned_auto = true;
            }
        }

        decl_node = try p.tree.addNode(.{ .empty_decl = .{
            .semicolon = p.tok_i - 1,
        } });
        init_d = (try p.initDeclarator(&decl_spec, attr_buf_top, decl_node)) orelse {
            try p.err(p.tok_i, .expected_ident_or_l_paren, .{});
            continue;
        };
    }

    _ = try p.expectToken(.semicolon);
    return true;
}

fn staticAssertMessage(p: *Parser, cond_node: Node.Index, maybe_message: ?Result, allocating: *Io.Writer.Allocating) !?[]const u8 {
    const w = &allocating.writer;

    const cond = cond_node.get(&p.tree);
    if (cond == .builtin_types_compatible_p) {
        try w.writeAll("'__builtin_types_compatible_p(");

        const lhs_ty = cond.builtin_types_compatible_p.lhs;
        try lhs_ty.print(p.comp, w);
        try w.writeAll(", ");

        const rhs_ty = cond.builtin_types_compatible_p.rhs;
        try rhs_ty.print(p.comp, w);

        try w.writeAll(")'");
    } else if (maybe_message == null) return null;

    if (maybe_message) |message| {
        assert(message.node.get(&p.tree) == .string_literal_expr);
        if (allocating.written().len > 0) {
            try w.writeByte(' ');
        }
        const bytes = p.comp.interner.get(message.val.ref()).bytes;
        try Value.printString(bytes, message.qt, p.comp, w);
    }
    return allocating.written();
}

/// staticAssert
///    : keyword_static_assert '(' integerConstExpr (',' STRING_LITERAL)? ')' ';'
///    | keyword_c23_static_assert '(' integerConstExpr (',' STRING_LITERAL)? ')' ';'
fn staticAssert(p: *Parser) Error!bool {
    const gpa = p.comp.gpa;
    const static_assert = p.eatToken(.keyword_static_assert) orelse p.eatToken(.keyword_c23_static_assert) orelse return false;
    const l_paren = try p.expectToken(.l_paren);
    const res_token = p.tok_i;
    var res = try p.constExpr(.gnu_folding_extension);
    const res_node = res.node;
    const str = if (p.eatToken(.comma) != null)
        switch (p.tok_ids[p.tok_i]) {
            .string_literal,
            .string_literal_utf_16,
            .string_literal_utf_8,
            .string_literal_utf_32,
            .string_literal_wide,
            .unterminated_string_literal,
            => try p.stringLiteral(),
            else => {
                try p.err(p.tok_i, .expected_str_literal, .{});
                return error.ParsingFailed;
            },
        }
    else
        null;
    try p.expectClosing(l_paren, .r_paren);
    _ = try p.expectToken(.semicolon);
    if (str == null) {
        try p.err(static_assert, .static_assert_missing_message, .{});
        try p.err(static_assert, .pre_c23_compat, .{"'_Static_assert' with no message"});
    }

    const is_int_expr = res.qt.isInvalid() or res.qt.isInt(p.comp);
    try res.castToBool(p, .bool, res_token);
    if (!is_int_expr) {
        res.val = .{};
    }
    if (res.val.opt_ref == .none) {
        if (!res.qt.isInvalid()) {
            try p.err(res_token, .static_assert_not_constant, .{});
        }
    } else {
        if (!res.val.toBool(p.comp)) {
            var sf = std.heap.stackFallback(1024, gpa);
            var allocating: Io.Writer.Allocating = .init(sf.get());
            defer allocating.deinit();

            if (p.staticAssertMessage(res_node, str, &allocating) catch return error.OutOfMemory) |message| {
                try p.err(static_assert, .static_assert_failure_message, .{message});
            } else {
                try p.err(static_assert, .static_assert_failure, .{});
            }
        }
    }

    const node = try p.addNode(.{
        .static_assert = .{
            .assert_tok = static_assert,
            .cond = res.node,
            .message = if (str) |some| some.node else null,
        },
    });
    try p.decl_buf.append(gpa, node);
    return true;
}

pub const DeclSpec = struct {
    storage_class: union(enum) {
        auto: TokenIndex,
        @"extern": TokenIndex,
        register: TokenIndex,
        static: TokenIndex,
        typedef: TokenIndex,
        none,
    } = .none,
    thread_local: ?TokenIndex = null,
    constexpr: ?TokenIndex = null,
    @"inline": ?TokenIndex = null,
    noreturn: ?TokenIndex = null,
    auto_type: ?TokenIndex = null,
    c23_auto: ?TokenIndex = null,
    qt: QualType,

    fn validateParam(d: DeclSpec, p: *Parser) Error!void {
        switch (d.storage_class) {
            .none, .register => {},
            .auto, .@"extern", .static, .typedef => |tok_i| try p.err(tok_i, .invalid_storage_on_param, .{}),
        }
        if (d.thread_local) |tok_i| try p.err(tok_i, .threadlocal_non_var, .{});
        if (d.@"inline") |tok_i| try p.err(tok_i, .func_spec_non_func, .{"inline"});
        if (d.noreturn) |tok_i| try p.err(tok_i, .func_spec_non_func, .{"_Noreturn"});
        if (d.constexpr) |tok_i| try p.err(tok_i, .invalid_storage_on_param, .{});
    }

    fn validateFnDef(d: DeclSpec, p: *Parser) Error!void {
        switch (d.storage_class) {
            .none, .@"extern", .static => {},
            .auto, .register, .typedef => |tok_i| try p.err(tok_i, .illegal_storage_on_func, .{}),
        }
        if (d.thread_local) |tok_i| try p.err(tok_i, .threadlocal_non_var, .{});
        if (d.constexpr) |tok_i| try p.err(tok_i, .illegal_storage_on_func, .{});
    }

    fn validateFnDecl(d: DeclSpec, p: *Parser) Error!void {
        switch (d.storage_class) {
            .none, .@"extern" => {},
            .static => |tok_i| if (p.func.qt != null) try p.err(tok_i, .static_func_not_global, .{}),
            .typedef => unreachable,
            .auto, .register => |tok_i| try p.err(tok_i, .illegal_storage_on_func, .{}),
        }
        if (d.thread_local) |tok_i| try p.err(tok_i, .threadlocal_non_var, .{});
        if (d.constexpr) |tok_i| try p.err(tok_i, .illegal_storage_on_func, .{});
    }

    fn validateDecl(d: DeclSpec, p: *Parser) Error!void {
        if (d.@"inline") |tok_i| try p.err(tok_i, .func_spec_non_func, .{"inline"});
        // TODO move to attribute validation
        if (d.noreturn) |tok_i| try p.err(tok_i, .func_spec_non_func, .{"_Noreturn"});
        switch (d.storage_class) {
            .auto => std.debug.assert(!p.comp.langopts.standard.atLeast(.c23)),
            .register => if (p.func.qt == null) try p.err(p.tok_i, .illegal_storage_on_global, .{}),
            else => {},
        }
    }

    fn initContext(d: DeclSpec, p: *Parser) InitContext {
        if (d.constexpr != null) return .constexpr;
        if (p.func.qt == null or d.storage_class == .static) return .static;
        return .runtime;
    }
};

/// typeof
///   : keyword_typeof '(' typeName ')'
///   | keyword_typeof '(' expr ')'
fn typeof(p: *Parser) Error!?QualType {
    const gpa = p.comp.gpa;
    var unqual = false;
    switch (p.tok_ids[p.tok_i]) {
        .keyword_typeof, .keyword_typeof1, .keyword_typeof2 => p.tok_i += 1,
        .keyword_typeof_unqual => {
            p.tok_i += 1;
            unqual = true;
        },
        else => return null,
    }
    const l_paren = try p.expectToken(.l_paren);
    if (try p.typeName()) |qt| {
        try p.expectClosing(l_paren, .r_paren);
        if (qt.isInvalid()) return null;

        return (try p.comp.type_store.put(gpa, .{ .typeof = .{
            .base = qt,
            .expr = null,
        } })).withQualifiers(qt);
    }
    const typeof_expr = try p.parseNoEval(expr);
    try p.expectClosing(l_paren, .r_paren);
    if (typeof_expr.qt.isInvalid()) return null;

    const typeof_qt = try p.comp.type_store.put(gpa, .{ .typeof = .{
        .base = typeof_expr.qt,
        .expr = typeof_expr.node,
    } });
    if (unqual) return typeof_qt;
    return typeof_qt.withQualifiers(typeof_expr.qt);
}

/// declSpec: (storageClassSpec | typeSpec | funcSpec | autoTypeSpec)+
/// funcSpec : keyword_inline | keyword_noreturn
/// autoTypeSpec : keyword_auto_type
fn declSpec(p: *Parser) Error!?DeclSpec {
    var d: DeclSpec = .{ .qt = .invalid };
    var builder: TypeStore.Builder = .{ .parser = p };

    const start = p.tok_i;
    while (true) {
        const id = p.tok_ids[p.tok_i];
        switch (id) {
            .keyword_inline, .keyword_inline1, .keyword_inline2 => {
                if (d.@"inline" != null) {
                    try p.err(p.tok_i, .duplicate_decl_spec, .{"inline"});
                }
                d.@"inline" = p.tok_i;
                p.tok_i += 1;
                continue;
            },
            .keyword_noreturn => {
                if (d.noreturn != null) {
                    try p.err(p.tok_i, .duplicate_decl_spec, .{"_Noreturn"});
                }
                d.noreturn = p.tok_i;
                p.tok_i += 1;
                continue;
            },
            .keyword_auto_type => {
                try p.err(p.tok_i, .auto_type_extension, .{});
                try builder.combine(.auto_type, p.tok_i);
                if (builder.type == .auto_type) d.auto_type = p.tok_i;
                p.tok_i += 1;
                continue;
            },
            .keyword_auto => if (p.comp.langopts.standard.atLeast(.c23)) {
                try builder.combine(.c23_auto, p.tok_i);
                if (builder.type == .c23_auto) d.c23_auto = p.tok_i;
                p.tok_i += 1;
                continue;
            },
            .keyword_forceinline, .keyword_forceinline2 => {
                try p.attr_buf.append(p.comp.gpa, .{
                    .attr = .{ .tag = .always_inline, .args = .{ .always_inline = .{} }, .syntax = .keyword },
                    .tok = p.tok_i,
                });
                p.tok_i += 1;
                continue;
            },
            else => {},
        }

        if (try p.storageClassSpec(&d)) continue;
        if (try p.typeSpec(&builder)) continue;
        if (p.tok_i == start) return null;

        d.qt = try builder.finish();
        return d;
    }
}

/// storageClassSpec:
///  : keyword_typedef
///  | keyword_extern
///  | keyword_static
///  | keyword_threadlocal
///  | keyword_auto
///  | keyword_register
fn storageClassSpec(p: *Parser, d: *DeclSpec) Error!bool {
    const start = p.tok_i;
    while (true) {
        const id = p.tok_ids[p.tok_i];
        switch (id) {
            .keyword_typedef,
            .keyword_extern,
            .keyword_static,
            .keyword_auto,
            .keyword_register,
            => {
                if (d.storage_class != .none) {
                    try p.err(p.tok_i, .multiple_storage_class, .{@tagName(d.storage_class)});
                    return error.ParsingFailed;
                }
                if (d.thread_local != null) {
                    switch (id) {
                        .keyword_extern, .keyword_static => {},
                        else => try p.err(p.tok_i, .cannot_combine_spec, .{id.lexeme().?}),
                    }
                    if (d.constexpr) |tok| try p.err(p.tok_i, .cannot_combine_spec, .{p.tok_ids[tok].lexeme().?});
                }
                if (d.constexpr != null) {
                    switch (id) {
                        .keyword_auto, .keyword_register, .keyword_static => {},
                        else => try p.err(p.tok_i, .cannot_combine_spec, .{id.lexeme().?}),
                    }
                    if (d.thread_local) |tok| try p.err(p.tok_i, .cannot_combine_spec, .{p.tok_ids[tok].lexeme().?});
                }
                switch (id) {
                    .keyword_typedef => d.storage_class = .{ .typedef = p.tok_i },
                    .keyword_extern => d.storage_class = .{ .@"extern" = p.tok_i },
                    .keyword_static => d.storage_class = .{ .static = p.tok_i },
                    .keyword_auto => d.storage_class = .{ .auto = p.tok_i },
                    .keyword_register => d.storage_class = .{ .register = p.tok_i },
                    else => unreachable,
                }
            },
            .keyword_thread_local,
            .keyword_c23_thread_local,
            => {
                if (d.thread_local != null) {
                    try p.err(p.tok_i, .duplicate_decl_spec, .{id.lexeme().?});
                }
                if (d.constexpr) |tok| try p.err(p.tok_i, .cannot_combine_spec, .{p.tok_ids[tok].lexeme().?});
                switch (d.storage_class) {
                    .@"extern", .none, .static => {},
                    else => try p.err(p.tok_i, .cannot_combine_spec, .{@tagName(d.storage_class)}),
                }
                d.thread_local = p.tok_i;
            },
            .keyword_constexpr => {
                if (d.constexpr != null) {
                    try p.err(p.tok_i, .duplicate_decl_spec, .{id.lexeme().?});
                }
                if (d.thread_local) |tok| try p.err(p.tok_i, .cannot_combine_spec, .{p.tok_ids[tok].lexeme().?});
                switch (d.storage_class) {
                    .auto, .register, .none, .static => {},
                    else => try p.err(p.tok_i, .cannot_combine_spec, .{@tagName(d.storage_class)}),
                }
                d.constexpr = p.tok_i;
            },
            else => break,
        }
        p.tok_i += 1;
    }
    return p.tok_i != start;
}

const InitDeclarator = struct { d: Declarator, initializer: ?Result = null };

/// attribute
///  : attrIdentifier
///  | attrIdentifier '(' identifier ')'
///  | attrIdentifier '(' identifier (',' expr)+ ')'
///  | attrIdentifier '(' (expr (',' expr)*)? ')'
fn attribute(p: *Parser, kind: Attribute.Kind, namespace: ?[]const u8) Error!?TentativeAttribute {
    const name_tok = p.tok_i;
    if (!p.tok_ids[p.tok_i].isMacroIdentifier()) {
        return p.errExpectedToken(.identifier, p.tok_ids[p.tok_i]);
    }
    _ = (try p.eatIdentifier()) orelse {
        p.tok_i += 1;
    };
    const name = p.tokSlice(name_tok);

    const attr = Attribute.fromString(kind, namespace, name) orelse {
        try p.err(name_tok, if (kind == .declspec) .declspec_attr_not_supported else .unknown_attribute, .{name});
        if (p.eatToken(.l_paren)) |_| p.skipTo(.r_paren);
        return null;
    };
    if (attr == .availability) {
        // TODO parse introduced=10.4 etc
        if (p.eatToken(.l_paren)) |_| p.skipTo(.r_paren);
        return null;
    }

    const required_count = Attribute.requiredArgCount(attr);
    var arguments = Attribute.initArguments(attr, name_tok);
    var arg_idx: u32 = 0;

    switch (p.tok_ids[p.tok_i]) {
        .comma, .r_paren => {}, // will be consumed in attributeList
        .l_paren => blk: {
            p.tok_i += 1;
            if (p.eatToken(.r_paren)) |_| break :blk;

            if (Attribute.wantsIdentEnum(attr)) {
                if (try p.eatIdentifier()) |ident| {
                    if (try Attribute.diagnoseIdent(attr, &arguments, ident, p)) {
                        p.skipTo(.r_paren);
                        return error.ParsingFailed;
                    }
                } else {
                    try p.err(name_tok, .attribute_requires_identifier, .{name});
                    return error.ParsingFailed;
                }
            } else {
                const arg_start = p.tok_i;
                const first_expr = try p.expect(assignExpr);
                if (try p.diagnose(attr, &arguments, arg_idx, first_expr, arg_start)) {
                    p.skipTo(.r_paren);
                    return error.ParsingFailed;
                }
            }
            arg_idx += 1;
            while (p.eatToken(.r_paren) == null) : (arg_idx += 1) {
                _ = try p.expectToken(.comma);

                const arg_start = p.tok_i;
                const arg_expr = try p.expect(assignExpr);
                if (try p.diagnose(attr, &arguments, arg_idx, arg_expr, arg_start)) {
                    p.skipTo(.r_paren);
                    return error.ParsingFailed;
                }
            }
        },
        else => {},
    }
    if (arg_idx < required_count) {
        try p.err(name_tok, .attribute_not_enough_args, .{
            @tagName(attr), required_count,
        });
        return error.ParsingFailed;
    }
    return TentativeAttribute{ .attr = .{ .tag = attr, .args = arguments, .syntax = kind.toSyntax() }, .tok = name_tok };
}

fn diagnose(p: *Parser, attr: Attribute.Tag, arguments: *Attribute.Arguments, arg_idx: u32, res: Result, arg_start: TokenIndex) !bool {
    if (Attribute.wantsAlignment(attr, arg_idx)) {
        return Attribute.diagnoseAlignment(attr, arguments, arg_idx, res, arg_start, p);
    }
    return Attribute.diagnose(attr, arguments, arg_idx, res, arg_start, res.node.get(&p.tree), p);
}

/// attributeList : (attribute (',' attribute)*)?
fn gnuAttributeList(p: *Parser) Error!void {
    if (p.tok_ids[p.tok_i] == .r_paren) return;
    const gpa = p.comp.gpa;

    if (try p.attribute(.gnu, null)) |attr| try p.attr_buf.append(gpa, attr);
    while (p.tok_ids[p.tok_i] != .r_paren) {
        _ = try p.expectToken(.comma);
        if (try p.attribute(.gnu, null)) |attr| try p.attr_buf.append(gpa, attr);
    }
}

fn c23AttributeList(p: *Parser) Error!void {
    while (p.tok_ids[p.tok_i] != .r_bracket) {
        const namespace_tok = try p.expectIdentifier();
        var namespace: ?[]const u8 = null;
        if (p.eatToken(.colon_colon)) |_| {
            namespace = p.tokSlice(namespace_tok);
        } else {
            p.tok_i -= 1;
        }
        if (try p.attribute(.c23, namespace)) |attr| try p.attr_buf.append(p.comp.gpa, attr);
        _ = p.eatToken(.comma);
    }
}

fn msvcAttributeList(p: *Parser) Error!void {
    while (p.tok_ids[p.tok_i] != .r_paren) {
        if (try p.attribute(.declspec, null)) |attr| try p.attr_buf.append(p.comp.gpa, attr);
        _ = p.eatToken(.comma);
    }
}

fn c23Attribute(p: *Parser) !bool {
    if (!p.comp.langopts.standard.atLeast(.c23)) return false;
    const bracket1 = p.eatToken(.l_bracket) orelse return false;
    const bracket2 = p.eatToken(.l_bracket) orelse {
        p.tok_i -= 1;
        return false;
    };

    try p.c23AttributeList();

    _ = try p.expectClosing(bracket2, .r_bracket);
    _ = try p.expectClosing(bracket1, .r_bracket);

    return true;
}

fn msvcAttribute(p: *Parser) !bool {
    _ = p.eatToken(.keyword_declspec) orelse return false;
    const l_paren = try p.expectToken(.l_paren);
    try p.msvcAttributeList();
    _ = try p.expectClosing(l_paren, .r_paren);

    return true;
}

fn gnuAttribute(p: *Parser) !bool {
    switch (p.tok_ids[p.tok_i]) {
        .keyword_attribute1, .keyword_attribute2 => p.tok_i += 1,
        else => return false,
    }
    const paren1 = try p.expectToken(.l_paren);
    const paren2 = try p.expectToken(.l_paren);

    try p.gnuAttributeList();

    _ = try p.expectClosing(paren2, .r_paren);
    _ = try p.expectClosing(paren1, .r_paren);
    return true;
}

fn attributeSpecifierGnu(p: *Parser) Error!void {
    while (true) {
        if (try p.gnuAttribute()) continue;

        const tok = p.tok_i;
        const attr_buf_top_declarator = p.attr_buf.len;
        defer p.attr_buf.len = attr_buf_top_declarator;

        if (try p.c23Attribute()) {
            try p.err(tok, .invalid_attribute_location, .{"an attribute list"});
            continue;
        }
        if (try p.msvcAttribute()) {
            try p.err(tok, .invalid_attribute_location, .{"a declspec attribute"});
            continue;
        }
        break;
    }
}

fn attributeSpecifier(p: *Parser) Error!void {
    return attributeSpecifierExtra(p, null);
}

/// attributeSpecifier : (keyword_attribute '( '(' attributeList ')' ')')*
fn attributeSpecifierExtra(p: *Parser, declarator_name: ?TokenIndex) Error!void {
    while (true) {
        if (try p.gnuAttribute()) continue;
        if (try p.c23Attribute()) continue;
        const maybe_declspec_tok = p.tok_i;
        const attr_buf_top = p.attr_buf.len;
        if (try p.msvcAttribute()) {
            if (declarator_name) |name_tok| {
                try p.err(maybe_declspec_tok, .declspec_not_allowed_after_declarator, .{});
                try p.err(name_tok, .declarator_name_tok, .{});
                p.attr_buf.len = attr_buf_top;
            }
            continue;
        }
        break;
    }
}

/// initDeclarator : declarator assembly? attributeSpecifier? ('=' initializer)?
fn initDeclarator(p: *Parser, decl_spec: *DeclSpec, attr_buf_top: usize, decl_node: Node.Index) Error!?InitDeclarator {
    const this_attr_buf_top = p.attr_buf.len;
    defer p.attr_buf.len = this_attr_buf_top;
    const gpa = p.comp.gpa;

    var init_d = InitDeclarator{
        .d = (try p.declarator(decl_spec.qt, .normal)) orelse return null,
    };

    try p.attributeSpecifierExtra(init_d.d.name);
    _ = try p.assembly(.decl_label);
    try p.attributeSpecifierExtra(init_d.d.name);

    switch (init_d.d.declarator_type) {
        .func => {
            if (decl_spec.auto_type) |tok_i| {
                try p.err(tok_i, .auto_type_not_allowed, .{"function return type"});
                init_d.d.qt = .invalid;
            } else if (decl_spec.c23_auto) |tok_i| {
                try p.err(tok_i, .c23_auto_not_allowed, .{"function return type"});
                init_d.d.qt = .invalid;
            }
        },
        .array => {
            if (decl_spec.auto_type) |tok_i| {
                try p.err(tok_i, .auto_type_array, .{p.tokSlice(init_d.d.name)});
                init_d.d.qt = .invalid;
            } else if (decl_spec.c23_auto) |tok_i| {
                try p.err(tok_i, .c23_auto_array, .{p.tokSlice(init_d.d.name)});
                init_d.d.qt = .invalid;
            }
        },
        .pointer => {
            if (decl_spec.auto_type != null or decl_spec.c23_auto != null) {
                // TODO this is not a hard error in clang
                try p.err(p.tok_i, .auto_type_requires_plain_declarator, .{});
                init_d.d.qt = .invalid;
            }
        },
        .other => if (decl_spec.storage_class == .typedef) {
            if (decl_spec.auto_type) |tok_i| {
                try p.err(tok_i, .auto_type_not_allowed, .{"typedef"});
                init_d.d.qt = .invalid;
            } else if (decl_spec.c23_auto) |tok_i| {
                try p.err(tok_i, .c23_auto_not_allowed, .{"typedef"});
                init_d.d.qt = .invalid;
            }
        },
    }

    var apply_var_attributes = false;
    if (decl_spec.storage_class == .typedef) {
        init_d.d.qt = try Attribute.applyTypeAttributes(p, init_d.d.qt, attr_buf_top, null);
    } else if (init_d.d.declarator_type == .func or init_d.d.qt.is(p.comp, .func)) {
        init_d.d.qt = try Attribute.applyFunctionAttributes(p, init_d.d.qt, attr_buf_top);
    } else {
        apply_var_attributes = true;
    }

    if (p.eatToken(.equal)) |eq| {
        if (decl_spec.storage_class == .typedef or
            (init_d.d.declarator_type == .func and init_d.d.qt.is(p.comp, .func)))
        {
            try p.err(eq, .illegal_initializer, .{});
        } else if (init_d.d.qt.get(p.comp, .array)) |array_ty| {
            if (array_ty.len == .variable) try p.err(eq, .vla_init, .{});
        } else if (decl_spec.storage_class == .@"extern") {
            try p.err(p.tok_i, .extern_initializer, .{});
            decl_spec.storage_class = .none;
        }

        incomplete: {
            if (init_d.d.qt.isInvalid()) break :incomplete;
            if (init_d.d.qt.isC23Auto()) break :incomplete;
            if (init_d.d.qt.isAutoType()) break :incomplete;
            if (!init_d.d.qt.hasIncompleteSize(p.comp)) break :incomplete;
            if (init_d.d.qt.get(p.comp, .array)) |array_ty| {
                if (array_ty.len == .incomplete) break :incomplete;
            }
            try p.err(init_d.d.name, .variable_incomplete_ty, .{init_d.d.qt});
            init_d.d.qt = .invalid;
        }

        try p.syms.pushScope(p);
        defer p.syms.popScope();

        const interned_name = try p.comp.internString(p.tokSlice(init_d.d.name));
        try p.syms.declareSymbol(p, interned_name, init_d.d.qt, init_d.d.name, decl_node);

        // TODO this should be a stack of auto type names because of statement expressions.
        if (init_d.d.qt.isAutoType() or init_d.d.qt.isC23Auto()) {
            p.auto_type_decl_name = interned_name;
        }
        defer p.auto_type_decl_name = .empty;

        const init_context = p.init_context;
        defer p.init_context = init_context;
        p.init_context = decl_spec.initContext(p);
        var init_list_expr = try p.initializer(init_d.d.qt);
        init_d.initializer = init_list_expr;

        // Set incomplete array length if possible.
        if (init_d.d.qt.get(p.comp, .array)) |base_array_ty| {
            if (base_array_ty.len == .incomplete) if (init_list_expr.qt.get(p.comp, .array)) |init_array_ty| {
                switch (init_array_ty.len) {
                    .fixed, .static => |len| {
                        init_d.d.qt = (try p.comp.type_store.put(gpa, .{ .array = .{
                            .elem = base_array_ty.elem,
                            .len = .{ .fixed = len },
                        } })).withQualifiers(init_d.d.qt);
                    },
                    else => {},
                }
            };
        }
    }

    const name = init_d.d.name;
    if (init_d.d.qt.isAutoType() or init_d.d.qt.isC23Auto()) {
        if (init_d.initializer) |some| {
            init_d.d.qt = some.qt.withQualifiers(init_d.d.qt);
        } else {
            if (init_d.d.qt.isC23Auto()) {
                try p.err(name, .c23_auto_requires_initializer, .{});
            } else {
                try p.err(name, .auto_type_requires_initializer, .{p.tokSlice(name)});
            }
            init_d.d.qt = .invalid;
            return init_d;
        }
    }
    if (apply_var_attributes) {
        init_d.d.qt = try Attribute.applyVariableAttributes(p, init_d.d.qt, attr_buf_top, null);
    }

    incomplete: {
        if (decl_spec.storage_class == .typedef) break :incomplete;
        if (init_d.d.qt.isInvalid()) break :incomplete;
        if (!init_d.d.qt.hasIncompleteSize(p.comp)) break :incomplete;

        const init_type = init_d.d.qt.base(p.comp).type;
        if (decl_spec.storage_class == .@"extern") switch (init_type) {
            .@"struct", .@"union", .@"enum" => break :incomplete,
            .array => |array_ty| if (array_ty.len == .incomplete) break :incomplete,
            else => {},
        };
        // if there was an initializer expression it must have contained an error
        if (init_d.initializer != null) break :incomplete;

        if (p.func.qt == null) {
            switch (init_type) {
                .array => |array_ty| if (array_ty.len == .incomplete) {
                    // TODO properly check this after finishing parsing
                    try p.err(name, .tentative_array, .{});
                    break :incomplete;
                },
                .@"struct", .@"union" => |record_ty| {
                    _ = try p.tentative_defs.getOrPutValue(gpa, record_ty.name, init_d.d.name);
                    break :incomplete;
                },
                .@"enum" => |enum_ty| {
                    _ = try p.tentative_defs.getOrPutValue(gpa, enum_ty.name, init_d.d.name);
                    break :incomplete;
                },
                else => {},
            }
        }
        try p.err(name, .variable_incomplete_ty, .{init_d.d.qt});
        init_d.d.qt = .invalid;
    }
    return init_d;
}

/// typeSpec
///  : keyword_void
///  | keyword_char
///  | keyword_short
///  | keyword_int
///  | keyword_long
///  | keyword_float
///  | keyword_double
///  | keyword_signed
///  | keyword_signed1
///  | keyword_signed2
///  | keyword_unsigned
///  | keyword_bool
///  | keyword_c23_bool
///  | keyword_complex
///  | keyword_atomic '(' typeName ')'
///  | recordSpec
///  | enumSpec
///  | typedef  // IDENTIFIER
///  | typeof
///  | keyword_bit_int '(' integerConstExpr ')'
///  | typeQual
///  | keyword_alignas '(' typeName ')'
///  | keyword_alignas '(' integerConstExpr ')'
///  | keyword_c23_alignas '(' typeName ')'
///  | keyword_c23_alignas '(' integerConstExpr ')'
fn typeSpec(p: *Parser, builder: *TypeStore.Builder) Error!bool {
    const start = p.tok_i;
    while (true) {
        try p.attributeSpecifier();

        if (try p.typeof()) |typeof_qt| {
            try builder.combineFromTypeof(typeof_qt, start);
            continue;
        }
        if (try p.typeQual(builder, true)) continue;
        switch (p.tok_ids[p.tok_i]) {
            .keyword_void => try builder.combine(.void, p.tok_i),
            .keyword_bool, .keyword_c23_bool => try builder.combine(.bool, p.tok_i),
            .keyword_int8, .keyword_int8_2, .keyword_char => try builder.combine(.char, p.tok_i),
            .keyword_int16, .keyword_int16_2, .keyword_short => try builder.combine(.short, p.tok_i),
            .keyword_int32, .keyword_int32_2, .keyword_int => try builder.combine(.int, p.tok_i),
            .keyword_long => try builder.combine(.long, p.tok_i),
            .keyword_int64, .keyword_int64_2 => try builder.combine(.long_long, p.tok_i),
            .keyword_int128 => try builder.combine(.int128, p.tok_i),
            .keyword_signed, .keyword_signed1, .keyword_signed2 => try builder.combine(.signed, p.tok_i),
            .keyword_unsigned => try builder.combine(.unsigned, p.tok_i),
            .keyword_fp16 => try builder.combine(.fp16, p.tok_i),
            .keyword_bf16 => try builder.combine(.bf16, p.tok_i),
            .keyword_float16 => try builder.combine(.float16, p.tok_i),
            .keyword_float32 => try builder.combine(.float32, p.tok_i),
            .keyword_float64 => try builder.combine(.float64, p.tok_i),
            .keyword_float32x => try builder.combine(.float32x, p.tok_i),
            .keyword_float64x => try builder.combine(.float64x, p.tok_i),
            .keyword_float128x => {
                try p.err(p.tok_i, .type_not_supported_on_target, .{p.tok_ids[p.tok_i].lexeme().?});
                return error.ParsingFailed;
            },
            .keyword_dfloat32 => try builder.combine(.dfloat32, p.tok_i),
            .keyword_dfloat64 => try builder.combine(.dfloat64, p.tok_i),
            .keyword_dfloat128 => try builder.combine(.dfloat128, p.tok_i),
            .keyword_dfloat64x => try builder.combine(.dfloat64x, p.tok_i),
            .keyword_float => try builder.combine(.float, p.tok_i),
            .keyword_double => try builder.combine(.double, p.tok_i),
            .keyword_complex => try builder.combine(.complex, p.tok_i),
            .keyword_float128, .keyword_float128_1 => {
                if (!p.comp.hasFloat128()) {
                    try p.err(p.tok_i, .type_not_supported_on_target, .{p.tok_ids[p.tok_i].lexeme().?});
                }
                try builder.combine(.float128, p.tok_i);
            },
            .keyword_atomic => {
                const atomic_tok = p.tok_i;
                p.tok_i += 1;
                const l_paren = p.eatToken(.l_paren) orelse {
                    // _Atomic qualifier not _Atomic(typeName)
                    p.tok_i = atomic_tok;
                    break;
                };
                const base_qt = (try p.typeName()) orelse {
                    try p.err(p.tok_i, .expected_type, .{});
                    return error.ParsingFailed;
                };
                try p.expectClosing(l_paren, .r_paren);

                if (base_qt.isQualified() and !base_qt.isInvalid()) {
                    try p.err(atomic_tok, .atomic_qualified, .{base_qt});
                    builder.type = .{ .other = .invalid };
                    continue;
                }

                try builder.combineAtomic(base_qt, atomic_tok);
                continue;
            },
            .keyword_alignas,
            .keyword_c23_alignas,
            => {
                const align_tok = p.tok_i;
                p.tok_i += 1;
                const gpa = p.comp.gpa;
                const l_paren = try p.expectToken(.l_paren);
                const typename_start = p.tok_i;
                if (try p.typeName()) |inner_qt| {
                    if (!inner_qt.alignable(p.comp)) {
                        try p.err(typename_start, .invalid_alignof, .{inner_qt});
                    }
                    const alignment = Attribute.Alignment{ .requested = inner_qt.alignof(p.comp) };
                    try p.attr_buf.append(gpa, .{
                        .attr = .{ .tag = .aligned, .args = .{
                            .aligned = .{ .alignment = alignment, .__name_tok = align_tok },
                        }, .syntax = .keyword },
                        .tok = align_tok,
                    });
                } else {
                    const arg_start = p.tok_i;
                    const res = try p.integerConstExpr(.no_const_decl_folding);
                    if (!res.val.isZero(p.comp)) {
                        var args = Attribute.initArguments(.aligned, align_tok);
                        if (try p.diagnose(.aligned, &args, 0, res, arg_start)) {
                            p.skipTo(.r_paren);
                            return error.ParsingFailed;
                        }
                        args.aligned.alignment.?.node = .pack(res.node);
                        try p.attr_buf.append(gpa, .{
                            .attr = .{ .tag = .aligned, .args = args, .syntax = .keyword },
                            .tok = align_tok,
                        });
                    }
                }
                try p.expectClosing(l_paren, .r_paren);
                continue;
            },
            .keyword_struct, .keyword_union => {
                const tag_tok = p.tok_i;
                const record_ty = try p.recordSpec();
                try builder.combine(.{ .other = record_ty }, tag_tok);
                continue;
            },
            .keyword_enum => {
                const tag_tok = p.tok_i;
                const enum_ty = try p.enumSpec();
                try builder.combine(.{ .other = enum_ty }, tag_tok);
                continue;
            },
            .identifier, .extended_identifier => {
                var interned_name = try p.comp.internString(p.tokSlice(p.tok_i));
                var declspec_found = false;

                if (interned_name == p.string_ids.declspec_id) {
                    try p.err(p.tok_i, .declspec_not_enabled, .{});
                    p.tok_i += 1;
                    if (p.eatToken(.l_paren)) |_| {
                        p.skipTo(.r_paren);
                        continue;
                    }
                    declspec_found = true;
                }
                if (declspec_found) {
                    interned_name = try p.comp.internString(p.tokSlice(p.tok_i));
                }
                const typedef = (try p.syms.findTypedef(p, interned_name, p.tok_i, builder.type != .none)) orelse break;
                if (!builder.combineTypedef(typedef.qt)) break;
            },
            .keyword_bit_int => {
                try p.err(p.tok_i, .bit_int, .{});
                const bit_int_tok = p.tok_i;
                p.tok_i += 1;
                const l_paren = try p.expectToken(.l_paren);
                const res = try p.integerConstExpr(.gnu_folding_extension);
                try p.expectClosing(l_paren, .r_paren);

                var bits: u64 = undefined;
                if (res.val.opt_ref == .none) {
                    try p.err(bit_int_tok, .expected_integer_constant_expr, .{});
                    return error.ParsingFailed;
                } else if (res.val.compare(.lte, .zero, p.comp)) {
                    bits = 0;
                } else {
                    bits = res.val.toInt(u64, p.comp) orelse std.math.maxInt(u64);
                }

                try builder.combine(.{ .bit_int = bits }, bit_int_tok);
                continue;
            },
            else => break,
        }
        // consume single token specifiers here
        p.tok_i += 1;
    }
    return p.tok_i != start;
}

fn getAnonymousName(p: *Parser, kind_tok: TokenIndex) !StringId {
    const loc = p.pp.tokens.items(.loc)[kind_tok];
    const source = p.comp.getSource(loc.id);
    const line_col = source.lineCol(loc);

    const kind_str = switch (p.tok_ids[kind_tok]) {
        .keyword_struct, .keyword_union, .keyword_enum => p.tokSlice(kind_tok),
        else => "record field",
    };

    var arena = p.comp.type_store.anon_name_arena.promote(p.comp.gpa);
    defer p.comp.type_store.anon_name_arena = arena.state;
    const str = try std.fmt.allocPrint(
        arena.allocator(),
        "(anonymous {s} at {s}:{d}:{d})",
        .{ kind_str, source.path, line_col.line_no, line_col.col },
    );
    return p.comp.internString(str);
}

/// recordSpec
///  : (keyword_struct | keyword_union) IDENTIFIER? { recordDecls }
///  | (keyword_struct | keyword_union) IDENTIFIER
fn recordSpec(p: *Parser) Error!QualType {
    const gpa = p.comp.gpa;
    const starting_pragma_pack = p.pragma_pack;
    const kind_tok = p.tok_i;
    const is_struct = p.tok_ids[kind_tok] == .keyword_struct;
    p.tok_i += 1;
    const attr_buf_top = p.attr_buf.len;
    defer p.attr_buf.len = attr_buf_top;
    try p.attributeSpecifier();

    const reserved_index = try p.tree.nodes.addOne(gpa);

    const maybe_ident = try p.eatIdentifier();
    const l_brace = p.eatToken(.l_brace) orelse {
        const ident = maybe_ident orelse {
            try p.err(p.tok_i, .ident_or_l_brace, .{});
            return error.ParsingFailed;
        };
        // check if this is a reference to a previous type
        const interned_name = try p.comp.internString(p.tokSlice(ident));
        if (try p.syms.findTag(p, interned_name, p.tok_ids[kind_tok], ident, p.tok_ids[p.tok_i])) |prev| {
            return prev.qt;
        } else {
            // this is a forward declaration, create a new record type.
            const record_ty: Type.Record = .{
                .name = interned_name,
                .layout = null,
                .decl_node = @enumFromInt(reserved_index),
                .fields = &.{},
            };
            const record_qt = try p.comp.type_store.put(gpa, if (is_struct)
                .{ .@"struct" = record_ty }
            else
                .{ .@"union" = record_ty });

            const attributed_qt = try Attribute.applyTypeAttributes(p, record_qt, attr_buf_top, null);
            try p.syms.define(gpa, .{
                .kind = if (is_struct) .@"struct" else .@"union",
                .name = interned_name,
                .tok = ident,
                .qt = attributed_qt,
                .val = .{},
            });

            const fw: Node.ContainerForwardDecl = .{
                .name_or_kind_tok = ident,
                .container_qt = attributed_qt,
                .definition = null,
            };
            try p.tree.setNode(if (is_struct)
                .{ .struct_forward_decl = fw }
            else
                .{ .union_forward_decl = fw }, reserved_index);
            try p.decl_buf.append(gpa, @enumFromInt(reserved_index));
            return attributed_qt;
        }
    };

    var done = false;
    errdefer if (!done) p.skipTo(.r_brace);

    // Get forward declared type or create a new one
    var record_ty: Type.Record, const qt: QualType = blk: {
        const interned_name = if (maybe_ident) |ident| interned: {
            const ident_str = p.tokSlice(ident);
            const interned_name = try p.comp.internString(ident_str);
            if (try p.syms.defineTag(p, interned_name, p.tok_ids[kind_tok], ident)) |prev| {
                const record_ty = prev.qt.getRecord(p.comp).?;
                if (record_ty.layout != null) {
                    // if the record isn't incomplete, this is a redefinition
                    try p.err(ident, .redefinition, .{ident_str});
                    try p.err(prev.tok, .previous_definition, .{});
                } else {
                    break :blk .{ record_ty, prev.qt };
                }
            }
            break :interned interned_name;
        } else try p.getAnonymousName(kind_tok);

        // Initially create ty as a regular non-attributed type, since attributes for a record
        // can be specified after the closing rbrace, which we haven't encountered yet.
        const record_ty: Type.Record = .{
            .name = interned_name,
            .decl_node = @enumFromInt(reserved_index),
            .layout = null,
            .fields = &.{},
        };
        const record_qt = try p.comp.type_store.put(gpa, if (is_struct)
            .{ .@"struct" = record_ty }
        else
            .{ .@"union" = record_ty });

        // declare a symbol for the type
        // We need to replace the symbol's type if it has attributes
        if (maybe_ident != null) {
            try p.syms.define(gpa, .{
                .kind = if (is_struct) .@"struct" else .@"union",
                .name = record_ty.name,
                .tok = maybe_ident.?,
                .qt = record_qt,
                .val = .{},
            });
        }

        break :blk .{ record_ty, record_qt };
    };

    try p.decl_buf.append(gpa, @enumFromInt(reserved_index));
    const decl_buf_top = p.decl_buf.items.len;
    const record_buf_top = p.record_buf.items.len;
    errdefer p.decl_buf.items.len = decl_buf_top - 1;
    defer {
        p.decl_buf.items.len = decl_buf_top;
        p.record_buf.items.len = record_buf_top;
    }

    const old_record = p.record;
    const old_members = p.record_members.items.len;
    p.record = .{
        .kind = p.tok_ids[kind_tok],
        .start = p.record_members.items.len,
    };
    defer p.record = old_record;
    defer p.record_members.items.len = old_members;

    try p.recordDecls();

    const fields = p.record_buf.items[record_buf_top..];

    if (p.record.flexible_field) |some| {
        if (fields.len == 1 and is_struct) {
            if (p.comp.langopts.emulate == .msvc) {
                try p.err(some, .flexible_in_empty_msvc, .{});
            } else {
                try p.err(some, .flexible_in_empty, .{});
            }
        }
    }

    if (p.record_buf.items.len == record_buf_top) {
        try p.err(kind_tok, .empty_record, .{p.tokSlice(kind_tok)});
        try p.err(kind_tok, .empty_record_size, .{p.tokSlice(kind_tok)});
    }
    try p.expectClosing(l_brace, .r_brace);
    done = true;
    try p.attributeSpecifier();

    const any_incomplete = blk: {
        for (fields) |field| {
            if (field.qt.hasIncompleteSize(p.comp) and !field.qt.is(p.comp, .array)) break :blk true;
        }
        // Set fields and a dummy layout before addign attributes.
        record_ty.fields = fields;
        record_ty.layout = .{
            .size_bits = 8,
            .field_alignment_bits = 8,
            .pointer_alignment_bits = 8,
            .required_alignment_bits = 8,
        };
        record_ty.decl_node = @enumFromInt(reserved_index);

        const base_type = qt.base(p.comp);
        if (is_struct) {
            std.debug.assert(base_type.type.@"struct".name == record_ty.name);
            try p.comp.type_store.set(gpa, .{ .@"struct" = record_ty }, @intFromEnum(base_type.qt._index));
        } else {
            std.debug.assert(base_type.type.@"union".name == record_ty.name);
            try p.comp.type_store.set(gpa, .{ .@"union" = record_ty }, @intFromEnum(base_type.qt._index));
        }
        break :blk false;
    };

    const attributed_qt = try Attribute.applyTypeAttributes(p, qt, attr_buf_top, null);

    // Make sure the symbol for this record points to the attributed type.
    if (attributed_qt != qt and maybe_ident != null) {
        const ident_str = p.tokSlice(maybe_ident.?);
        const interned_name = try p.comp.internString(ident_str);
        const ptr = p.syms.getPtr(interned_name, .tags);
        ptr.qt = attributed_qt;
    }

    if (!any_incomplete) {
        const pragma_pack_value = switch (p.comp.langopts.emulate) {
            .clang => starting_pragma_pack,
            .gcc => p.pragma_pack,
            // TODO: msvc considers `#pragma pack` on a per-field basis
            .msvc => p.pragma_pack,
        };
        if (record_layout.compute(fields, attributed_qt, p.comp, pragma_pack_value)) |layout| {
            record_ty.fields = fields;
            record_ty.layout = layout;
        } else |er| switch (er) {
            error.Overflow => try p.err(maybe_ident orelse kind_tok, .record_too_large, .{qt}),
        }

        // Override previous incomplete layout and fields.
        const base_qt = qt.base(p.comp).qt;
        const ts = &p.comp.type_store;
        var extra_index = ts.types.items(.data)[@intFromEnum(base_qt._index)][1];

        const layout_size = 5;
        comptime std.debug.assert(@sizeOf(Type.Record.Layout) == @sizeOf(u32) * layout_size);
        const field_size = 10;
        comptime std.debug.assert(@sizeOf(Type.Record.Field) == @sizeOf(u32) * field_size);

        extra_index += 1; // For decl_node
        const casted_layout: *const [layout_size]u32 = @ptrCast(&record_ty.layout);
        ts.extra.items[extra_index..][0..layout_size].* = casted_layout.*;
        extra_index += layout_size;
        extra_index += 1; // For field length

        for (record_ty.fields) |*field| {
            const casted: *const [field_size]u32 = @ptrCast(field);
            ts.extra.items[extra_index..][0..field_size].* = casted.*;
            extra_index += field_size;
        }
    }

    // finish by creating a node
    const cd: Node.ContainerDecl = .{
        .name_or_kind_tok = maybe_ident orelse kind_tok,
        .container_qt = attributed_qt,
        .fields = p.decl_buf.items[decl_buf_top..],
    };
    try p.tree.setNode(if (is_struct) .{ .struct_decl = cd } else .{ .union_decl = cd }, reserved_index);
    if (p.func.qt == null) {
        _ = p.tentative_defs.remove(record_ty.name);
    }
    return attributed_qt;
}

/// recordDecls : (keyword_extension? recordDecl | staticAssert)*
fn recordDecls(p: *Parser) Error!void {
    while (true) {
        if (try p.pragma()) continue;
        if (try p.parseOrNextDecl(staticAssert)) continue;
        if (p.eatToken(.keyword_extension)) |_| {
            const saved_extension = p.extension_suppressed;
            defer p.extension_suppressed = saved_extension;
            p.extension_suppressed = true;

            if (try p.parseOrNextDecl(recordDecl)) continue;
            try p.err(p.tok_i, .expected_type, .{});
            p.nextExternDecl();
            continue;
        }
        if (try p.parseOrNextDecl(recordDecl)) continue;
        break;
    }
}

/// recordDecl : typeSpec+ (recordDeclarator (',' recordDeclarator)*)?
/// recordDeclarator : declarator (':' integerConstExpr)?
fn recordDecl(p: *Parser) Error!bool {
    const gpa = p.comp.gpa;
    const attr_buf_top = p.attr_buf.len;
    defer p.attr_buf.len = attr_buf_top;

    const base_qt: QualType = blk: {
        const start = p.tok_i;
        var builder: TypeStore.Builder = .{ .parser = p };
        while (true) {
            if (try p.typeSpec(&builder)) continue;
            const id = p.tok_ids[p.tok_i];
            switch (id) {
                .keyword_auto => {
                    if (!p.comp.langopts.standard.atLeast(.c23)) break;

                    try p.err(p.tok_i, .c23_auto_not_allowed, .{if (p.record.kind == .keyword_struct) "struct member" else "union member"});
                    try builder.combine(.c23_auto, p.tok_i);
                },
                .keyword_auto_type => {
                    try p.err(p.tok_i, .auto_type_extension, .{});
                    try p.err(p.tok_i, .auto_type_not_allowed, .{if (p.record.kind == .keyword_struct) "struct member" else "union member"});
                    try builder.combine(.auto_type, p.tok_i);
                },
                .identifier, .extended_identifier => {
                    if (builder.type != .none) break;
                    try p.err(p.tok_i, .unknown_type_name, .{p.tokSlice(p.tok_i)});
                    builder.type = .{ .other = .invalid };
                },
                else => break,
            }
            p.tok_i += 1;
            break;
        }
        if (p.tok_i == start) return false;
        break :blk switch (builder.type) {
            .auto_type, .c23_auto => .invalid,
            else => try builder.finish(),
        };
    };

    try p.attributeSpecifier(); // .record
    var error_on_unnamed = false;
    while (true) {
        const this_decl_top = p.attr_buf.len;
        defer p.attr_buf.len = this_decl_top;

        try p.attributeSpecifier();

        // 0 means unnamed
        var name_tok: TokenIndex = 0;
        var qt = base_qt;
        var bits_node: ?Node.Index = null;
        var bits: ?u32 = null;
        const first_tok = p.tok_i;
        if (try p.declarator(qt, .record)) |d| {
            name_tok = d.name;
            qt = d.qt;
            error_on_unnamed = true;
        }

        if (p.eatToken(.colon)) |_| bits: {
            const bits_tok = p.tok_i;
            const res = try p.integerConstExpr(.gnu_folding_extension);
            if (!qt.isInvalid() and !qt.isRealInt(p.comp)) {
                try p.err(first_tok, .non_int_bitfield, .{qt});
                break :bits;
            }

            if (res.val.opt_ref == .none) {
                try p.err(bits_tok, .expected_integer_constant_expr, .{});
                break :bits;
            } else if (res.val.compare(.lt, .zero, p.comp)) {
                try p.err(first_tok, .negative_bitwidth, .{res});
                break :bits;
            }

            // incomplete size error is reported later
            const bit_size = qt.bitSizeofOrNull(p.comp) orelse break :bits;
            const bits_unchecked = res.val.toInt(u32, p.comp) orelse std.math.maxInt(u32);
            if (bits_unchecked > bit_size) {
                try p.err(name_tok, .bitfield_too_big, .{});
                break :bits;
            } else if (bits_unchecked == 0 and name_tok != 0) {
                try p.err(name_tok, .zero_width_named_field, .{});
                break :bits;
            }

            bits = bits_unchecked;
            bits_node = res.node;
        }

        try p.attributeSpecifier(); // .record

        const to_append = try Attribute.applyFieldAttributes(p, &qt, attr_buf_top);

        const attr_index: u32 = @intCast(p.comp.type_store.attributes.items.len);
        const attr_len: u32 = @intCast(to_append.len);
        try p.comp.type_store.attributes.appendSlice(gpa, to_append);

        qt = try Attribute.applyTypeAttributes(p, qt, attr_buf_top, null);
        @memset(p.attr_buf.items(.seen)[attr_buf_top..], false);

        if (name_tok == 0 and bits == null) unnamed: {
            var is_typedef = false;
            if (!qt.isInvalid()) loop: switch (qt.type(p.comp)) {
                .attributed => |attributed_ty| continue :loop attributed_ty.base.type(p.comp),
                .typedef => |typedef_ty| {
                    is_typedef = true;
                    continue :loop typedef_ty.base.type(p.comp);
                },
                // typeof intentionally ignored here
                .@"enum" => break :unnamed,
                .@"struct", .@"union" => |record_ty| if ((record_ty.isAnonymous(p.comp) and !is_typedef) or
                    (p.comp.langopts.ms_extensions and is_typedef))
                {
                    if (!(record_ty.isAnonymous(p.comp) and !is_typedef)) {
                        try p.err(first_tok, .anonymous_struct, .{});
                    }
                    // An anonymous record appears as indirect fields on the parent
                    try p.record_buf.append(gpa, .{
                        .name = try p.getAnonymousName(first_tok),
                        .qt = qt,
                        ._attr_index = attr_index,
                        ._attr_len = attr_len,
                    });

                    const node = try p.addNode(.{
                        .record_field = .{
                            .name_or_first_tok = name_tok,
                            .qt = qt,
                            .bit_width = null,
                        },
                    });
                    try p.decl_buf.append(gpa, node);
                    try p.record.addFieldsFromAnonymous(p, record_ty);
                    break; // must be followed by a semicolon
                },
                else => {},
            };
            if (error_on_unnamed) {
                try p.err(first_tok, .expected_member_name, .{});
            } else {
                try p.err(p.tok_i, .missing_declaration, .{});
            }
            if (p.eatToken(.comma) == null) break;
            continue;
        } else {
            const interned_name = if (name_tok != 0) try p.comp.internString(p.tokSlice(name_tok)) else try p.getAnonymousName(first_tok);
            try p.record_buf.append(gpa, .{
                .name = interned_name,
                .qt = qt,
                .name_tok = name_tok,
                .bit_width = if (bits) |some| @enumFromInt(some) else .null,
                ._attr_index = attr_index,
                ._attr_len = attr_len,
            });
            if (name_tok != 0) try p.record.addField(p, interned_name, name_tok);
            const node = try p.addNode(.{
                .record_field = .{
                    .name_or_first_tok = name_tok,
                    .qt = qt,
                    .bit_width = bits_node,
                },
            });
            try p.decl_buf.append(gpa, node);
        }

        if (!qt.isInvalid()) {
            const field_type = qt.base(p.comp);
            switch (field_type.type) {
                .func => {
                    try p.err(first_tok, .func_field, .{});
                    qt = .invalid;
                },
                .array => |array_ty| switch (array_ty.len) {
                    .static, .unspecified_variable => unreachable,
                    .variable => {
                        try p.err(first_tok, .vla_field, .{});
                        qt = .invalid;
                    },
                    .fixed => {},
                    .incomplete => {
                        if (p.record.kind == .keyword_union) {
                            if (p.comp.langopts.emulate == .msvc) {
                                try p.err(first_tok, .flexible_in_union_msvc, .{});
                            } else {
                                try p.err(first_tok, .flexible_in_union, .{});
                                qt = .invalid;
                            }
                        }
                        if (p.record.flexible_field) |some| {
                            if (p.record.kind == .keyword_struct) {
                                try p.err(some, .flexible_non_final, .{});
                            }
                        }
                        p.record.flexible_field = first_tok;
                    },
                },
                else => if (field_type.qt.hasIncompleteSize(p.comp)) {
                    try p.err(first_tok, .field_incomplete_ty, .{qt});
                } else if (p.record.flexible_field) |some| {
                    std.debug.assert(some != first_tok);
                    if (p.record.kind == .keyword_struct) try p.err(some, .flexible_non_final, .{});
                },
            }
        }

        if (p.eatToken(.comma) == null) break;
        error_on_unnamed = true;
    }

    if (p.eatToken(.semicolon) == null) {
        const tok_id = p.tok_ids[p.tok_i];
        if (tok_id == .r_brace) {
            try p.err(p.tok_i, .missing_semicolon, .{});
        } else {
            return p.errExpectedToken(.semicolon, tok_id);
        }
    }

    return true;
}

/// specQual : typeSpec+
fn specQual(p: *Parser) Error!?QualType {
    var builder: TypeStore.Builder = .{ .parser = p };
    if (try p.typeSpec(&builder)) {
        return try builder.finish();
    }
    return null;
}

/// enumSpec
///  : keyword_enum IDENTIFIER? (: typeName)? { enumerator (',' enumerator)? ',') }
///  | keyword_enum IDENTIFIER (: typeName)?
fn enumSpec(p: *Parser) Error!QualType {
    const gpa = p.comp.gpa;
    const enum_tok = p.tok_i;
    p.tok_i += 1;
    const attr_buf_top = p.attr_buf.len;
    defer p.attr_buf.len = attr_buf_top;
    try p.attributeSpecifier();

    const maybe_ident = try p.eatIdentifier();
    const fixed_qt: ?QualType = if (p.eatToken(.colon)) |colon| fixed: {
        const ty_start = p.tok_i;
        const fixed = (try p.specQual()) orelse {
            if (p.record.kind != .invalid) {
                // This is a bit field.
                p.tok_i -= 1;
                break :fixed null;
            }
            try p.err(p.tok_i, .expected_type, .{});
            try p.err(colon, .enum_fixed, .{});
            break :fixed .int;
        };

        var final = fixed;
        while (true) {
            switch (final.base(p.comp).type) {
                .int => {
                    try p.err(colon, .enum_fixed, .{});
                    if (final.isQualified()) try p.err(ty_start, .enum_qualifiers_ignored, .{});
                    break :fixed final.unqualified();
                },
                .atomic => |atomic| {
                    try p.err(ty_start, .enum_atomic_ignored, .{});
                    final = atomic.withQualifiers(final);
                },
                else => {
                    try p.err(ty_start, .enum_invalid_underlying_type, .{fixed});
                    break :fixed .int;
                },
            }
        }
    } else null;

    const reserved_index = try p.tree.nodes.addOne(gpa);

    const l_brace = p.eatToken(.l_brace) orelse {
        const ident = maybe_ident orelse {
            try p.err(p.tok_i, .ident_or_l_brace, .{});
            return error.ParsingFailed;
        };
        // check if this is a reference to a previous type
        const interned_name = try p.comp.internString(p.tokSlice(ident));
        if (try p.syms.findTag(p, interned_name, p.tok_ids[enum_tok], ident, p.tok_ids[p.tok_i])) |prev| {
            // only check fixed underlying type in forward declarations and not in references.
            if (p.tok_ids[p.tok_i] == .semicolon)
                try p.checkEnumFixedTy(fixed_qt, ident, prev);
            return prev.qt;
        } else {
            if (fixed_qt == null) try p.err(ident, .enum_forward_declaration, .{});

            const enum_qt = try p.comp.type_store.put(gpa, .{ .@"enum" = .{
                .name = interned_name,
                .tag = fixed_qt,
                .fixed = fixed_qt != null,
                .incomplete = true,
                .decl_node = @enumFromInt(reserved_index),
                .fields = &.{},
            } });

            const attributed_qt = try Attribute.applyTypeAttributes(p, enum_qt, attr_buf_top, null);
            try p.syms.define(gpa, .{
                .kind = .@"enum",
                .name = interned_name,
                .tok = ident,
                .qt = attributed_qt,
                .val = .{},
            });

            try p.decl_buf.append(gpa, try p.addNode(.{ .enum_forward_decl = .{
                .name_or_kind_tok = ident,
                .container_qt = attributed_qt,
                .definition = null,
            } }));
            return attributed_qt;
        }
    };

    var done = false;
    errdefer if (!done) p.skipTo(.r_brace);

    // Get forward declared type or create a new one
    var defined = false;
    var enum_ty: Type.Enum, const qt: QualType = blk: {
        const interned_name = if (maybe_ident) |ident| interned: {
            const ident_str = p.tokSlice(ident);
            const interned_name = try p.comp.internString(ident_str);
            if (try p.syms.defineTag(p, interned_name, p.tok_ids[enum_tok], ident)) |prev| {
                const enum_ty = prev.qt.get(p.comp, .@"enum").?;
                if (!enum_ty.incomplete) {
                    // if the record isn't incomplete, this is a redefinition
                    try p.err(ident, .redefinition, .{ident_str});
                    try p.err(prev.tok, .previous_definition, .{});
                } else {
                    try p.checkEnumFixedTy(fixed_qt, ident, prev);
                    defined = true;
                    break :blk .{ enum_ty, prev.qt };
                }
            }
            break :interned interned_name;
        } else try p.getAnonymousName(enum_tok);

        // Initially create ty as a regular non-attributed type, since attributes for a record
        // can be specified after the closing rbrace, which we haven't encountered yet.
        const enum_ty: Type.Enum = .{
            .name = interned_name,
            .decl_node = @enumFromInt(reserved_index),
            .tag = fixed_qt,
            .incomplete = true,
            .fixed = fixed_qt != null,
            .fields = &.{},
        };
        const enum_qt = try p.comp.type_store.put(gpa, .{ .@"enum" = enum_ty });
        break :blk .{ enum_ty, enum_qt };
    };

    // reserve space for this enum
    try p.decl_buf.append(gpa, @enumFromInt(reserved_index));
    const decl_buf_top = p.decl_buf.items.len;
    const list_buf_top = p.list_buf.items.len;
    const enum_buf_top = p.enum_buf.items.len;
    errdefer p.decl_buf.items.len = decl_buf_top - 1;
    defer {
        p.decl_buf.items.len = decl_buf_top;
        p.list_buf.items.len = list_buf_top;
        p.enum_buf.items.len = enum_buf_top;
    }

    var e = Enumerator.init(fixed_qt);
    while (try p.enumerator(&e)) |field_and_node| {
        try p.enum_buf.append(gpa, field_and_node.field);
        try p.list_buf.append(gpa, field_and_node.node);
        if (p.eatToken(.comma) == null) break;
    }

    if (p.enum_buf.items.len == enum_buf_top) try p.err(p.tok_i, .empty_enum, .{});
    try p.expectClosing(l_brace, .r_brace);
    done = true;
    try p.attributeSpecifier();

    const attributed_qt = try Attribute.applyTypeAttributes(p, qt, attr_buf_top, null);
    if (!enum_ty.fixed) {
        enum_ty.tag = try e.getTypeSpecifier(p, attributed_qt.enumIsPacked(p.comp), maybe_ident orelse enum_tok);
    }

    const enum_fields = p.enum_buf.items[enum_buf_top..];
    const field_nodes = p.list_buf.items[list_buf_top..];

    if (fixed_qt == null) {
        // Coerce all fields to final type.
        const tag_qt = enum_ty.tag.?;
        const keep_int = e.num_positive_bits < Type.Int.int.bits(p.comp);
        for (enum_fields, field_nodes) |*field, field_node| {
            const sym = p.syms.get(field.name, .vars) orelse continue;
            if (sym.kind != .enumeration) continue; // already an error

            var res: Result = .{ .node = undefined, .qt = field.qt, .val = sym.val };
            const dest_qt: QualType = if (keep_int and try res.intFitsInType(p, .int))
                .int
            else
                tag_qt;
            if (field.qt.eql(dest_qt, p.comp)) continue;

            const symbol = p.syms.getPtr(field.name, .vars);
            _ = try symbol.val.intCast(dest_qt, p.comp);
            try p.tree.value_map.put(gpa, field_node, symbol.val);

            symbol.qt = dest_qt;
            field.qt = dest_qt;
            res.qt = dest_qt;

            // Create a new enum_field node with the correct type.
            var new_field_node = field_node.get(&p.tree);
            new_field_node.enum_field.qt = dest_qt;

            if (new_field_node.enum_field.init) |some| {
                res.node = some;
                try res.implicitCast(p, .int_cast, some.tok(&p.tree));
                new_field_node.enum_field.init = res.node;
            }

            try p.tree.setNode(new_field_node, @intFromEnum(field_node));
        }
    }

    { // Override previous incomplete type
        enum_ty.fields = enum_fields;
        enum_ty.incomplete = false;
        enum_ty.decl_node = @enumFromInt(reserved_index);
        const base_type = attributed_qt.base(p.comp);
        std.debug.assert(base_type.type.@"enum".name == enum_ty.name);
        try p.comp.type_store.set(gpa, .{ .@"enum" = enum_ty }, @intFromEnum(base_type.qt._index));
    }

    // declare a symbol for the type
    if (maybe_ident != null and !defined) {
        try p.syms.define(gpa, .{
            .kind = .@"enum",
            .name = enum_ty.name,
            .qt = attributed_qt,
            .tok = maybe_ident.?,
            .val = .{},
        });
    }

    // finish by creating a node
    try p.tree.setNode(.{ .enum_decl = .{
        .name_or_kind_tok = maybe_ident orelse enum_tok,
        .container_qt = attributed_qt,
        .fields = field_nodes,
    } }, reserved_index);

    if (p.func.qt == null) {
        _ = p.tentative_defs.remove(enum_ty.name);
    }
    return attributed_qt;
}

fn checkEnumFixedTy(p: *Parser, fixed_qt: ?QualType, ident_tok: TokenIndex, prev: Symbol) !void {
    const enum_ty = prev.qt.get(p.comp, .@"enum").?;
    if (fixed_qt) |some| {
        if (!enum_ty.fixed) {
            try p.err(ident_tok, .enum_prev_nonfixed, .{});
            try p.err(prev.tok, .previous_definition, .{});
            return error.ParsingFailed;
        }

        if (!enum_ty.tag.?.eql(some, p.comp)) {
            try p.err(ident_tok, .enum_different_explicit_ty, .{ some, enum_ty.tag.? });
            try p.err(prev.tok, .previous_definition, .{});
            return error.ParsingFailed;
        }
    } else if (enum_ty.fixed) {
        try p.err(ident_tok, .enum_prev_fixed, .{});
        try p.err(prev.tok, .previous_definition, .{});
        return error.ParsingFailed;
    }
}

const Enumerator = struct {
    val: Value = .{},
    qt: QualType,
    num_positive_bits: usize = 0,
    num_negative_bits: usize = 0,
    fixed: bool,

    fn init(fixed_ty: ?QualType) Enumerator {
        return .{
            .qt = fixed_ty orelse .int,
            .fixed = fixed_ty != null,
        };
    }

    /// Increment enumerator value adjusting type if needed.
    fn incr(e: *Enumerator, p: *Parser, tok: TokenIndex) !void {
        const old_val = e.val;
        if (old_val.opt_ref == .none) {
            // First enumerator, set to 0 fits in all types.
            e.val = .zero;
            return;
        }
        if (try e.val.add(e.val, .one, e.qt, p.comp)) {
            if (e.fixed) {
                try p.err(tok, .enum_not_representable_fixed, .{e.qt});
                return;
            }
            if (p.comp.nextLargestIntSameSign(e.qt)) |larger| {
                try p.err(tok, .enumerator_overflow, .{});
                e.qt = larger;
            } else {
                const signed = e.qt.signedness(p.comp) == .signed;
                const bit_size = e.qt.bitSizeof(p.comp) - @intFromBool(signed);
                try p.err(tok, .enum_not_representable, .{switch (bit_size) {
                    63 => "9223372036854775808",
                    64 => "18446744073709551616",
                    127 => "170141183460469231731687303715884105728",
                    128 => "340282366920938463463374607431768211456",
                    else => unreachable,
                }});
                e.qt = .ulong_long;
            }
            _ = try e.val.add(old_val, .one, e.qt, p.comp);
        }
    }

    /// Set enumerator value to specified value.
    fn set(e: *Enumerator, p: *Parser, res: *Result, tok: TokenIndex) !void {
        if (res.qt.isInvalid()) return;
        if (e.fixed and !res.qt.eql(e.qt, p.comp)) {
            if (!try res.intFitsInType(p, e.qt)) {
                try p.err(tok, .enum_not_representable_fixed, .{e.qt});
                return error.ParsingFailed;
            }
            res.qt = e.qt;
            try res.implicitCast(p, .int_cast, tok);
            e.val = res.val;
        } else {
            try res.castToInt(p, res.qt.promoteInt(p.comp), tok);
            e.qt = res.qt;
            e.val = res.val;
        }
    }

    fn getTypeSpecifier(e: *const Enumerator, p: *Parser, is_packed: bool, tok: TokenIndex) !QualType {
        if (p.comp.fixedEnumTagType()) |tag_specifier| return tag_specifier;

        const char_width = Type.Int.schar.bits(p.comp);
        const short_width = Type.Int.short.bits(p.comp);
        const int_width = Type.Int.int.bits(p.comp);
        if (e.num_negative_bits > 0) {
            if (is_packed and e.num_negative_bits <= char_width and e.num_positive_bits < char_width) {
                return .schar;
            } else if (is_packed and e.num_negative_bits <= short_width and e.num_positive_bits < short_width) {
                return .short;
            } else if (e.num_negative_bits <= int_width and e.num_positive_bits < int_width) {
                return .int;
            }
            const long_width = Type.Int.long.bits(p.comp);
            if (e.num_negative_bits <= long_width and e.num_positive_bits < long_width) {
                return .long;
            }
            const long_long_width = Type.Int.long_long.bits(p.comp);
            if (e.num_negative_bits > long_long_width or e.num_positive_bits >= long_long_width) {
                try p.err(tok, .enum_too_large, .{});
            }
            return .long_long;
        }
        if (is_packed and e.num_positive_bits <= char_width) {
            return .uchar;
        } else if (is_packed and e.num_positive_bits <= short_width) {
            return .ushort;
        } else if (e.num_positive_bits <= int_width) {
            return .uint;
        } else if (e.num_positive_bits <= Type.Int.long.bits(p.comp)) {
            return .ulong;
        }
        return .ulong_long;
    }
};

const EnumFieldAndNode = struct { field: Type.Enum.Field, node: Node.Index };

/// enumerator : IDENTIFIER ('=' integerConstExpr)
fn enumerator(p: *Parser, e: *Enumerator) Error!?EnumFieldAndNode {
    _ = try p.pragma();
    const name_tok = (try p.eatIdentifier()) orelse {
        if (p.tok_ids[p.tok_i] == .r_brace) return null;
        try p.err(p.tok_i, .expected_identifier, .{});
        p.skipTo(.r_brace);
        return error.ParsingFailed;
    };
    const attr_buf_top = p.attr_buf.len;
    defer p.attr_buf.len = attr_buf_top;
    try p.attributeSpecifier();

    const prev_total = p.diagnostics.total;
    const field_init = if (p.eatToken(.equal)) |_| blk: {
        var specified = try p.integerConstExpr(.gnu_folding_extension);
        if (specified.val.opt_ref == .none) {
            try p.err(name_tok + 2, .enum_val_unavailable, .{});
            try e.incr(p, name_tok);
            break :blk null;
        } else {
            try e.set(p, &specified, name_tok);
            break :blk specified.node;
        }
    } else blk: {
        try e.incr(p, name_tok);
        break :blk null;
    };

    if (e.qt.signedness(p.comp) == .unsigned or e.val.compare(.gte, .zero, p.comp)) {
        e.num_positive_bits = @max(e.num_positive_bits, e.val.minUnsignedBits(p.comp));
    } else {
        e.num_negative_bits = @max(e.num_negative_bits, e.val.minSignedBits(p.comp));
    }

    if (prev_total == p.diagnostics.total) {
        // only do these warnings if we didn't already warn about overflow or non-representable values
        if (e.val.compare(.lt, .zero, p.comp)) {
            const min_val = try Value.minInt(.int, p.comp);
            if (e.val.compare(.lt, min_val, p.comp)) {
                try p.err(name_tok, .enumerator_too_small, .{e});
            }
        } else {
            const max_val = try Value.maxInt(.int, p.comp);
            if (e.val.compare(.gt, max_val, p.comp)) {
                try p.err(name_tok, .enumerator_too_large, .{e});
            }
        }
    }

    const attributed_qt = try Attribute.applyEnumeratorAttributes(p, e.qt, attr_buf_top);
    const node = try p.addNode(.{
        .enum_field = .{
            .name_tok = name_tok,
            .qt = attributed_qt,
            .init = field_init,
        },
    });
    try p.tree.value_map.put(p.comp.gpa, node, e.val);

    const interned_name = try p.comp.internString(p.tokSlice(name_tok));
    try p.syms.defineEnumeration(p, interned_name, attributed_qt, name_tok, e.val, node);

    return .{ .field = .{
        .name = interned_name,
        .qt = attributed_qt,
        .name_tok = name_tok,
    }, .node = node };
}

/// typeQual : keyword_const | keyword_restrict | keyword_volatile | keyword_atomic
fn typeQual(p: *Parser, b: *TypeStore.Builder, allow_attr: bool) Error!bool {
    var any = false;
    while (true) {
        if (allow_attr and try p.msTypeAttribute()) continue;
        if (allow_attr) try p.attributeSpecifier();
        switch (p.tok_ids[p.tok_i]) {
            .keyword_restrict, .keyword_restrict1, .keyword_restrict2 => {
                if (b.restrict != null)
                    try p.err(p.tok_i, .duplicate_decl_spec, .{"restrict"})
                else
                    b.restrict = p.tok_i;
            },
            .keyword_const, .keyword_const1, .keyword_const2 => {
                if (b.@"const" != null)
                    try p.err(p.tok_i, .duplicate_decl_spec, .{"const"})
                else
                    b.@"const" = p.tok_i;
            },
            .keyword_volatile, .keyword_volatile1, .keyword_volatile2 => {
                if (b.@"volatile" != null)
                    try p.err(p.tok_i, .duplicate_decl_spec, .{"volatile"})
                else
                    b.@"volatile" = p.tok_i;
            },
            .keyword_atomic => {
                // _Atomic(typeName) instead of just _Atomic
                if (p.tok_ids[p.tok_i + 1] == .l_paren) break;
                if (b.atomic != null)
                    try p.err(p.tok_i, .duplicate_decl_spec, .{"atomic"})
                else
                    b.atomic = p.tok_i;
            },
            .keyword_unaligned, .keyword_unaligned2 => {
                if (b.unaligned != null)
                    try p.err(p.tok_i, .duplicate_decl_spec, .{"__unaligned"})
                else
                    b.unaligned = p.tok_i;
            },
            .keyword_nonnull, .keyword_nullable, .keyword_nullable_result, .keyword_null_unspecified => |tok_id| {
                const sym_str = p.tok_ids[p.tok_i].symbol();
                try p.err(p.tok_i, .nullability_extension, .{sym_str});
                const new: @FieldType(TypeStore.Builder, "nullability") = switch (tok_id) {
                    .keyword_nonnull => .{ .nonnull = p.tok_i },
                    .keyword_nullable => .{ .nullable = p.tok_i },
                    .keyword_nullable_result => .{ .nullable_result = p.tok_i },
                    .keyword_null_unspecified => .{ .null_unspecified = p.tok_i },
                    else => unreachable,
                };
                if (std.meta.activeTag(b.nullability) == new) {
                    try p.err(p.tok_i, .duplicate_nullability, .{sym_str});
                } else switch (b.nullability) {
                    .none => {
                        b.nullability = new;
                        try p.attr_buf.append(p.comp.gpa, .{
                            .attr = .{ .tag = .nullability, .args = .{
                                .nullability = .{ .kind = switch (tok_id) {
                                    .keyword_nonnull => .nonnull,
                                    .keyword_nullable => .nullable,
                                    .keyword_nullable_result => .nullable_result,
                                    .keyword_null_unspecified => .unspecified,
                                    else => unreachable,
                                } },
                            }, .syntax = .keyword },
                            .tok = p.tok_i,
                        });
                    },
                    .nonnull,
                    .nullable,
                    .nullable_result,
                    .null_unspecified,
                    => |prev| try p.err(p.tok_i, .conflicting_nullability, .{ p.tok_ids[p.tok_i], p.tok_ids[prev] }),
                }
            },
            else => break,
        }
        p.tok_i += 1;
        any = true;
    }
    return any;
}

fn msTypeAttribute(p: *Parser) !bool {
    var any = false;
    while (true) {
        switch (p.tok_ids[p.tok_i]) {
            .keyword_stdcall,
            .keyword_stdcall2,
            .keyword_thiscall,
            .keyword_thiscall2,
            .keyword_vectorcall,
            .keyword_vectorcall2,
            .keyword_fastcall,
            .keyword_fastcall2,
            .keyword_regcall,
            .keyword_cdecl,
            .keyword_cdecl2,
            => {
                try p.attr_buf.append(p.comp.gpa, .{
                    .attr = .{ .tag = .calling_convention, .args = .{
                        .calling_convention = .{ .cc = switch (p.tok_ids[p.tok_i]) {
                            .keyword_stdcall,
                            .keyword_stdcall2,
                            => .stdcall,
                            .keyword_thiscall,
                            .keyword_thiscall2,
                            => .thiscall,
                            .keyword_vectorcall,
                            .keyword_vectorcall2,
                            => .vectorcall,
                            .keyword_fastcall,
                            .keyword_fastcall2,
                            => .fastcall,
                            .keyword_regcall,
                            => .regcall,
                            .keyword_cdecl,
                            .keyword_cdecl2,
                            => .c,
                            else => unreachable,
                        } },
                    }, .syntax = .keyword },
                    .tok = p.tok_i,
                });
                any = true;
                p.tok_i += 1;
            },
            else => break,
        }
    }
    return any;
}

const Declarator = struct {
    name: TokenIndex,
    qt: QualType,
    old_style_func: ?TokenIndex = null,

    /// What kind of a type did this declarator declare?
    /// Used redundantly with `qt` in case it was set to `.invalid` by `validate`.
    declarator_type: enum { other, func, array, pointer } = .other,

    const Kind = enum { normal, abstract, param, record };

    fn validate(d: *Declarator, p: *Parser, source_tok: TokenIndex) Parser.Error!void {
        switch (try validateExtra(p, d.qt, source_tok)) {
            .normal => return,
            .nested_invalid => if (d.declarator_type == .func) return,
            .nested_auto => {
                if (d.declarator_type == .func) return;
                if (d.qt.isAutoType() or d.qt.isC23Auto()) return;
            },
            .declarator_combine => return,
        }
        d.qt = .invalid;
    }

    const ValidationResult = enum {
        nested_invalid,
        nested_auto,
        declarator_combine,
        normal,
    };

    fn validateExtra(p: *Parser, cur: QualType, source_tok: TokenIndex) Parser.Error!ValidationResult {
        if (cur.isInvalid()) return .nested_invalid;
        if (cur.isAutoType()) return .nested_auto;
        if (cur.isC23Auto()) return .nested_auto;
        if (cur._index == .declarator_combine) return .declarator_combine;

        switch (cur.type(p.comp)) {
            .pointer => |pointer_ty| {
                return validateExtra(p, pointer_ty.child, source_tok);
            },
            .atomic => |atomic_ty| {
                return validateExtra(p, atomic_ty, source_tok);
            },
            .array => |array_ty| {
                const elem_qt = array_ty.elem;
                const child_res = try validateExtra(p, elem_qt, source_tok);
                if (child_res != .normal) return child_res;

                if (elem_qt.hasIncompleteSize(p.comp)) {
                    try p.err(source_tok, .array_incomplete_elem, .{elem_qt});
                    return .nested_invalid;
                }
                switch (array_ty.len) {
                    .fixed, .static => |len| {
                        const elem_size = elem_qt.sizeofOrNull(p.comp) orelse 1;
                        const max_elems = p.comp.maxArrayBytes() / @max(1, elem_size);
                        if (len > max_elems) {
                            try p.err(source_tok, .array_too_large, .{});
                            return .nested_invalid;
                        }
                    },
                    else => {},
                }

                if (elem_qt.is(p.comp, .func)) {
                    try p.err(source_tok, .array_func_elem, .{});
                    return .nested_invalid;
                }
                if (elem_qt.get(p.comp, .array)) |elem_array_ty| {
                    if (elem_array_ty.len == .static) {
                        try p.err(source_tok, .static_non_outermost_array, .{});
                    }
                    if (elem_qt.isQualified()) {
                        try p.err(source_tok, .qualifier_non_outermost_array, .{});
                    }
                }
                return .normal;
            },
            .func => |func_ty| {
                const ret_qt = func_ty.return_type;
                const child_res = try validateExtra(p, ret_qt, source_tok);
                if (child_res != .normal) return child_res;

                if (ret_qt.is(p.comp, .array)) try p.err(source_tok, .func_cannot_return_array, .{});
                if (ret_qt.is(p.comp, .func)) try p.err(source_tok, .func_cannot_return_func, .{});
                if (ret_qt.@"const") {
                    try p.err(source_tok, .qual_on_ret_type, .{"const"});
                }
                if (ret_qt.@"volatile") {
                    try p.err(source_tok, .qual_on_ret_type, .{"volatile"});
                }
                if (ret_qt.get(p.comp, .float)) |float| {
                    if (float == .fp16 and !p.comp.hasHalfPrecisionFloatABI()) {
                        try p.err(source_tok, .suggest_pointer_for_invalid_fp16, .{"function return value"});
                    }
                }
                return .normal;
            },
            else => return .normal,
        }
    }
};

/// declarator : pointer? (IDENTIFIER | '(' declarator ')') directDeclarator*
/// abstractDeclarator
/// : pointer? ('(' abstractDeclarator ')')? directAbstractDeclarator*
/// pointer : '*' typeQual* pointer?
fn declarator(
    p: *Parser,
    base_qt: QualType,
    kind: Declarator.Kind,
) Error!?Declarator {
    var d = Declarator{ .name = 0, .qt = base_qt };

    // Parse potential pointer declarators first.
    while (p.eatToken(.asterisk)) |_| {
        d.declarator_type = .pointer;
        var builder: TypeStore.Builder = .{ .parser = p };
        _ = try p.typeQual(&builder, true);

        const pointer_qt = try p.comp.type_store.put(p.comp.gpa, .{ .pointer = .{
            .child = d.qt,
            .decayed = null,
        } });
        d.qt = try builder.finishQuals(pointer_qt);
    }

    const maybe_ident = p.tok_i;
    if (kind != .abstract and (try p.eatIdentifier()) != null) {
        d.name = maybe_ident;
        const combine_tok = p.tok_i;
        d.qt = try p.directDeclarator(&d, kind);
        try d.validate(p, combine_tok);
        return d;
    } else if (p.eatToken(.l_paren)) |l_paren| blk: {
        // C23 and declspec attributes are not allowed here
        try p.attributeSpecifierGnu();

        // Parse Microsoft keyword type attributes.
        _ = try p.msTypeAttribute();

        const special_marker: QualType = .{ ._index = .declarator_combine };
        var res = (try p.declarator(special_marker, kind)) orelse {
            p.tok_i = l_paren;
            break :blk;
        };
        try p.expectClosing(l_paren, .r_paren);
        const suffix_start = p.tok_i;
        const outer = try p.directDeclarator(&d, kind);

        // Correct the base type now that it is known.
        // If res.qt is the special marker there was no inner type.
        if (res.qt._index == .declarator_combine) {
            res.qt = outer;
            res.declarator_type = d.declarator_type;
        } else if (outer.isInvalid() or res.qt.isInvalid()) {
            res.qt = outer;
        } else {
            var cur = res.qt;
            while (true) {
                switch (cur.type(p.comp)) {
                    .pointer => |pointer_ty| if (pointer_ty.child._index != .declarator_combine) {
                        cur = pointer_ty.child;
                        continue;
                    },
                    .atomic => |atomic_ty| if (atomic_ty._index != .declarator_combine) {
                        cur = atomic_ty;
                        continue;
                    },
                    .array => |array_ty| if (array_ty.elem._index != .declarator_combine) {
                        cur = array_ty.elem;
                        continue;
                    },
                    .func => |func_ty| if (func_ty.return_type._index != .declarator_combine) {
                        cur = func_ty.return_type;
                        continue;
                    },
                    else => unreachable,
                }
                // Child type is always stored in repr.data[0]
                p.comp.type_store.types.items(.data)[@intFromEnum(cur._index)][0] = @bitCast(outer);
                break;
            }
        }

        try res.validate(p, suffix_start);
        return res;
    }

    const expected_ident = p.tok_i;

    d.qt = try p.directDeclarator(&d, kind);
    if (kind == .normal) {
        var cur = d.qt;
        while (true) {
            // QualType.base inlined here because of potential
            // .declarator_combine.
            if (cur._index == .declarator_combine) break;
            switch (cur.type(p.comp)) {
                .typeof => |typeof_ty| cur = typeof_ty.base,
                .typedef => |typedef_ty| cur = typedef_ty.base,
                .attributed => |attributed_ty| cur = attributed_ty.base,
                else => |ty| switch (ty) {
                    .@"enum", .@"struct", .@"union" => break,
                    else => {
                        try p.err(expected_ident, .expected_ident_or_l_paren, .{});
                        return error.ParsingFailed;
                    },
                },
            }
        }
    }
    try d.validate(p, expected_ident);
    if (d.qt == base_qt) return null;
    return d;
}

/// directDeclarator
///  : '[' typeQual* assignExpr? ']' directDeclarator?
///  | '[' keyword_static typeQual* assignExpr ']' directDeclarator?
///  | '[' typeQual+ keyword_static assignExpr ']' directDeclarator?
///  | '[' typeQual* '*' ']' directDeclarator?
///  | '(' paramDecls ')' directDeclarator?
///  | '(' (IDENTIFIER (',' IDENTIFIER))? ')' directDeclarator?
/// directAbstractDeclarator
///  : '[' typeQual* assignExpr? ']'
///  | '[' keyword_static typeQual* assignExpr ']'
///  | '[' typeQual+ keyword_static assignExpr ']'
///  | '[' '*' ']'
///  | '(' paramDecls? ')'
fn directDeclarator(
    p: *Parser,
    base_declarator: *Declarator,
    kind: Declarator.Kind,
) Error!QualType {
    const gpa = p.comp.gpa;
    if (p.eatToken(.l_bracket)) |l_bracket| {
        // Check for C23 attribute
        if (p.tok_ids[p.tok_i] == .l_bracket) {
            switch (kind) {
                .normal, .record => if (p.comp.langopts.standard.atLeast(.c23)) {
                    p.tok_i -= 1;
                    return base_declarator.qt;
                },
                .param, .abstract => {},
            }
            try p.err(p.tok_i, .expected_expr, .{});
            return error.ParsingFailed;
        }

        var builder: TypeStore.Builder = .{ .parser = p };

        var got_quals = try p.typeQual(&builder, false);
        var static = p.eatToken(.keyword_static);
        if (static != null and !got_quals) got_quals = try p.typeQual(&builder, false);
        var star = p.eatToken(.asterisk);
        const size_tok = p.tok_i;

        const const_decl_folding = p.const_decl_folding;
        p.const_decl_folding = .gnu_vla_folding_extension;
        const opt_size = if (star) |_| null else try p.assignExpr();
        p.const_decl_folding = const_decl_folding;

        try p.expectClosing(l_bracket, .r_bracket);

        if (star != null and static != null) {
            try p.err(static.?, .invalid_static_star, .{});
            static = null;
        }
        if (kind != .param) {
            if (static != null)
                try p.err(l_bracket, .static_non_param, .{})
            else if (got_quals)
                try p.err(l_bracket, .array_qualifiers, .{});
            if (star) |some| try p.err(some, .star_non_param, .{});
            static = null;
            builder = .{ .parser = p };
            star = null;
        }
        if (static) |_| _ = try p.expectResult(opt_size);

        const outer = try p.directDeclarator(base_declarator, kind);

        // Set after call to `directDeclarator` since we will return an
        // array type from here.
        base_declarator.declarator_type = .array;

        if (opt_size != null and !opt_size.?.qt.isInvalid() and !opt_size.?.qt.isRealInt(p.comp)) {
            try p.err(size_tok, .array_size_non_int, .{opt_size.?.qt});
            return error.ParsingFailed;
        }

        if (opt_size) |size| {
            if (size.val.opt_ref == .none) {
                try p.err(size_tok, .vla, .{});
                if (p.func.qt == null and kind != .param and p.record.kind == .invalid) {
                    try p.err(base_declarator.name, .variable_len_array_file_scope, .{});
                }

                const array_qt = try p.comp.type_store.put(gpa, .{ .array = .{
                    .elem = outer,
                    .len = .{ .variable = size.node },
                } });

                if (static) |some| try p.err(some, .useless_static, .{});
                return builder.finishQuals(array_qt);
            } else {
                if (size.val.isZero(p.comp)) {
                    try p.err(l_bracket, .zero_length_array, .{});
                } else if (size.val.compare(.lt, .zero, p.comp)) {
                    try p.err(l_bracket, .negative_array_size, .{});
                    return error.ParsingFailed;
                }

                const len = size.val.toInt(u64, p.comp) orelse std.math.maxInt(u64);
                const array_qt = try p.comp.type_store.put(gpa, .{ .array = .{
                    .elem = outer,
                    .len = if (static != null)
                        .{ .static = len }
                    else
                        .{ .fixed = len },
                } });
                return builder.finishQuals(array_qt);
            }
        } else if (star) |_| {
            const array_qt = try p.comp.type_store.put(gpa, .{ .array = .{
                .elem = outer,
                .len = .unspecified_variable,
            } });
            return builder.finishQuals(array_qt);
        } else {
            const array_qt = try p.comp.type_store.put(gpa, .{ .array = .{
                .elem = outer,
                .len = .incomplete,
            } });
            return builder.finishQuals(array_qt);
        }
    } else if (p.eatToken(.l_paren)) |l_paren| {
        var func_ty: Type.Func = .{
            .kind = undefined,
            .return_type = undefined,
            .params = &.{},
        };

        if (p.eatToken(.ellipsis)) |_| {
            try p.err(p.tok_i, .param_before_var_args, .{});
            try p.expectClosing(l_paren, .r_paren);
            func_ty.kind = .variadic;

            func_ty.return_type = try p.directDeclarator(base_declarator, kind);

            // Set after call to `directDeclarator` since we will return
            // a function type from here.
            base_declarator.declarator_type = .func;
            return p.comp.type_store.put(gpa, .{ .func = func_ty });
        }

        // Set here so the call to directDeclarator for the return type
        // doesn't clobber this function type's parameters.
        const param_buf_top = p.param_buf.items.len;
        defer p.param_buf.items.len = param_buf_top;

        if (try p.paramDecls()) |params| {
            func_ty.kind = .normal;
            func_ty.params = params;
            if (p.eatToken(.ellipsis)) |_| func_ty.kind = .variadic;
        } else if (p.tok_ids[p.tok_i] == .r_paren) {
            func_ty.kind = if (p.comp.langopts.standard.atLeast(.c23))
                .normal
            else
                .old_style;
        } else if (p.tok_ids[p.tok_i] == .identifier or p.tok_ids[p.tok_i] == .extended_identifier) {
            base_declarator.old_style_func = p.tok_i;
            try p.syms.pushScope(p);
            defer p.syms.popScope();

            func_ty.kind = .old_style;
            while (true) {
                const name_tok = try p.expectIdentifier();
                const interned_name = try p.comp.internString(p.tokSlice(name_tok));
                try p.syms.defineParam(p, interned_name, undefined, name_tok, null);
                try p.param_buf.append(gpa, .{
                    .name = interned_name,
                    .name_tok = name_tok,
                    .qt = .int,
                    .node = .null,
                });
                if (p.eatToken(.comma) == null) break;
            }
            func_ty.params = p.param_buf.items[param_buf_top..];
        } else {
            try p.err(p.tok_i, .expected_param_decl, .{});
        }

        try p.expectClosing(l_paren, .r_paren);
        func_ty.return_type = try p.directDeclarator(base_declarator, kind);

        // Set after call to `directDeclarator` since we will return
        // a function type from here.
        base_declarator.declarator_type = .func;

        return p.comp.type_store.put(gpa, .{ .func = func_ty });
    } else return base_declarator.qt;
}

/// paramDecls : paramDecl (',' paramDecl)* (',' '...')
/// paramDecl : declSpec (declarator | abstractDeclarator)
fn paramDecls(p: *Parser) Error!?[]Type.Func.Param {
    // TODO warn about visibility of types declared here
    try p.syms.pushScope(p);
    defer p.syms.popScope();

    // Clearing the param buf is handled in directDeclarator.
    const param_buf_top = p.param_buf.items.len;
    const gpa = p.comp.gpa;

    while (true) {
        const attr_buf_top = p.attr_buf.len;
        defer p.attr_buf.len = attr_buf_top;
        const param_decl_spec = if (try p.declSpec()) |some|
            some
        else if (p.comp.langopts.standard.atLeast(.c23) and
            (p.tok_ids[p.tok_i] == .identifier or p.tok_ids[p.tok_i] == .extended_identifier))
        {
            // handle deprecated K&R style parameters
            const identifier = try p.expectIdentifier();
            try p.err(identifier, .unknown_type_name, .{p.tokSlice(identifier)});

            try p.param_buf.append(gpa, .{
                .name = try p.comp.internString(p.tokSlice(identifier)),
                .name_tok = identifier,
                .qt = .int,
                .node = .null,
            });

            if (p.eatToken(.comma) == null) break;
            if (p.tok_ids[p.tok_i] == .ellipsis) break;
            continue;
        } else if (p.param_buf.items.len == param_buf_top) {
            return null;
        } else blk: {
            try p.err(p.tok_i, .missing_type_specifier, .{});
            break :blk DeclSpec{ .qt = .int };
        };

        var name_tok: TokenIndex = 0;
        var interned_name: StringId = .empty;
        const first_tok = p.tok_i;
        var param_qt = param_decl_spec.qt;
        if (param_decl_spec.auto_type) |tok_i| {
            try p.err(tok_i, .auto_type_not_allowed, .{"function prototype"});
            param_qt = .invalid;
        }
        if (param_decl_spec.c23_auto) |tok_i| {
            try p.err(tok_i, .c23_auto_not_allowed, .{"function prototype"});
            param_qt = .invalid;
        }

        if (try p.declarator(param_qt, .param)) |some| {
            if (some.old_style_func) |tok_i| try p.err(tok_i, .invalid_old_style_params, .{});
            try p.attributeSpecifier();
            name_tok = some.name;
            param_qt = some.qt;
        }

        if (param_qt.is(p.comp, .void)) {
            // validate void parameters
            if (p.param_buf.items.len == param_buf_top) {
                if (p.tok_ids[p.tok_i] != .r_paren) {
                    try p.err(p.tok_i, .void_only_param, .{});
                    if (param_qt.isQualified()) try p.err(p.tok_i, .void_param_qualified, .{});
                    return error.ParsingFailed;
                }
                return &.{};
            }
            try p.err(p.tok_i, .void_must_be_first_param, .{});
            return error.ParsingFailed;
        } else {
            // Decay params declared as functions or arrays to pointer.
            param_qt = try param_qt.decay(p.comp);
        }
        try param_decl_spec.validateParam(p);
        param_qt = try Attribute.applyParameterAttributes(p, param_qt, attr_buf_top, .alignas_on_param);

        if (param_qt.get(p.comp, .float)) |float| {
            if (float == .fp16 and !p.comp.hasHalfPrecisionFloatABI()) {
                try p.err(first_tok, .suggest_pointer_for_invalid_fp16, .{"parameters"});
            }
        }

        var param_node: Node.OptIndex = .null;
        if (name_tok != 0) {
            const node = try p.addNode(.{
                .param = .{
                    .name_tok = name_tok,
                    .qt = param_qt,
                    .storage_class = switch (param_decl_spec.storage_class) {
                        .none => .auto,
                        .register => .register,
                        else => .auto, // Error reported in `validateParam`
                    },
                },
            });
            param_node = .pack(node);
            interned_name = try p.comp.internString(p.tokSlice(name_tok));
            try p.syms.defineParam(p, interned_name, param_qt, name_tok, node);
        }

        try p.param_buf.append(gpa, .{
            .name = interned_name,
            .name_tok = if (name_tok == 0) first_tok else name_tok,
            .qt = param_qt,
            .node = param_node,
        });

        if (p.eatToken(.comma) == null) break;
        if (p.tok_ids[p.tok_i] == .ellipsis) break;
    }
    return p.param_buf.items[param_buf_top..];
}

/// typeName : specQual abstractDeclarator
fn typeName(p: *Parser) Error!?QualType {
    const attr_buf_top = p.attr_buf.len;
    defer p.attr_buf.len = attr_buf_top;
    const ty = (try p.specQual()) orelse return null;
    if (try p.declarator(ty, .abstract)) |some| {
        if (some.old_style_func) |tok_i| try p.err(tok_i, .invalid_old_style_params, .{});
        return try Attribute.applyTypeAttributes(p, some.qt, attr_buf_top, .align_ignored);
    }
    return try Attribute.applyTypeAttributes(p, ty, attr_buf_top, .align_ignored);
}

/// initializer
///  : assignExpr
///  | '{' initializerItems '}'
fn initializer(p: *Parser, init_qt: QualType) Error!Result {
    const l_brace = p.eatToken(.l_brace) orelse {
        // fast path for non-braced initializers
        const tok = p.tok_i;
        var res = try p.expect(assignExpr);
        if (try p.coerceArrayInit(res, tok, init_qt)) return res;
        try p.coerceInit(&res, tok, init_qt);
        return res;
    };

    // We want to parse the initializer even if the target is
    // invalidly inferred.
    var final_init_qt = init_qt;
    if (init_qt.isAutoType()) {
        try p.err(l_brace, .auto_type_with_init_list, .{});
        final_init_qt = .invalid;
    } else if (init_qt.isC23Auto()) {
        try p.err(l_brace, .c23_auto_with_init_list, .{});
        final_init_qt = .invalid;
    }

    var il: InitList = .{};
    defer il.deinit(p.comp.gpa);

    try p.initializerItem(&il, final_init_qt, l_brace);

    const list_node = try p.convertInitList(il, final_init_qt);
    return .{
        .qt = list_node.qt(&p.tree).withQualifiers(final_init_qt),
        .node = list_node,
        .val = p.tree.value_map.get(list_node) orelse .{},
    };
}

const IndexList = std.ArrayList(u64);

/// initializerItems : designation? initializer (',' designation? initializer)* ','?
fn initializerItem(p: *Parser, il: *InitList, init_qt: QualType, l_brace: TokenIndex) Error!void {
    const gpa = p.comp.gpa;
    const is_scalar = !init_qt.isInvalid() and init_qt.scalarKind(p.comp) != .none;

    if (p.eatToken(.r_brace)) |_| {
        try p.err(l_brace, .empty_initializer, .{});
        if (il.tok != 0 and !init_qt.isInvalid()) {
            try p.err(l_brace, .initializer_overrides, .{});
            try p.err(il.tok, .previous_initializer, .{});
        }
        il.node = .null;
        il.tok = l_brace;
        return;
    }

    var index_list: IndexList = .empty;
    defer index_list.deinit(gpa);

    var seen_any = false;
    var warned_excess = init_qt.isInvalid();
    while (true) : (seen_any = true) {
        errdefer p.skipTo(.r_brace);

        const designated = try p.designation(il, init_qt, &index_list);
        if (!designated and init_qt.hasAttribute(p.comp, .designated_init)) {
            try p.err(p.tok_i, .designated_init_needed, .{});
        }

        const first_tok = p.tok_i;
        if (p.eatToken(.l_brace)) |inner_l_brace| {
            if (try p.findBracedInitializer(il, init_qt, first_tok, &index_list)) |item| {
                if (item.il.tok != 0 and !init_qt.isInvalid()) {
                    try p.err(first_tok, .initializer_overrides, .{});
                    try p.err(item.il.tok, .previous_initializer, .{});
                    item.il.deinit(gpa);
                    item.il.* = .{};
                }
                try p.initializerItem(item.il, item.qt, inner_l_brace);
            } else {
                // discard further values
                var tmp_il: InitList = .{};
                defer tmp_il.deinit(gpa);
                try p.initializerItem(&tmp_il, .invalid, inner_l_brace);
                if (!warned_excess) try p.err(first_tok, switch (init_qt.base(p.comp).type) {
                    .array => if (il.node != .null and p.isStringInit(init_qt, il.node.unpack().?))
                        .excess_str_init
                    else
                        .excess_array_init,
                    .@"struct" => .excess_struct_init,
                    .@"union" => .excess_union_init,
                    .vector => .excess_vector_init,
                    else => .excess_scalar_init,
                }, .{});

                warned_excess = true;
            }
        } else if (try p.assignExpr()) |res| {
            if (is_scalar and il.node != .null) {
                if (!warned_excess) try p.err(first_tok, .excess_scalar_init, .{});
                warned_excess = true;
            } else {
                _ = try p.findScalarInitializer(il, init_qt, res, first_tok, &warned_excess, &index_list, 0);
            }
        } else if (designated or (seen_any and p.tok_ids[p.tok_i] != .r_brace)) {
            try p.err(p.tok_i, .expected_expr, .{});
        } else break;

        if (p.eatToken(.comma) == null) break;
    }
    try p.expectClosing(l_brace, .r_brace);

    if (il.tok == 0) il.tok = l_brace;
}

fn setInitializer(p: *Parser, il: *InitList, init_qt: QualType, tok: TokenIndex, res: Result) !void {
    var copy = res;

    const arr = try p.coerceArrayInit(copy, tok, init_qt);
    if (!arr) try p.coerceInit(&copy, tok, init_qt);
    if (il.tok != 0 and !init_qt.isInvalid()) {
        try p.err(tok, .initializer_overrides, .{});
        try p.err(il.tok, .previous_initializer, .{});
    }
    il.node = .pack(copy.node);
    il.tok = tok;
}

/// designation : designator+ '='?
/// designator
///  : '[' integerConstExpr ']'
///  | '.' identifier
fn designation(p: *Parser, il: *InitList, init_qt: QualType, index_list: *IndexList) !bool {
    switch (p.tok_ids[p.tok_i]) {
        .l_bracket, .period => index_list.items.len = 0,
        else => return false,
    }
    const gpa = p.comp.gpa;

    var cur_qt = init_qt;
    var cur_il = il;
    while (true) {
        if (p.eatToken(.l_bracket)) |l_bracket| {
            const array_ty = cur_qt.get(p.comp, .array) orelse {
                try p.err(l_bracket, .invalid_array_designator, .{cur_qt});
                return error.ParsingFailed;
            };
            const expr_tok = p.tok_i;
            const index_res = try p.integerConstExpr(.gnu_folding_extension);
            try p.expectClosing(l_bracket, .r_bracket);
            if (cur_qt.isInvalid()) continue;

            if (index_res.val.opt_ref == .none) {
                try p.err(expr_tok, .expected_integer_constant_expr, .{});
                return error.ParsingFailed;
            } else if (index_res.val.compare(.lt, .zero, p.comp)) {
                try p.err(l_bracket + 1, .negative_array_designator, .{index_res});
                return error.ParsingFailed;
            }

            const max_len = switch (array_ty.len) {
                .fixed, .static => |len| len,
                else => std.math.maxInt(u64),
            };
            const index_int = index_res.val.toInt(u64, p.comp) orelse std.math.maxInt(u64);
            if (index_int >= max_len) {
                try p.err(l_bracket + 1, .oob_array_designator, .{index_res});
                return error.ParsingFailed;
            }

            try index_list.append(gpa, index_int);
            cur_il = try cur_il.find(gpa, index_int);
            cur_qt = array_ty.elem;
        } else if (p.eatToken(.period)) |period| {
            const field_tok = try p.expectIdentifier();
            if (cur_qt.isInvalid()) continue;

            const field_str = p.tokSlice(field_tok);
            const target_name = try p.comp.internString(field_str);
            var record_ty = cur_qt.getRecord(p.comp) orelse {
                try p.err(period, .invalid_field_designator, .{cur_qt});
                return error.ParsingFailed;
            };

            var field_index: u32 = 0;
            while (field_index < record_ty.fields.len) {
                const field = record_ty.fields[field_index];
                if (field.name_tok == 0) if (field.qt.getRecord(p.comp)) |field_record_ty| {
                    // Recurse into anonymous field if it has a field by the name.
                    if (!field_record_ty.hasField(p.comp, target_name)) continue;
                    try index_list.append(gpa, field_index);
                    cur_il = try il.find(gpa, field_index);
                    record_ty = field_record_ty;
                    field_index = 0;
                    continue;
                };
                if (field.name == target_name) {
                    cur_qt = field.qt;
                    try index_list.append(gpa, field_index);
                    cur_il = try cur_il.find(gpa, field_index);
                    break;
                }
                field_index += 1;
            } else {
                try p.err(period, .no_such_field_designator, .{field_str});
                return error.ParsingFailed;
            }
        } else break;
    }

    if (p.eatToken(.equal) == null) {
        try p.err(p.tok_i, .gnu_missing_eq_designator, .{});
    }
    return true;
}

/// Returns true if the item was filled.
fn findScalarInitializer(
    p: *Parser,
    il: *InitList,
    qt: QualType,
    res: Result,
    first_tok: TokenIndex,
    warned_excess: *bool,
    index_list: *IndexList,
    index_list_top: u32,
) Error!bool {
    if (qt.isInvalid()) return false;
    const gpa = p.comp.gpa;
    if (index_list.items.len <= index_list_top) try index_list.append(gpa, 0);
    const index = index_list.items[index_list_top];

    switch (qt.base(p.comp).type) {
        .complex => |complex_ty| {
            if (il.node != .null or index >= 2) {
                if (!warned_excess.*) try p.err(first_tok, .excess_scalar_init, .{});
                warned_excess.* = true;
                return true;
            }
            if (res.qt.eql(qt, p.comp) and il.list.items.len == 0) {
                try p.setInitializer(il, qt, first_tok, res);
                return true;
            }

            const elem_il = try il.find(gpa, index);
            if (try p.setInitializerIfEqual(elem_il, complex_ty, first_tok, res) or
                try p.findScalarInitializer(
                    elem_il,
                    complex_ty,
                    res,
                    first_tok,
                    warned_excess,
                    index_list,
                    index_list_top + 1,
                ))
            {
                const new_index = index + 1;
                index_list.items[index_list_top] = new_index;
                index_list.items.len = index_list_top + 1;
                return new_index >= 2;
            }

            return false;
        },
        .vector => |vector_ty| {
            if (il.node != .null or index >= vector_ty.len) {
                if (!warned_excess.*) try p.err(first_tok, .excess_vector_init, .{});
                warned_excess.* = true;
                return true;
            }
            if (il.list.items.len == 0 and (res.qt.eql(qt, p.comp) or
                (res.qt.is(p.comp, .vector) and res.qt.sizeCompare(qt, p.comp) == .eq)))
            {
                try p.setInitializer(il, qt, first_tok, res);
                return true;
            }

            const elem_il = try il.find(gpa, index);
            if (try p.setInitializerIfEqual(elem_il, vector_ty.elem, first_tok, res) or
                try p.findScalarInitializer(
                    elem_il,
                    vector_ty.elem,
                    res,
                    first_tok,
                    warned_excess,
                    index_list,
                    index_list_top + 1,
                ))
            {
                const new_index = index + 1;
                index_list.items[index_list_top] = new_index;
                index_list.items.len = index_list_top + 1;
                return new_index >= vector_ty.len;
            }

            return false;
        },
        .array => |array_ty| {
            const max_len = switch (array_ty.len) {
                .fixed, .static => |len| len,
                else => std.math.maxInt(u64),
            };
            if (max_len == 0) {
                try p.err(first_tok, .empty_aggregate_init_braces, .{});
                return true;
            }

            if (il.node != .null or index >= max_len) {
                if (!warned_excess.*) {
                    if (il.node.unpack()) |some| if (p.isStringInit(qt, some)) {
                        try p.err(first_tok, .excess_str_init, .{});
                        warned_excess.* = true;
                        return true;
                    };
                    try p.err(first_tok, .excess_array_init, .{});
                }
                warned_excess.* = true;
                return true;
            }
            if (il.list.items.len == 0 and p.isStringInit(qt, res.node) and
                try p.coerceArrayInit(res, first_tok, qt))
            {
                try p.setInitializer(il, qt, first_tok, res);
                return true;
            }

            const elem_il = try il.find(gpa, index);
            if (try p.setInitializerIfEqual(elem_il, array_ty.elem, first_tok, res) or
                try p.findScalarInitializer(
                    elem_il,
                    array_ty.elem,
                    res,
                    first_tok,
                    warned_excess,
                    index_list,
                    index_list_top + 1,
                ))
            {
                const new_index = index + 1;
                index_list.items[index_list_top] = new_index;
                index_list.items.len = index_list_top + 1;
                return new_index >= max_len;
            }

            return false;
        },
        .@"struct" => |struct_ty| {
            if (struct_ty.fields.len == 0) {
                try p.err(first_tok, .empty_aggregate_init_braces, .{});
                return true;
            }

            if (il.node != .null or index >= struct_ty.fields.len) {
                if (!warned_excess.*) try p.err(first_tok, .excess_struct_init, .{});
                warned_excess.* = true;
                return true;
            }

            const field = struct_ty.fields[@intCast(index)];
            const field_il = try il.find(gpa, index);
            if (try p.setInitializerIfEqual(field_il, field.qt, first_tok, res) or
                try p.findScalarInitializer(
                    field_il,
                    field.qt,
                    res,
                    first_tok,
                    warned_excess,
                    index_list,
                    index_list_top + 1,
                ))
            {
                const new_index = index + 1;
                index_list.items[index_list_top] = new_index;
                index_list.items.len = index_list_top + 1;
                return new_index >= struct_ty.fields.len;
            }

            return false;
        },
        .@"union" => |union_ty| {
            if (union_ty.fields.len == 0) {
                try p.err(first_tok, .empty_aggregate_init_braces, .{});
                return true;
            }

            if (il.node != .null or il.list.items.len > 1 or
                (il.list.items.len == 1 and il.list.items[0].index != index))
            {
                if (!warned_excess.*) try p.err(first_tok, .excess_union_init, .{});
                warned_excess.* = true;
                return true;
            }

            const field = union_ty.fields[@intCast(index)];
            const field_il = try il.find(gpa, index);
            if (try p.setInitializerIfEqual(field_il, field.qt, first_tok, res) or
                try p.findScalarInitializer(
                    field_il,
                    field.qt,
                    res,
                    first_tok,
                    warned_excess,
                    index_list,
                    index_list_top + 1,
                ))
            {
                const new_index = index + 1;
                index_list.items[index_list_top] = new_index;
                index_list.items.len = index_list_top + 1;
            }

            return true;
        },
        else => {
            try p.setInitializer(il, qt, first_tok, res);
            return true;
        },
    }
}

fn setInitializerIfEqual(p: *Parser, il: *InitList, init_qt: QualType, tok: TokenIndex, res: Result) !bool {
    if (!res.qt.eql(init_qt, p.comp)) return false;
    try p.setInitializer(il, init_qt, tok, res);
    return true;
}

const InitItem = struct { il: *InitList, qt: QualType };

fn findBracedInitializer(
    p: *Parser,
    il: *InitList,
    qt: QualType,
    first_tok: TokenIndex,
    index_list: *IndexList,
) Error!?InitItem {
    if (qt.isInvalid()) {
        if (il.node != .null) return .{ .il = il, .qt = qt };
        return null;
    }
    const gpa = p.comp.gpa;
    if (index_list.items.len == 0) try index_list.append(gpa, 0);
    const index = index_list.items[0];

    switch (qt.base(p.comp).type) {
        .complex => |complex_ty| {
            if (il.node != .null) return null;

            if (index < 2) {
                index_list.items[0] = index + 1;
                index_list.items.len = 1;
                return .{ .il = try il.find(gpa, index), .qt = complex_ty };
            }
        },
        .vector => |vector_ty| {
            if (il.node != .null) return null;

            if (index < vector_ty.len) {
                index_list.items[0] = index + 1;
                index_list.items.len = 1;
                return .{ .il = try il.find(gpa, index), .qt = vector_ty.elem };
            }
        },
        .array => |array_ty| {
            if (il.node != .null) return null;

            const max_len = switch (array_ty.len) {
                .fixed, .static => |len| len,
                else => std.math.maxInt(u64),
            };
            if (index < max_len) {
                index_list.items[0] = index + 1;
                index_list.items.len = 1;
                return .{ .il = try il.find(gpa, index), .qt = array_ty.elem };
            }
        },
        .@"struct" => |struct_ty| {
            if (il.node != .null) return null;

            if (index < struct_ty.fields.len) {
                index_list.items[0] = index + 1;
                index_list.items.len = 1;
                const field_qt = struct_ty.fields[@intCast(index)].qt;
                return .{ .il = try il.find(gpa, index), .qt = field_qt };
            }
        },
        .@"union" => |union_ty| {
            if (il.node != .null) return null;
            if (union_ty.fields.len == 0) return null;

            if (index < union_ty.fields.len) {
                index_list.items[0] = index + 1;
                index_list.items.len = 1;
                const field_qt = union_ty.fields[@intCast(index)].qt;
                return .{ .il = try il.find(gpa, index), .qt = field_qt };
            }
        },
        else => {
            try p.err(first_tok, .too_many_scalar_init_braces, .{});
            if (il.node == .null) return .{ .il = il, .qt = qt };
        },
    }
    return null;
}

fn coerceArrayInit(p: *Parser, item: Result, tok: TokenIndex, target: QualType) !bool {
    if (target.isInvalid()) return false;
    const target_array_ty = target.get(p.comp, .array) orelse return false;

    const is_str_lit = p.nodeIs(item.node, .string_literal_expr);
    const maybe_item_array_ty = item.qt.get(p.comp, .array);
    if (!is_str_lit and (!p.nodeIs(item.node, .compound_literal_expr) or maybe_item_array_ty == null)) {
        try p.err(tok, .array_init_str, .{});
        return true; // do not do further coercion
    }

    const target_elem = target_array_ty.elem;
    const item_elem = maybe_item_array_ty.?.elem;

    const target_int = target_elem.get(p.comp, .int) orelse .int; // not int; string compat checks below will fail by design
    const item_int = item_elem.get(p.comp, .int) orelse .int; // not int; string compat checks below will fail by design

    const compatible = target_elem.eql(item_elem, p.comp) or
        (is_str_lit and item_int == .char and (target_int == .uchar or target_int == .schar)) or
        (is_str_lit and item_int == .uchar and (target_int == .uchar or target_int == .schar or target_int == .char));
    if (!compatible) {
        try p.err(tok, .incompatible_array_init, .{ target, item.qt });
        return true; // do not do further coercion
    }

    if (target_array_ty.len == .fixed) {
        const target_len = target_array_ty.len.fixed;
        const item_len = switch (maybe_item_array_ty.?.len) {
            .fixed, .static => |len| len,
            else => unreachable,
        };

        if (is_str_lit) {
            // the null byte of a string can be dropped
            if (item_len - 1 > target_len) {
                try p.err(tok, .str_init_too_long, .{});
            }
        } else if (item_len > target_len) {
            try p.err(tok, .arr_init_too_long, .{ target, item.qt });
        }
    }
    return true;
}

fn coerceInit(p: *Parser, item: *Result, tok: TokenIndex, target: QualType) !void {
    if (target.isInvalid()) return;

    const node = item.node;
    if (target.isAutoType() or target.isC23Auto()) {
        if (p.getNode(node, .member_access_expr) orelse p.getNode(node, .member_access_ptr_expr)) |access| {
            if (access.isBitFieldWidth(&p.tree) != null) try p.err(tok, .auto_type_from_bitfield, .{});
        }
        try item.lvalConversion(p, tok);
        return;
    }

    try item.coerce(p, target, tok, .init);
    if (item.val.opt_ref == .none) runtime: {
        const diagnostic: Diagnostic = switch (p.init_context) {
            .runtime => break :runtime,
            .constexpr => .constexpr_requires_const,
            .static => break :runtime, // TODO: set this to .non_constant_initializer once we are capable of saving all valid values
        };
        p.init_context = .runtime; // Suppress further "non-constant initializer" errors
        try p.err(tok, diagnostic, .{});
    }
    if (target.@"const" or p.init_context == .constexpr) {
        return item.putValue(p);
    }
    return item.saveValue(p);
}

fn isStringInit(p: *Parser, init_qt: QualType, node: Node.Index) bool {
    const init_array_ty = init_qt.get(p.comp, .array) orelse return false;
    if (!init_array_ty.elem.is(p.comp, .int)) return false;
    return p.nodeIs(node, .string_literal_expr);
}

/// Convert InitList into an AST
fn convertInitList(p: *Parser, il: InitList, init_qt: QualType) Error!Node.Index {
    if (init_qt.isInvalid()) {
        return try p.addNode(.{ .default_init_expr = .{
            .last_tok = p.tok_i,
            .qt = init_qt,
        } });
    }

    if (il.node.unpack()) |some| return some;

    const gpa = p.comp.gpa;
    switch (init_qt.base(p.comp).type) {
        .complex => |complex_ty| {
            if (il.list.items.len == 0) {
                return p.addNode(.{ .default_init_expr = .{
                    .last_tok = p.tok_i - 1,
                    .qt = init_qt,
                } });
            }
            const first = try p.convertInitList(il.list.items[0].list, complex_ty);
            const second = if (il.list.items.len > 1)
                try p.convertInitList(il.list.items[1].list, complex_ty)
            else
                null;

            if (il.list.items.len == 2) {
                try p.err(il.tok, .complex_component_init, .{});
            }

            const node = try p.addNode(.{ .array_init_expr = .{
                .container_qt = init_qt,
                .items = if (second) |some|
                    &.{ first, some }
                else
                    &.{first},
                .l_brace_tok = il.tok,
            } });
            if (!complex_ty.isFloat(p.comp)) return node;

            const first_node = il.list.items[0].list.node.unpack() orelse return node;
            const second_node = if (il.list.items.len > 1) il.list.items[1].list.node else .null;

            const first_val = p.tree.value_map.get(first_node) orelse return node;
            const second_val = if (second_node.unpack()) |some| p.tree.value_map.get(some) orelse return node else Value.zero;
            const complex_val = try Value.intern(p.comp, switch (complex_ty.bitSizeof(p.comp)) {
                32 => .{ .complex = .{ .cf32 = .{ first_val.toFloat(f32, p.comp), second_val.toFloat(f32, p.comp) } } },
                64 => .{ .complex = .{ .cf64 = .{ first_val.toFloat(f64, p.comp), second_val.toFloat(f64, p.comp) } } },
                80 => .{ .complex = .{ .cf80 = .{ first_val.toFloat(f80, p.comp), second_val.toFloat(f80, p.comp) } } },
                128 => .{ .complex = .{ .cf128 = .{ first_val.toFloat(f128, p.comp), second_val.toFloat(f128, p.comp) } } },
                else => unreachable,
            });
            try p.tree.value_map.put(gpa, node, complex_val);
            return node;
        },
        .vector => |vector_ty| {
            const list_buf_top = p.list_buf.items.len;
            defer p.list_buf.items.len = list_buf_top;

            const elem_ty = init_qt.childType(p.comp);

            const max_len = vector_ty.len;
            var start: u64 = 0;
            for (il.list.items) |*init| {
                if (init.index > start) {
                    const elem = try p.addNode(.{
                        .array_filler_expr = .{
                            .last_tok = p.tok_i - 1,
                            .count = init.index - start,
                            .qt = elem_ty,
                        },
                    });
                    try p.list_buf.append(gpa, elem);
                }
                start = init.index + 1;

                const elem = try p.convertInitList(init.list, elem_ty);
                try p.list_buf.append(gpa, elem);
            }

            if (start < max_len) {
                const elem = try p.addNode(.{
                    .array_filler_expr = .{
                        .last_tok = p.tok_i - 1,
                        .count = max_len - start,
                        .qt = elem_ty,
                    },
                });
                try p.list_buf.append(gpa, elem);
            }

            return p.addNode(.{ .array_init_expr = .{
                .l_brace_tok = il.tok,
                .container_qt = init_qt,
                .items = p.list_buf.items[list_buf_top..],
            } });
        },
        .array => |array_ty| {
            const list_buf_top = p.list_buf.items.len;
            defer p.list_buf.items.len = list_buf_top;

            const elem_ty = init_qt.childType(p.comp);

            const max_len = switch (array_ty.len) {
                .fixed, .static => |len| len,
                // vla invalid, reported earlier
                .variable => return try p.addNode(.{ .default_init_expr = .{
                    .last_tok = p.tok_i,
                    .qt = init_qt,
                } }),
                else => std.math.maxInt(u64),
            };
            var start: u64 = 0;
            for (il.list.items) |*init| {
                if (init.index > start) {
                    const elem = try p.addNode(.{
                        .array_filler_expr = .{
                            .last_tok = p.tok_i - 1,
                            .count = init.index - start,
                            .qt = elem_ty,
                        },
                    });
                    try p.list_buf.append(gpa, elem);
                }
                start = init.index + 1;

                const elem = try p.convertInitList(init.list, elem_ty);
                try p.list_buf.append(gpa, elem);
            }

            const max_elems = p.comp.maxArrayBytes() / (@max(1, elem_ty.sizeofOrNull(p.comp) orelse 1));
            if (start > max_elems) {
                try p.err(il.tok, .array_too_large, .{});
                start = max_elems;
            }

            var arr_init_qt = init_qt;
            if (array_ty.len == .incomplete) {
                arr_init_qt = try p.comp.type_store.put(gpa, .{ .array = .{
                    .elem = array_ty.elem,
                    .len = .{ .fixed = start },
                } });
            } else if (start < max_len) {
                const elem = try p.addNode(.{
                    .array_filler_expr = .{
                        .last_tok = p.tok_i - 1,
                        .count = max_len - start,
                        .qt = elem_ty,
                    },
                });
                try p.list_buf.append(gpa, elem);
            }

            return p.addNode(.{ .array_init_expr = .{
                .l_brace_tok = il.tok,
                .container_qt = arr_init_qt,
                .items = p.list_buf.items[list_buf_top..],
            } });
        },
        .@"struct" => |struct_ty| {
            assert(struct_ty.layout != null);
            const list_buf_top = p.list_buf.items.len;
            defer p.list_buf.items.len = list_buf_top;

            var init_index: usize = 0;
            for (struct_ty.fields, 0..) |field, i| {
                if (init_index < il.list.items.len and il.list.items[init_index].index == i) {
                    const item = try p.convertInitList(il.list.items[init_index].list, field.qt);
                    try p.list_buf.append(gpa, item);
                    init_index += 1;
                } else {
                    const item = try p.addNode(.{
                        .default_init_expr = .{
                            .last_tok = il.tok,
                            .qt = field.qt,
                        },
                    });
                    try p.list_buf.append(gpa, item);
                }
            }

            return p.addNode(.{ .struct_init_expr = .{
                .l_brace_tok = il.tok,
                .container_qt = init_qt,
                .items = p.list_buf.items[list_buf_top..],
            } });
        },
        .@"union" => |union_ty| {
            const init_node, const index = if (union_ty.fields.len == 0)
                // do nothing for empty unions
                .{ null, 0 }
            else if (il.list.items.len == 0)
                .{ try p.addNode(.{ .default_init_expr = .{
                    .last_tok = p.tok_i - 1,
                    .qt = init_qt,
                } }), 0 }
            else blk: {
                const init = il.list.items[0];
                const index: u32 = @truncate(init.index);
                const field_qt = union_ty.fields[index].qt;

                break :blk .{ try p.convertInitList(init.list, field_qt), index };
            };
            return p.addNode(.{ .union_init_expr = .{
                .field_index = index,
                .initializer = init_node,
                .l_brace_tok = il.tok,
                .union_qt = init_qt,
            } });
        },
        // initializer target is invalid, reported earlier
        else => return try p.addNode(.{ .default_init_expr = .{
            .last_tok = p.tok_i,
            .qt = init_qt,
        } }),
    }
}

fn msvcAsmStmt(p: *Parser) Error!?Node.Index {
    return p.todo("MSVC assembly statements");
}

/// asmOperand : ('[' IDENTIFIER ']')? asmStr '(' expr ')'
fn asmOperand(p: *Parser, kind: enum { output, input }) Error!Tree.Node.AsmStmt.Operand {
    const name = if (p.eatToken(.l_bracket)) |l_bracket| name: {
        const ident = (try p.eatIdentifier()) orelse {
            try p.err(p.tok_i, .expected_identifier, .{});
            return error.ParsingFailed;
        };
        try p.expectClosing(l_bracket, .r_bracket);
        break :name ident;
    } else 0;

    const constraint = try p.asmStr();

    const l_paren = p.eatToken(.l_paren) orelse {
        try p.err(p.tok_i, .expected_token, .{ p.tok_ids[p.tok_i], Token.Id.l_paren });
        return error.ParsingFailed;
    };
    const maybe_res = try p.expr();
    try p.expectClosing(l_paren, .r_paren);
    var res = try p.expectResult(maybe_res);
    if (kind == .output and !p.tree.isLval(res.node)) {
        try p.err(l_paren + 1, .invalid_asm_output, .{});
    } else if (kind == .input) {
        try res.lvalConversion(p, l_paren + 1);
    }
    return .{
        .name = name,
        .constraint = constraint.node,
        .expr = res.node,
    };
}

/// gnuAsmStmt
///  : asmStr
///  | asmStr ':' asmOperand*
///  | asmStr ':' asmOperand* ':' asmOperand*
///  | asmStr ':' asmOperand* ':' asmOperand* : asmStr? (',' asmStr)*
///  | asmStr ':' asmOperand* ':' asmOperand* : asmStr? (',' asmStr)* : IDENTIFIER (',' IDENTIFIER)*
fn gnuAsmStmt(p: *Parser, quals: Tree.GNUAssemblyQualifiers, asm_tok: TokenIndex, l_paren: TokenIndex) Error!Node.Index {
    const gpa = p.comp.gpa;
    const asm_str = try p.asmStr();
    try p.checkAsmStr(asm_str.val, l_paren);

    if (p.tok_ids[p.tok_i] == .r_paren) {
        if (quals.goto) try p.err(p.tok_i, .expected_token, .{ Tree.Token.Id.r_paren, p.tok_ids[p.tok_i] });

        return try p.addNode(.{
            .asm_stmt = .{
                .asm_tok = asm_tok,
                .asm_str = asm_str.node,
                .outputs = &.{},
                .inputs = &.{},
                .clobbers = &.{},
                .labels = &.{},
                .quals = quals,
            },
        });
    }

    const expected_items = 8; // arbitrarily chosen, most assembly will have fewer than 8 inputs/outputs/constraints/names
    const bytes_needed = expected_items * @sizeOf(Tree.Node.AsmStmt.Operand) + expected_items * 2 * @sizeOf(Node.Index);

    var stack_fallback = std.heap.stackFallback(bytes_needed, gpa);
    const allocator = stack_fallback.get();

    var operands: std.ArrayList(Tree.Node.AsmStmt.Operand) = .empty;
    defer operands.deinit(allocator);
    operands.ensureUnusedCapacity(allocator, expected_items) catch unreachable; //stack allocation already succeeded
    var clobbers: NodeList = .empty;
    defer clobbers.deinit(allocator);
    clobbers.ensureUnusedCapacity(allocator, expected_items) catch unreachable; //stack allocation already succeeded
    var labels: NodeList = .empty;
    defer labels.deinit(allocator);
    labels.ensureUnusedCapacity(allocator, expected_items) catch unreachable; //stack allocation already succeeded

    // Outputs
    var ate_extra_colon = false;
    if (p.eatToken(.colon) orelse p.eatToken(.colon_colon)) |tok_i| {
        ate_extra_colon = p.tok_ids[tok_i] == .colon_colon;
        if (!ate_extra_colon) {
            if (p.tok_ids[p.tok_i].isStringLiteral() or p.tok_ids[p.tok_i] == .l_bracket) {
                while (true) {
                    const operand = try p.asmOperand(.output);
                    try operands.append(allocator, operand);
                    if (p.eatToken(.comma) == null) break;
                }
            }
        }
    }
    const num_outputs = operands.items.len;

    // Inputs
    if (ate_extra_colon or p.tok_ids[p.tok_i] == .colon or p.tok_ids[p.tok_i] == .colon_colon) {
        if (ate_extra_colon) {
            ate_extra_colon = false;
        } else {
            ate_extra_colon = p.tok_ids[p.tok_i] == .colon_colon;
            p.tok_i += 1;
        }
        if (!ate_extra_colon) {
            if (p.tok_ids[p.tok_i].isStringLiteral() or p.tok_ids[p.tok_i] == .l_bracket) {
                while (true) {
                    const operand = try p.asmOperand(.input);
                    try operands.append(allocator, operand);
                    if (p.eatToken(.comma) == null) break;
                }
            }
        }
    }

    // Clobbers
    if (ate_extra_colon or p.tok_ids[p.tok_i] == .colon or p.tok_ids[p.tok_i] == .colon_colon) {
        if (ate_extra_colon) {
            ate_extra_colon = false;
        } else {
            ate_extra_colon = p.tok_ids[p.tok_i] == .colon_colon;
            p.tok_i += 1;
        }
        if (!ate_extra_colon and p.tok_ids[p.tok_i].isStringLiteral()) {
            while (true) {
                const clobber = try p.asmStr();
                try clobbers.append(allocator, clobber.node);
                if (p.eatToken(.comma) == null) break;
            }
        }
    }

    if (!quals.goto and (p.tok_ids[p.tok_i] != .r_paren or ate_extra_colon)) {
        try p.err(p.tok_i, .expected_token, .{ Tree.Token.Id.r_paren, p.tok_ids[p.tok_i] });
        return error.ParsingFailed;
    }

    // Goto labels
    if (ate_extra_colon or p.tok_ids[p.tok_i] == .colon) {
        if (!ate_extra_colon) {
            p.tok_i += 1;
        }
        while (true) {
            const ident = (try p.eatIdentifier()) orelse {
                try p.err(p.tok_i, .expected_identifier, .{});
                return error.ParsingFailed;
            };
            const ident_str = p.tokSlice(ident);
            const label = p.findLabel(ident_str) orelse blk: {
                try p.labels.append(gpa, .{ .unresolved_goto = ident });
                break :blk ident;
            };

            const label_addr_node = try p.addNode(.{
                .addr_of_label = .{
                    .label_tok = label,
                    .qt = .void_pointer,
                },
            });
            try labels.append(allocator, label_addr_node);

            if (p.eatToken(.comma) == null) break;
        }
    } else if (quals.goto) {
        try p.err(p.tok_i, .expected_token, .{ Token.Id.colon, p.tok_ids[p.tok_i] });
        return error.ParsingFailed;
    }

    // TODO: validate
    return p.addNode(.{
        .asm_stmt = .{
            .asm_tok = asm_tok,
            .asm_str = asm_str.node,
            .outputs = operands.items[0..num_outputs],
            .inputs = operands.items[num_outputs..],
            .clobbers = clobbers.items,
            .labels = labels.items,
            .quals = quals,
        },
    });
}

fn checkAsmStr(p: *Parser, asm_str: Value, tok: TokenIndex) !void {
    if (!p.comp.langopts.gnu_asm) {
        const str = p.comp.interner.get(asm_str.ref()).bytes;
        if (str.len > 1) {
            // Empty string (just a NUL byte) is ok because it does not emit any assembly
            try p.err(tok, .gnu_asm_disabled, .{});
        }
    }
}

/// assembly
///  : keyword_asm asmQual* '(' asmStr ')'
///  | keyword_asm asmQual* '(' gnuAsmStmt ')'
///  | keyword_asm msvcAsmStmt
fn assembly(p: *Parser, kind: enum { global, decl_label, stmt }) Error!?Node.Index {
    const asm_tok = p.tok_i;
    switch (p.tok_ids[p.tok_i]) {
        .keyword_asm => {
            try p.err(p.tok_i, .extension_token_used, .{});
            p.tok_i += 1;
        },
        .keyword_asm1, .keyword_asm2 => p.tok_i += 1,
        else => return null,
    }

    if (!p.tok_ids[p.tok_i].canOpenGCCAsmStmt()) {
        return p.msvcAsmStmt();
    }

    var quals: Tree.GNUAssemblyQualifiers = .{};
    while (true) : (p.tok_i += 1) switch (p.tok_ids[p.tok_i]) {
        .keyword_volatile, .keyword_volatile1, .keyword_volatile2 => {
            if (kind != .stmt) try p.err(p.tok_i, .meaningless_asm_qual, .{"volatile"});
            if (quals.@"volatile") try p.err(p.tok_i, .duplicate_asm_qual, .{"volatile"});
            quals.@"volatile" = true;
        },
        .keyword_inline, .keyword_inline1, .keyword_inline2 => {
            if (kind != .stmt) try p.err(p.tok_i, .meaningless_asm_qual, .{"inline"});
            if (quals.@"inline") try p.err(p.tok_i, .duplicate_asm_qual, .{"inline"});
            quals.@"inline" = true;
        },
        .keyword_goto => {
            if (kind != .stmt) try p.err(p.tok_i, .meaningless_asm_qual, .{"goto"});
            if (quals.goto) try p.err(p.tok_i, .duplicate_asm_qual, .{"goto"});
            quals.goto = true;
        },
        else => break,
    };

    const l_paren = try p.expectToken(.l_paren);
    var result_node: ?Node.Index = null;
    switch (kind) {
        .decl_label => {
            const asm_str = try p.asmStr();
            const str = try p.removeNull(asm_str.val);

            const attr = Attribute{ .tag = .asm_label, .args = .{ .asm_label = .{ .name = str } }, .syntax = .keyword };
            try p.attr_buf.append(p.comp.gpa, .{ .attr = attr, .tok = asm_tok });
        },
        .global => {
            const asm_str = try p.asmStr();
            try p.checkAsmStr(asm_str.val, l_paren);
            result_node = try p.addNode(.{
                .global_asm = .{
                    .asm_tok = asm_tok,
                    .asm_str = asm_str.node,
                },
            });
        },
        .stmt => result_node = try p.gnuAsmStmt(quals, asm_tok, l_paren),
    }
    try p.expectClosing(l_paren, .r_paren);

    if (kind != .decl_label) _ = try p.expectToken(.semicolon);
    return result_node;
}

/// Same as stringLiteral but errors on unicode and wide string literals
fn asmStr(p: *Parser) Error!Result {
    var i = p.tok_i;
    while (true) : (i += 1) switch (p.tok_ids[i]) {
        .string_literal, .unterminated_string_literal => {},
        .string_literal_utf_16, .string_literal_utf_8, .string_literal_utf_32 => {
            try p.err(p.tok_i, .invalid_asm_str, .{"unicode"});
            return error.ParsingFailed;
        },
        .string_literal_wide => {
            try p.err(p.tok_i, .invalid_asm_str, .{"wide"});
            return error.ParsingFailed;
        },
        else => {
            if (i == p.tok_i) {
                try p.err(p.tok_i, .expected_str_literal_in, .{"asm"});
                return error.ParsingFailed;
            }
            break;
        },
    };
    return p.stringLiteral();
}

// ====== statements ======

/// stmt
///  : labeledStmt
///  | compoundStmt
///  | keyword_if '(' expr ')' stmt (keyword_else stmt)?
///  | keyword_switch '(' expr ')' stmt
///  | keyword_while '(' expr ')' stmt
///  | keyword_do stmt while '(' expr ')' ';'
///  | keyword_for '(' (decl | expr? ';') expr? ';' expr? ')' stmt
///  | keyword_goto (IDENTIFIER | ('*' expr)) ';'
///  | keyword_continue ';'
///  | keyword_break ';'
///  | keyword_return expr? ';'
///  | assembly ';'
///  | expr? ';'
fn stmt(p: *Parser) Error!Node.Index {
    if (try p.labeledStmt()) |some| return some;
    if (try p.compoundStmt(false, null)) |some| return some;
    const gpa = p.comp.gpa;
    if (p.eatToken(.keyword_if)) |kw_if| {
        const l_paren = try p.expectToken(.l_paren);

        const cond_tok = p.tok_i;
        var cond = try p.expect(expr);
        try cond.lvalConversion(p, cond_tok);
        try cond.usualUnaryConversion(p, cond_tok);
        if (!cond.qt.isInvalid() and cond.qt.scalarKind(p.comp) == .none)
            try p.err(l_paren + 1, .statement_scalar, .{cond.qt});
        try cond.saveValue(p);

        try p.expectClosing(l_paren, .r_paren);

        const then_body = try p.stmt();
        const else_body = if (p.eatToken(.keyword_else)) |_| try p.stmt() else null;

        if (p.nodeIs(then_body, .null_stmt) and else_body == null) {
            const semicolon_tok = then_body.get(&p.tree).null_stmt.semicolon_or_r_brace_tok;
            const locs = p.pp.tokens.items(.loc);
            const if_loc = locs[kw_if];
            const semicolon_loc = locs[semicolon_tok];
            if (if_loc.line == semicolon_loc.line) {
                try p.err(semicolon_tok, .empty_if_body, .{});
                try p.err(semicolon_tok, .empty_if_body_note, .{});
            }
        }

        return p.addNode(.{ .if_stmt = .{
            .if_tok = kw_if,
            .cond = cond.node,
            .then_body = then_body,
            .else_body = else_body,
        } });
    }
    if (p.eatToken(.keyword_switch)) |kw_switch| {
        const l_paren = try p.expectToken(.l_paren);
        const cond_tok = p.tok_i;
        var cond = try p.expect(expr);
        try cond.lvalConversion(p, cond_tok);
        try cond.usualUnaryConversion(p, cond_tok);

        // Switch condition can't be complex.
        if (!cond.qt.isInvalid() and !cond.qt.isRealInt(p.comp)) {
            try p.err(l_paren + 1, .statement_int, .{cond.qt});
        }

        try cond.saveValue(p);
        try p.expectClosing(l_paren, .r_paren);

        const old_switch = p.@"switch";
        var @"switch": Switch = .{
            .qt = cond.qt,
            .comp = p.comp,
        };
        p.@"switch" = &@"switch";
        defer {
            @"switch".ranges.deinit(gpa);
            p.@"switch" = old_switch;
        }

        const body = try p.stmt();

        return p.addNode(.{ .switch_stmt = .{
            .switch_tok = kw_switch,
            .cond = cond.node,
            .body = body,
        } });
    }
    if (p.eatToken(.keyword_while)) |kw_while| {
        const l_paren = try p.expectToken(.l_paren);

        const cond_tok = p.tok_i;
        var cond = try p.expect(expr);
        try cond.lvalConversion(p, cond_tok);
        try cond.usualUnaryConversion(p, cond_tok);
        if (!cond.qt.isInvalid() and cond.qt.scalarKind(p.comp) == .none)
            try p.err(l_paren + 1, .statement_scalar, .{cond.qt});
        try cond.saveValue(p);

        try p.expectClosing(l_paren, .r_paren);

        const body = body: {
            const old_loop = p.in_loop;
            p.in_loop = true;
            defer p.in_loop = old_loop;
            break :body try p.stmt();
        };

        return p.addNode(.{ .while_stmt = .{
            .while_tok = kw_while,
            .cond = cond.node,
            .body = body,
        } });
    }
    if (p.eatToken(.keyword_do)) |kw_do| {
        const body = body: {
            const old_loop = p.in_loop;
            p.in_loop = true;
            defer p.in_loop = old_loop;
            break :body try p.stmt();
        };

        _ = try p.expectToken(.keyword_while);
        const l_paren = try p.expectToken(.l_paren);

        const cond_tok = p.tok_i;
        var cond = try p.expect(expr);
        try cond.lvalConversion(p, cond_tok);
        try cond.usualUnaryConversion(p, cond_tok);

        if (!cond.qt.isInvalid() and cond.qt.scalarKind(p.comp) == .none)
            try p.err(l_paren + 1, .statement_scalar, .{cond.qt});
        try cond.saveValue(p);
        try p.expectClosing(l_paren, .r_paren);

        _ = try p.expectToken(.semicolon);

        return p.addNode(.{ .do_while_stmt = .{
            .do_tok = kw_do,
            .cond = cond.node,
            .body = body,
        } });
    }
    if (p.eatToken(.keyword_for)) |kw_for| {
        try p.syms.pushScope(p);
        defer p.syms.popScope();
        const decl_buf_top = p.decl_buf.items.len;
        defer p.decl_buf.items.len = decl_buf_top;

        const l_paren = try p.expectToken(.l_paren);
        const got_decl = try p.decl();

        // for (init
        const init_start = p.tok_i;
        var prev_total = p.diagnostics.total;
        const init = init: {
            if (got_decl) break :init null;
            var init = (try p.expr()) orelse break :init null;

            try init.saveValue(p);
            try init.maybeWarnUnused(p, init_start, prev_total);
            break :init init.node;
        };
        if (!got_decl) _ = try p.expectToken(.semicolon);

        // for (init; cond
        const cond = cond: {
            const cond_tok = p.tok_i;
            var cond = (try p.expr()) orelse break :cond null;

            try cond.lvalConversion(p, cond_tok);
            try cond.usualUnaryConversion(p, cond_tok);
            if (!cond.qt.isInvalid() and cond.qt.scalarKind(p.comp) == .none)
                try p.err(l_paren + 1, .statement_scalar, .{cond.qt});
            try cond.saveValue(p);
            break :cond cond.node;
        };
        _ = try p.expectToken(.semicolon);

        // for (init; cond; incr
        const incr_start = p.tok_i;
        prev_total = p.diagnostics.total;
        const incr = incr: {
            var incr = (try p.expr()) orelse break :incr null;

            try incr.maybeWarnUnused(p, incr_start, prev_total);
            try incr.saveValue(p);
            break :incr incr.node;
        };
        try p.expectClosing(l_paren, .r_paren);

        const body = body: {
            const old_loop = p.in_loop;
            p.in_loop = true;
            defer p.in_loop = old_loop;
            break :body try p.stmt();
        };

        return p.addNode(.{ .for_stmt = .{
            .for_tok = kw_for,
            .init = if (decl_buf_top == p.decl_buf.items.len)
                .{ .expr = init }
            else
                .{ .decls = p.decl_buf.items[decl_buf_top..] },
            .cond = cond,
            .incr = incr,
            .body = body,
        } });
    }
    if (p.eatToken(.keyword_goto)) |goto_tok| {
        if (p.eatToken(.asterisk)) |_| {
            const expr_tok = p.tok_i;
            var goto_expr = try p.expect(expr);
            try goto_expr.lvalConversion(p, expr_tok);
            p.computed_goto_tok = p.computed_goto_tok orelse goto_tok;

            if (!goto_expr.qt.isInvalid() and !goto_expr.qt.isPointer(p.comp)) {
                const result_qt = try p.comp.type_store.put(gpa, .{ .pointer = .{
                    .child = .{ .@"const" = true, ._index = .void },
                    .decayed = null,
                } });
                if (!goto_expr.qt.isRealInt(p.comp)) {
                    try p.err(expr_tok, .incompatible_arg, .{ goto_expr.qt, result_qt });
                    return error.ParsingFailed;
                }
                if (goto_expr.val.isZero(p.comp)) {
                    try goto_expr.nullToPointer(p, result_qt, expr_tok);
                } else {
                    try p.err(expr_tok, .implicit_int_to_ptr, .{ goto_expr.qt, result_qt });
                    try goto_expr.castToPointer(p, result_qt, expr_tok);
                }
            }

            return p.addNode(.{ .computed_goto_stmt = .{ .goto_tok = goto_tok, .expr = goto_expr.node } });
        }
        const name_tok = try p.expectIdentifier();
        const str = p.tokSlice(name_tok);
        if (p.findLabel(str) == null) {
            try p.labels.append(gpa, .{ .unresolved_goto = name_tok });
        }
        _ = try p.expectToken(.semicolon);
        return p.addNode(.{ .goto_stmt = .{ .label_tok = name_tok } });
    }
    if (p.eatToken(.keyword_continue)) |cont| {
        if (!p.in_loop) try p.err(cont, .continue_not_in_loop, .{});
        _ = try p.expectToken(.semicolon);
        return p.addNode(.{ .continue_stmt = .{ .continue_tok = cont } });
    }
    if (p.eatToken(.keyword_break)) |br| {
        if (!p.in_loop and p.@"switch" == null) try p.err(br, .break_not_in_loop_or_switch, .{});
        _ = try p.expectToken(.semicolon);
        return p.addNode(.{ .break_stmt = .{ .break_tok = br } });
    }
    if (try p.returnStmt()) |some| return some;
    if (try p.assembly(.stmt)) |some| return some;

    const expr_start = p.tok_i;
    const prev_total = p.diagnostics.total;

    if (try p.expr()) |some| {
        _ = try p.expectToken(.semicolon);
        try some.maybeWarnUnused(p, expr_start, prev_total);
        return some.node;
    }

    const attr_buf_top = p.attr_buf.len;
    defer p.attr_buf.len = attr_buf_top;
    try p.attributeSpecifier();

    if (p.eatToken(.semicolon)) |semicolon| {
        return p.addNode(.{ .null_stmt = .{
            .semicolon_or_r_brace_tok = semicolon,
            .qt = try Attribute.applyStatementAttributes(p, expr_start, attr_buf_top),
        } });
    }

    try p.err(p.tok_i, .expected_stmt, .{});
    return error.ParsingFailed;
}

/// labeledStmt
/// : IDENTIFIER ':' stmt
/// | keyword_case integerConstExpr ':' stmt
/// | keyword_default ':' stmt
fn labeledStmt(p: *Parser) Error!?Node.Index {
    if ((p.tok_ids[p.tok_i] == .identifier or p.tok_ids[p.tok_i] == .extended_identifier) and p.tok_ids[p.tok_i + 1] == .colon) {
        const name_tok = try p.expectIdentifier();
        const str = p.tokSlice(name_tok);
        if (p.findLabel(str)) |some| {
            try p.err(name_tok, .duplicate_label, .{str});
            try p.err(some, .previous_label, .{str});
        } else {
            p.label_count += 1;
            try p.labels.append(p.comp.gpa, .{ .label = name_tok });
            var i: usize = 0;
            while (i < p.labels.items.len) {
                if (p.labels.items[i] == .unresolved_goto and
                    mem.eql(u8, p.tokSlice(p.labels.items[i].unresolved_goto), str))
                {
                    _ = p.labels.swapRemove(i);
                } else i += 1;
            }
        }

        p.tok_i += 1;
        const attr_buf_top = p.attr_buf.len;
        defer p.attr_buf.len = attr_buf_top;
        try p.attributeSpecifier();

        return try p.addNode(.{ .labeled_stmt = .{
            .qt = try Attribute.applyLabelAttributes(p, attr_buf_top),
            .body = try p.labelableStmt(),
            .label_tok = name_tok,
        } });
    } else if (p.eatToken(.keyword_case)) |case| {
        var first_item = try p.integerConstExpr(.gnu_folding_extension);
        const ellipsis = p.tok_i;
        var second_item = if (p.eatToken(.ellipsis) != null) blk: {
            try p.err(ellipsis, .gnu_switch_range, .{});
            break :blk try p.integerConstExpr(.gnu_folding_extension);
        } else null;
        _ = try p.expectToken(.colon);

        if (p.@"switch") |@"switch"| check: {
            if (@"switch".qt.hasIncompleteSize(p.comp)) break :check; // error already reported for incomplete size

            // Coerce to switch condition type
            try first_item.coerce(p, @"switch".qt, case + 1, .assign);
            try first_item.putValue(p);
            if (second_item) |*item| {
                try item.coerce(p, @"switch".qt, ellipsis + 1, .assign);
                try item.putValue(p);
            }

            const first = first_item.val;
            const last = if (second_item) |second| second.val else first;
            if (first.opt_ref == .none) {
                try p.err(case + 1, .case_val_unavailable, .{});
                break :check;
            } else if (last.opt_ref == .none) {
                try p.err(ellipsis + 1, .case_val_unavailable, .{});
                break :check;
            } else if (last.compare(.lt, first, p.comp)) {
                try p.err(case + 1, .empty_case_range, .{});
                break :check;
            }

            // TODO cast to target type
            const prev = (try @"switch".add(first, last, case + 1)) orelse break :check;

            // TODO check which value was already handled
            try p.err(case + 1, .duplicate_switch_case, .{first_item});
            try p.err(prev.tok, .previous_case, .{});
        } else {
            try p.err(case, .case_not_in_switch, .{"case"});
        }

        return try p.addNode(.{ .case_stmt = .{
            .case_tok = case,
            .start = first_item.node,
            .end = if (second_item) |some| some.node else null,
            .body = try p.labelableStmt(),
        } });
    } else if (p.eatToken(.keyword_default)) |default| {
        _ = try p.expectToken(.colon);
        const node = try p.addNode(.{ .default_stmt = .{
            .default_tok = default,
            .body = try p.labelableStmt(),
        } });

        const @"switch" = p.@"switch" orelse {
            try p.err(default, .case_not_in_switch, .{"default"});
            return node;
        };
        if (@"switch".default) |previous| {
            try p.err(default, .multiple_default, .{});
            try p.err(previous, .previous_case, .{});
        } else {
            @"switch".default = default;
        }
        return node;
    } else return null;
}

fn labelableStmt(p: *Parser) Error!Node.Index {
    if (p.tok_ids[p.tok_i] == .r_brace) {
        try p.err(p.tok_i, .label_compound_end, .{});
        return p.addNode(.{ .null_stmt = .{
            .semicolon_or_r_brace_tok = p.tok_i,
            .qt = .void,
        } });
    }
    return p.stmt();
}

const StmtExprState = struct {
    last_expr_tok: TokenIndex = 0,
    last_expr_qt: QualType = .void,
};

/// compoundStmt : '{' ( decl | keyword_extension decl | staticAssert | stmt)* '}'
fn compoundStmt(p: *Parser, is_fn_body: bool, stmt_expr_state: ?*StmtExprState) Error!?Node.Index {
    const l_brace = p.eatToken(.l_brace) orelse return null;

    const gpa = p.comp.gpa;
    const decl_buf_top = p.decl_buf.items.len;
    defer p.decl_buf.items.len = decl_buf_top;

    // the parameters of a function are in the same scope as the body
    if (!is_fn_body) try p.syms.pushScope(p);
    defer if (!is_fn_body) p.syms.popScope();

    var noreturn_index: ?TokenIndex = null;
    var noreturn_label_count: u32 = 0;

    while (p.eatToken(.r_brace) == null) : (_ = try p.pragma()) {
        if (stmt_expr_state) |state| state.* = .{};
        if (try p.parseOrNextStmt(staticAssert, l_brace)) continue;
        if (try p.parseOrNextStmt(decl, l_brace)) continue;
        if (p.eatToken(.keyword_extension)) |ext| {
            const saved_extension = p.extension_suppressed;
            defer p.extension_suppressed = saved_extension;
            p.extension_suppressed = true;

            if (try p.parseOrNextStmt(decl, l_brace)) continue;
            p.tok_i = ext;
        }
        const stmt_tok = p.tok_i;
        const s = p.stmt() catch |er| switch (er) {
            error.ParsingFailed => {
                try p.nextStmt(l_brace);
                continue;
            },
            else => |e| return e,
        };
        if (stmt_expr_state) |state| {
            state.* = .{
                .last_expr_tok = stmt_tok,
                .last_expr_qt = s.qt(&p.tree),
            };
        }
        try p.decl_buf.append(gpa, s);

        if (noreturn_index == null and p.nodeIsNoreturn(s) == .yes) {
            noreturn_index = p.tok_i;
            noreturn_label_count = p.label_count;
        }
        switch (s.get(&p.tree)) {
            .case_stmt, .default_stmt, .labeled_stmt => noreturn_index = null,
            else => {},
        }
    }
    const r_brace = p.tok_i - 1;

    if (noreturn_index) |some| {
        // if new labels were defined we cannot be certain that the code is unreachable
        if (some != p.tok_i - 1 and noreturn_label_count == p.label_count) try p.err(some, .unreachable_code, .{});
    }
    if (is_fn_body) {
        const last_noreturn = if (p.decl_buf.items.len == decl_buf_top)
            .no
        else
            p.nodeIsNoreturn(p.decl_buf.items[p.decl_buf.items.len - 1]);

        const ret_qt: QualType = if (p.func.qt.?.get(p.comp, .func)) |func_ty| func_ty.return_type else .invalid;
        if (last_noreturn != .yes and !ret_qt.isInvalid()) {
            var return_zero = false;
            if (last_noreturn == .no) switch (ret_qt.base(p.comp).type) {
                .void => {},
                .func, .array => {}, // Invalid, error reported elsewhere
                else => {
                    const func_name = p.tokSlice(p.func.name);
                    const interned_name = try p.comp.internString(func_name);

                    if (interned_name == p.string_ids.main_id) {
                        if (ret_qt.get(p.comp, .int)) |int_ty| {
                            if (int_ty == .int) return_zero = true;
                        }
                    }

                    if (!return_zero) {
                        try p.err(p.tok_i - 1, .func_does_not_return, .{func_name});
                    }
                },
            };

            const implicit_ret = try p.addNode(.{ .return_stmt = .{
                .return_tok = r_brace,
                .return_qt = ret_qt,
                .operand = .{ .implicit = return_zero },
            } });
            try p.decl_buf.append(gpa, implicit_ret);
        }
        if (p.func.ident) |some| try p.decl_buf.insert(gpa, decl_buf_top, some.node);
        if (p.func.pretty_ident) |some| try p.decl_buf.insert(gpa, decl_buf_top, some.node);
    }

    return try p.addNode(.{ .compound_stmt = .{
        .body = p.decl_buf.items[decl_buf_top..],
        .l_brace_tok = l_brace,
    } });
}

fn pointerValue(p: *Parser, node: Node.Index, offset: Value) !Value {
    switch (node.get(&p.tree)) {
        .decl_ref_expr => |decl_ref| {
            const var_name = try p.comp.internString(p.tokSlice(decl_ref.name_tok));
            const sym = p.syms.findSymbol(var_name) orelse return .{};
            const sym_node = sym.node.unpack() orelse return .{};
            return Value.pointer(.{ .node = @intFromEnum(sym_node), .offset = offset.ref() }, p.comp);
        },
        .string_literal_expr => return p.tree.value_map.get(node).?,
        else => return .{},
    }
}

const NoreturnKind = enum { no, yes, complex };

fn nodeIsNoreturn(p: *Parser, node: Node.Index) NoreturnKind {
    switch (node.get(&p.tree)) {
        .break_stmt, .continue_stmt, .return_stmt => return .yes,
        .if_stmt => |@"if"| {
            const else_type = p.nodeIsNoreturn(@"if".else_body orelse return .no);
            const then_type = p.nodeIsNoreturn(@"if".then_body);
            if (then_type == .complex or else_type == .complex) return .complex;
            if (then_type == .yes and else_type == .yes) return .yes;
            return .no;
        },
        .compound_stmt => |compound| {
            for (compound.body) |body_stmt| {
                const kind = p.nodeIsNoreturn(body_stmt);
                if (kind != .no) return kind;
            }
            return .no;
        },
        .labeled_stmt => |labeled| {
            return p.nodeIsNoreturn(labeled.body);
        },
        .default_stmt => |default| {
            return p.nodeIsNoreturn(default.body);
        },
        .while_stmt, .do_while_stmt, .for_stmt, .switch_stmt => return .complex,
        else => return .no,
    }
}

fn parseOrNextStmt(p: *Parser, comptime func: fn (*Parser) Error!bool, l_brace: TokenIndex) !bool {
    return func(p) catch |er| switch (er) {
        error.ParsingFailed => {
            try p.nextStmt(l_brace);
            return true;
        },
        else => |e| return e,
    };
}

fn nextStmt(p: *Parser, l_brace: TokenIndex) !void {
    var parens: u32 = 0;
    while (p.tok_i < p.tok_ids.len) : (p.tok_i += 1) {
        switch (p.tok_ids[p.tok_i]) {
            .l_paren, .l_brace, .l_bracket => parens += 1,
            .r_paren, .r_bracket => if (parens != 0) {
                parens -= 1;
            },
            .r_brace => if (parens == 0)
                return
            else {
                parens -= 1;
            },
            .semicolon => if (parens == 0) {
                p.tok_i += 1;
                return;
            },
            .keyword_for,
            .keyword_while,
            .keyword_do,
            .keyword_if,
            .keyword_goto,
            .keyword_switch,
            .keyword_case,
            .keyword_default,
            .keyword_continue,
            .keyword_break,
            .keyword_return,
            .keyword_typedef,
            .keyword_extern,
            .keyword_static,
            .keyword_auto,
            .keyword_register,
            .keyword_thread_local,
            .keyword_c23_thread_local,
            .keyword_inline,
            .keyword_inline1,
            .keyword_inline2,
            .keyword_noreturn,
            .keyword_void,
            .keyword_bool,
            .keyword_c23_bool,
            .keyword_char,
            .keyword_short,
            .keyword_int,
            .keyword_long,
            .keyword_signed,
            .keyword_signed1,
            .keyword_signed2,
            .keyword_unsigned,
            .keyword_float,
            .keyword_double,
            .keyword_complex,
            .keyword_atomic,
            .keyword_enum,
            .keyword_struct,
            .keyword_union,
            .keyword_alignas,
            .keyword_c23_alignas,
            .keyword_typeof,
            .keyword_typeof1,
            .keyword_typeof2,
            .keyword_typeof_unqual,
            .keyword_extension,
            => if (parens == 0) return,
            .keyword_pragma => p.skipToPragmaSentinel(),
            else => {},
        }
    }
    p.tok_i -= 1; // So we can consume EOF
    try p.expectClosing(l_brace, .r_brace);
    unreachable;
}

fn returnStmt(p: *Parser) Error!?Node.Index {
    const ret_tok = p.eatToken(.keyword_return) orelse return null;

    const e_tok = p.tok_i;
    var ret_expr = try p.expr();
    _ = try p.expectToken(.semicolon);

    const func_qt = p.func.qt.?; // `return` cannot be parsed outside of a function.
    const ret_qt: QualType = if (func_qt.get(p.comp, .func)) |func_ty| func_ty.return_type else .invalid;
    const ret_void = !ret_qt.isInvalid() and ret_qt.is(p.comp, .void);

    if (func_qt.hasAttribute(p.comp, .noreturn)) {
        try p.err(e_tok, .invalid_noreturn, .{p.tokSlice(p.func.name)});
    }

    if (ret_expr) |*some| {
        if (ret_void) {
            if (!some.qt.is(p.comp, .void)) {
                try p.err(e_tok, .void_func_returns_value, .{p.tokSlice(p.func.name)});
            }
        } else {
            try some.coerce(p, ret_qt, e_tok, .ret);

            try some.saveValue(p);
        }
    } else if (!ret_void) {
        try p.err(ret_tok, .func_should_return, .{p.tokSlice(p.func.name)});
    }

    return try p.addNode(.{ .return_stmt = .{
        .return_tok = ret_tok,
        .operand = if (ret_expr) |some| .{ .expr = some.node } else .none,
        .return_qt = ret_qt,
    } });
}

// ====== expressions ======

pub fn macroExpr(p: *Parser) Compilation.Error!bool {
    const res = p.expect(condExpr) catch |e| switch (e) {
        error.OutOfMemory => return error.OutOfMemory,
        error.FatalError => return error.FatalError,
        error.ParsingFailed => return false,
    };
    return res.val.toBool(p.comp);
}

const CallExpr = union(enum) {
    standard: Node.Index,
    builtin: struct {
        builtin_tok: TokenIndex,
        expanded: Builtins.Expanded,
    },

    fn init(p: *Parser, call_node: Node.Index, func_node: Node.Index) CallExpr {
        if (p.getNode(call_node, .builtin_ref)) |builtin_ref| {
            const name = p.tokSlice(builtin_ref.name_tok);
            const expanded = p.comp.builtins.lookup(name);
            return .{ .builtin = .{ .builtin_tok = builtin_ref.name_tok, .expanded = expanded } };
        }
        return .{ .standard = func_node };
    }

    fn shouldPerformLvalConversion(self: CallExpr, arg_idx: u32) bool {
        return switch (self) {
            .standard => true,
            .builtin => |builtin| switch (builtin.expanded.tag) {
                .common => |tag| switch (tag) {
                    .__builtin_va_start,
                    .__va_start,
                    .va_start,
                    => arg_idx != 1,
                    else => true,
                },
                else => true,
            },
        };
    }

    fn shouldPromoteVarArg(self: CallExpr, arg_idx: u32) bool {
        return switch (self) {
            .standard => true,
            .builtin => |builtin| switch (builtin.expanded.tag) {
                .common => |tag| switch (tag) {
                    .__builtin_va_start,
                    .__va_start,
                    .va_start,
                    => arg_idx != 1,
                    .__builtin_add_overflow,
                    .__builtin_complex,
                    .__builtin_isinf,
                    .__builtin_isinf_sign,
                    .__builtin_mul_overflow,
                    .__builtin_isnan,
                    .__builtin_sub_overflow,
                    => false,
                    else => true,
                },
                else => false,
            },
        };
    }

    fn shouldCoerceArg(self: CallExpr, arg_idx: u32) bool {
        _ = self;
        _ = arg_idx;
        return true;
    }

    fn checkVarArg(self: CallExpr, p: *Parser, first_after: TokenIndex, param_tok: TokenIndex, arg: *Result, arg_idx: u32) !void {
        if (self == .standard) return;

        const builtin_tok = self.builtin.builtin_tok;
        switch (self.builtin.expanded.tag) {
            .common => |tag| switch (tag) {
                .__builtin_va_start,
                .__va_start,
                .va_start,
                => return p.checkVaStartArg(builtin_tok, first_after, param_tok, arg, arg_idx),
                .__builtin_complex => return p.checkComplexArg(builtin_tok, first_after, param_tok, arg, arg_idx),
                .__builtin_add_overflow,
                .__builtin_sub_overflow,
                .__builtin_mul_overflow,
                => return p.checkArithOverflowArg(builtin_tok, first_after, param_tok, arg, arg_idx),

                .__builtin_elementwise_abs,
                => return p.checkElementwiseArg(param_tok, arg, arg_idx, .sint_float),
                .__builtin_elementwise_bitreverse,
                .__builtin_elementwise_add_sat,
                .__builtin_elementwise_sub_sat,
                .__builtin_elementwise_popcount,
                => return p.checkElementwiseArg(param_tok, arg, arg_idx, .int),
                .__builtin_elementwise_canonicalize,
                .__builtin_elementwise_ceil,
                .__builtin_elementwise_cos,
                .__builtin_elementwise_exp,
                .__builtin_elementwise_exp2,
                .__builtin_elementwise_floor,
                .__builtin_elementwise_log,
                .__builtin_elementwise_log10,
                .__builtin_elementwise_log2,
                .__builtin_elementwise_nearbyint,
                .__builtin_elementwise_rint,
                .__builtin_elementwise_round,
                .__builtin_elementwise_roundeven,
                .__builtin_elementwise_sin,
                .__builtin_elementwise_sqrt,
                .__builtin_elementwise_trunc,
                .__builtin_elementwise_copysign,
                .__builtin_elementwise_pow,
                .__builtin_elementwise_fma,
                => return p.checkElementwiseArg(param_tok, arg, arg_idx, .float),
                .__builtin_elementwise_max,
                .__builtin_elementwise_min,
                => return p.checkElementwiseArg(param_tok, arg, arg_idx, .both),

                .__builtin_reduce_add,
                .__builtin_reduce_mul,
                .__builtin_reduce_and,
                .__builtin_reduce_or,
                .__builtin_reduce_xor,
                => return p.checkElementwiseArg(param_tok, arg, arg_idx, .int),
                .__builtin_reduce_max,
                .__builtin_reduce_min,
                => return p.checkElementwiseArg(param_tok, arg, arg_idx, .both),

                .__builtin_nondeterministic_value => return p.checkElementwiseArg(param_tok, arg, arg_idx, .both),
                .__builtin_nontemporal_load => return p.checkNonTemporalArg(param_tok, arg, arg_idx, .load),
                .__builtin_nontemporal_store => return p.checkNonTemporalArg(param_tok, arg, arg_idx, .store),

                .__sync_lock_release => return p.checkSyncArg(param_tok, arg, arg_idx, 1),
                .__sync_fetch_and_add,
                .__sync_fetch_and_and,
                .__sync_fetch_and_nand,
                .__sync_fetch_and_or,
                .__sync_fetch_and_sub,
                .__sync_fetch_and_xor,
                .__sync_add_and_fetch,
                .__sync_and_and_fetch,
                .__sync_nand_and_fetch,
                .__sync_or_and_fetch,
                .__sync_sub_and_fetch,
                .__sync_xor_and_fetch,
                .__sync_swap,
                .__sync_lock_test_and_set,
                => return p.checkSyncArg(param_tok, arg, arg_idx, 2),
                .__sync_bool_compare_and_swap,
                .__sync_val_compare_and_swap,
                => return p.checkSyncArg(param_tok, arg, arg_idx, 3),
                else => {},
            },
            else => {},
        }
    }

    /// Some functions cannot be expressed as standard C prototypes. For example `__builtin_complex` requires
    /// two arguments of the same real floating point type (e.g. two doubles or two floats). These functions are
    /// encoded as varargs functions with custom typechecking. Since varargs functions do not have a fixed number
    /// of arguments, `paramCountOverride` is used to tell us how many arguments we should actually expect to see for
    /// these custom-typechecked functions.
    fn paramCountOverride(self: CallExpr) ?u32 {
        return switch (self) {
            .standard => null,
            .builtin => |builtin| switch (builtin.expanded.tag) {
                .common => |tag| switch (tag) {
                    .__c11_atomic_thread_fence,
                    .__atomic_thread_fence,
                    .__c11_atomic_signal_fence,
                    .__atomic_signal_fence,
                    .__c11_atomic_is_lock_free,
                    .__builtin_isinf,
                    .__builtin_isinf_sign,
                    .__builtin_isnan,
                    .__builtin_elementwise_abs,
                    .__builtin_elementwise_bitreverse,
                    .__builtin_elementwise_canonicalize,
                    .__builtin_elementwise_ceil,
                    .__builtin_elementwise_cos,
                    .__builtin_elementwise_exp,
                    .__builtin_elementwise_exp2,
                    .__builtin_elementwise_floor,
                    .__builtin_elementwise_log,
                    .__builtin_elementwise_log10,
                    .__builtin_elementwise_log2,
                    .__builtin_elementwise_nearbyint,
                    .__builtin_elementwise_rint,
                    .__builtin_elementwise_round,
                    .__builtin_elementwise_roundeven,
                    .__builtin_elementwise_sin,
                    .__builtin_elementwise_sqrt,
                    .__builtin_elementwise_trunc,
                    .__builtin_elementwise_popcount,
                    .__builtin_nontemporal_load,
                    .__builtin_nondeterministic_value,
                    .__builtin_reduce_add,
                    .__builtin_reduce_mul,
                    .__builtin_reduce_and,
                    .__builtin_reduce_or,
                    .__builtin_reduce_xor,
                    .__builtin_reduce_max,
                    .__builtin_reduce_min,
                    => 1,

                    .__builtin_complex,
                    .__c11_atomic_load,
                    .__atomic_load_n,
                    .__c11_atomic_init,
                    .__builtin_elementwise_add_sat,
                    .__builtin_elementwise_copysign,
                    .__builtin_elementwise_max,
                    .__builtin_elementwise_min,
                    .__builtin_elementwise_pow,
                    .__builtin_elementwise_sub_sat,
                    .__builtin_nontemporal_store,
                    => 2,

                    .__c11_atomic_store,
                    .__atomic_store,
                    .__c11_atomic_exchange,
                    .__atomic_exchange,
                    .__c11_atomic_fetch_add,
                    .__c11_atomic_fetch_sub,
                    .__c11_atomic_fetch_or,
                    .__c11_atomic_fetch_xor,
                    .__c11_atomic_fetch_and,
                    .__atomic_fetch_add,
                    .__atomic_fetch_sub,
                    .__atomic_fetch_and,
                    .__atomic_fetch_xor,
                    .__atomic_fetch_or,
                    .__atomic_fetch_nand,
                    .__atomic_add_fetch,
                    .__atomic_sub_fetch,
                    .__atomic_and_fetch,
                    .__atomic_xor_fetch,
                    .__atomic_or_fetch,
                    .__atomic_nand_fetch,
                    .__builtin_add_overflow,
                    .__builtin_sub_overflow,
                    .__builtin_mul_overflow,
                    .__builtin_elementwise_fma,
                    .__atomic_exchange_n,
                    => 3,

                    .__c11_atomic_compare_exchange_strong,
                    .__c11_atomic_compare_exchange_weak,
                    => 5,

                    .__atomic_compare_exchange,
                    .__atomic_compare_exchange_n,
                    => 6,
                    else => null,
                },
                else => null,
            },
        };
    }

    fn returnType(self: CallExpr, p: *Parser, args: []const Node.Index, func_qt: QualType) !QualType {
        if (self == .standard) {
            return if (func_qt.get(p.comp, .func)) |func_ty| func_ty.return_type else .invalid;
        }
        const builtin = self.builtin;
        const func_ty = func_qt.get(p.comp, .func).?;
        return switch (builtin.expanded.tag) {
            .common => |tag| switch (tag) {
                .__c11_atomic_exchange => {
                    if (args.len != 4) return .invalid; // wrong number of arguments; already an error
                    const second_param = args[2];
                    return second_param.qt(&p.tree);
                },
                .__c11_atomic_load => {
                    if (args.len != 3) return .invalid; // wrong number of arguments; already an error
                    const first_param = args[1];
                    const qt = first_param.qt(&p.tree);
                    if (!qt.isPointer(p.comp)) return .invalid;
                    return qt.childType(p.comp);
                },

                .__atomic_fetch_add,
                .__atomic_add_fetch,
                .__c11_atomic_fetch_add,

                .__atomic_fetch_sub,
                .__atomic_sub_fetch,
                .__c11_atomic_fetch_sub,

                .__atomic_fetch_and,
                .__atomic_and_fetch,
                .__c11_atomic_fetch_and,

                .__atomic_fetch_xor,
                .__atomic_xor_fetch,
                .__c11_atomic_fetch_xor,

                .__atomic_fetch_or,
                .__atomic_or_fetch,
                .__c11_atomic_fetch_or,

                .__atomic_fetch_nand,
                .__atomic_nand_fetch,
                .__c11_atomic_fetch_nand,

                .__atomic_exchange_n,
                => {
                    if (args.len != 3) return .invalid; // wrong number of arguments; already an error
                    const second_param = args[2];
                    return second_param.qt(&p.tree);
                },
                .__builtin_complex => {
                    if (args.len < 1) return .invalid; // not enough arguments; already an error
                    const last_param = args[args.len - 1];
                    return try last_param.qt(&p.tree).toComplex(p.comp);
                },
                .__atomic_compare_exchange,
                .__atomic_compare_exchange_n,
                .__c11_atomic_is_lock_free,
                .__sync_bool_compare_and_swap,
                => .bool,

                .__c11_atomic_compare_exchange_strong,
                .__c11_atomic_compare_exchange_weak,
                => {
                    if (args.len != 6) return .invalid; // wrong number of arguments
                    const third_param = args[3];
                    return third_param.qt(&p.tree);
                },

                .__builtin_elementwise_abs,
                .__builtin_elementwise_bitreverse,
                .__builtin_elementwise_canonicalize,
                .__builtin_elementwise_ceil,
                .__builtin_elementwise_cos,
                .__builtin_elementwise_exp,
                .__builtin_elementwise_exp2,
                .__builtin_elementwise_floor,
                .__builtin_elementwise_log,
                .__builtin_elementwise_log10,
                .__builtin_elementwise_log2,
                .__builtin_elementwise_nearbyint,
                .__builtin_elementwise_rint,
                .__builtin_elementwise_round,
                .__builtin_elementwise_roundeven,
                .__builtin_elementwise_sin,
                .__builtin_elementwise_sqrt,
                .__builtin_elementwise_trunc,
                .__builtin_elementwise_add_sat,
                .__builtin_elementwise_copysign,
                .__builtin_elementwise_max,
                .__builtin_elementwise_min,
                .__builtin_elementwise_pow,
                .__builtin_elementwise_sub_sat,
                .__builtin_elementwise_fma,
                .__builtin_elementwise_popcount,
                .__builtin_nondeterministic_value,
                => {
                    if (args.len < 1) return .invalid; // not enough arguments; already an error
                    const last_param = args[args.len - 1];
                    return last_param.qt(&p.tree);
                },
                .__builtin_nontemporal_load,
                .__builtin_reduce_add,
                .__builtin_reduce_mul,
                .__builtin_reduce_and,
                .__builtin_reduce_or,
                .__builtin_reduce_xor,
                .__builtin_reduce_max,
                .__builtin_reduce_min,
                => {
                    if (args.len < 1) return .invalid; // not enough arguments; already an error
                    const last_param = args[args.len - 1];
                    return last_param.qt(&p.tree).childType(p.comp);
                },
                .__sync_add_and_fetch,
                .__sync_and_and_fetch,
                .__sync_fetch_and_add,
                .__sync_fetch_and_and,
                .__sync_fetch_and_nand,
                .__sync_fetch_and_or,
                .__sync_fetch_and_sub,
                .__sync_fetch_and_xor,
                .__sync_lock_test_and_set,
                .__sync_nand_and_fetch,
                .__sync_or_and_fetch,
                .__sync_sub_and_fetch,
                .__sync_swap,
                .__sync_xor_and_fetch,
                .__sync_val_compare_and_swap,
                .__atomic_load_n,
                => {
                    if (args.len < 1) return .invalid; // not enough arguments; already an error
                    const first_param = args[0];
                    return first_param.qt(&p.tree).childType(p.comp);
                },
                else => func_ty.return_type,
            },
            else => func_ty.return_type,
        };
    }

    fn finish(self: CallExpr, p: *Parser, func_qt: QualType, list_buf_top: usize, l_paren: TokenIndex) Error!Result {
        const args = p.list_buf.items[list_buf_top..];
        const return_qt = try self.returnType(p, args, func_qt);
        switch (self) {
            .standard => |func_node| return .{
                .qt = return_qt,
                .node = try p.addNode(.{ .call_expr = .{
                    .l_paren_tok = l_paren,
                    .qt = return_qt.unqualified(),
                    .callee = func_node,
                    .args = args,
                } }),
            },
            .builtin => |builtin| return .{
                .val = try evalBuiltin(builtin.expanded, p, args),
                .qt = return_qt,
                .node = try p.addNode(.{ .builtin_call_expr = .{
                    .builtin_tok = builtin.builtin_tok,
                    .qt = return_qt,
                    .args = args,
                } }),
            },
        }
    }
};

pub const Result = struct {
    node: Node.Index,
    qt: QualType = .int,
    val: Value = .{},

    fn maybeWarnUnused(res: Result, p: *Parser, expr_start: TokenIndex, prev_total: usize) Error!void {
        if (res.qt.is(p.comp, .void)) return;
        if (res.qt.isInvalid()) return;
        // // don't warn about unused result if the expression contained errors besides other unused results
        if (p.diagnostics.total != prev_total) return; // TODO improve
        // for (p.diagnostics.list.items[err_start..]) |err_item| {
        //     if (err_item.tag != .unused_value) return;
        // }
        loop: switch (res.node.get(&p.tree)) {
            .assign_expr,
            .mul_assign_expr,
            .div_assign_expr,
            .mod_assign_expr,
            .add_assign_expr,
            .sub_assign_expr,
            .shl_assign_expr,
            .shr_assign_expr,
            .bit_and_assign_expr,
            .bit_xor_assign_expr,
            .bit_or_assign_expr,
            .pre_inc_expr,
            .pre_dec_expr,
            .post_inc_expr,
            .post_dec_expr,
            => {},
            .call_expr => |call| {
                const call_info = p.tree.callableResultUsage(call.callee) orelse return;
                if (call_info.nodiscard) try p.err(expr_start, .nodiscard_unused, .{p.tokSlice(call_info.tok)});
                if (call_info.warn_unused_result) try p.err(expr_start, .warn_unused_result, .{p.tokSlice(call_info.tok)});
            },
            .builtin_call_expr => |call| {
                const expanded = p.comp.builtins.lookup(p.tokSlice(call.builtin_tok));
                const attributes = expanded.attributes;
                if (attributes.pure) try p.err(call.builtin_tok, .builtin_unused, .{"pure"});
                if (attributes.@"const") try p.err(call.builtin_tok, .builtin_unused, .{"const"});
            },
            .stmt_expr => |stmt_expr| {
                const compound = stmt_expr.operand.get(&p.tree).compound_stmt;
                continue :loop compound.body[compound.body.len - 1].get(&p.tree);
            },
            .comma_expr => |comma| continue :loop comma.rhs.get(&p.tree),
            .paren_expr => |grouped| continue :loop grouped.operand.get(&p.tree),
            else => try p.err(expr_start, .unused_value, .{}),
        }
    }

    fn boolRes(lhs: *Result, p: *Parser, tag: std.meta.Tag(Node), rhs: Result, tok_i: TokenIndex) !void {
        if (lhs.val.opt_ref == .null) {
            lhs.val = .zero;
        }
        if (!lhs.qt.isInvalid()) {
            if (lhs.qt.get(p.comp, .vector)) |vec| {
                if (!vec.elem.isInt(p.comp)) {
                    lhs.qt = try p.comp.type_store.put(p.comp.gpa, .{
                        .vector = .{ .elem = .int, .len = vec.len },
                    });
                }
            } else {
                lhs.qt = .int;
            }
        }
        return lhs.bin(p, tag, rhs, tok_i);
    }

    fn bin(lhs: *Result, p: *Parser, rt_tag: std.meta.Tag(Node), rhs: Result, tok_i: TokenIndex) !void {
        const bin_data: Node.Binary = .{
            .op_tok = tok_i,
            .lhs = lhs.node,
            .rhs = rhs.node,
            .qt = lhs.qt,
        };
        switch (rt_tag) {
            // zig fmt: off
            inline .comma_expr, .assign_expr, .mul_assign_expr, .div_assign_expr,
            .mod_assign_expr, .add_assign_expr, .sub_assign_expr, .shl_assign_expr,
            .shr_assign_expr, .bit_and_assign_expr, .bit_xor_assign_expr,
            .bit_or_assign_expr, .bool_or_expr, .bool_and_expr, .bit_or_expr,
            .bit_xor_expr, .bit_and_expr, .equal_expr, .not_equal_expr,
            .less_than_expr, .less_than_equal_expr, .greater_than_expr,
            .greater_than_equal_expr, .shl_expr, .shr_expr, .add_expr,
            .sub_expr, .mul_expr, .div_expr, .mod_expr,
            // zig fmt: on
            => |tag| lhs.node = try p.addNode(@unionInit(Node, @tagName(tag), bin_data)),
            else => unreachable,
        }
    }

    fn un(operand: *Result, p: *Parser, rt_tag: std.meta.Tag(Node), tok_i: TokenIndex) Error!void {
        const un_data: Node.Unary = .{
            .op_tok = tok_i,
            .operand = operand.node,
            .qt = operand.qt,
        };
        switch (rt_tag) {
            // zig fmt: off
            inline .addr_of_expr, .deref_expr, .plus_expr, .negate_expr,
            .bit_not_expr, .bool_not_expr, .pre_inc_expr, .pre_dec_expr,
            .imag_expr, .real_expr, .post_inc_expr,.post_dec_expr,
            .paren_expr, .stmt_expr, .imaginary_literal, .compound_assign_dummy_expr,
            // zig fmt: on
            => |tag| operand.node = try p.addNode(@unionInit(Node, @tagName(tag), un_data)),
            else => unreachable,
        }
    }

    fn implicitCast(operand: *Result, p: *Parser, kind: Node.Cast.Kind, tok: TokenIndex) Error!void {
        operand.node = try p.addNode(.{
            .cast = .{
                .l_paren = tok,
                .kind = kind,
                .operand = operand.node,
                .qt = operand.qt,
                .implicit = true,
            },
        });
    }

    fn adjustCondExprPtrs(a: *Result, tok: TokenIndex, b: *Result, p: *Parser) !bool {
        assert(a.qt.isPointer(p.comp) and b.qt.isPointer(p.comp));
        const gpa = p.comp.gpa;

        const a_elem = a.qt.childType(p.comp);
        const b_elem = b.qt.childType(p.comp);
        if (a_elem.eqlQualified(b_elem, p.comp)) return true;

        const has_void_pointer_branch = a.qt.scalarKind(p.comp) == .void_pointer or
            b.qt.scalarKind(p.comp) == .void_pointer;

        const only_quals_differ = a_elem.eql(b_elem, p.comp);
        const pointers_compatible = only_quals_differ or has_void_pointer_branch;

        var adjusted_elem_qt = a_elem;
        if (!pointers_compatible or has_void_pointer_branch) {
            if (!pointers_compatible) {
                try p.err(tok, .pointer_mismatch, .{ a.qt, b.qt });
            }
            adjusted_elem_qt = .void;
        }

        if (pointers_compatible) {
            adjusted_elem_qt.@"const" = a_elem.@"const" or b_elem.@"const";
            adjusted_elem_qt.@"volatile" = a_elem.@"volatile" or b_elem.@"volatile";
            // TODO restrict?
        }

        if (!adjusted_elem_qt.eqlQualified(a_elem, p.comp)) {
            a.qt = try p.comp.type_store.put(gpa, .{ .pointer = .{
                .child = adjusted_elem_qt,
                .decayed = null,
            } });
            try a.implicitCast(p, .bitcast, tok);
        }
        if (!adjusted_elem_qt.eqlQualified(b_elem, p.comp)) {
            b.qt = try p.comp.type_store.put(gpa, .{ .pointer = .{
                .child = adjusted_elem_qt,
                .decayed = null,
            } });
            try b.implicitCast(p, .bitcast, tok);
        }

        return true;
    }

    /// Adjust types for binary operation, returns true if the result can and should be evaluated.
    fn adjustTypes(a: *Result, tok: TokenIndex, b: *Result, p: *Parser, kind: enum {
        integer,
        arithmetic,
        boolean_logic,
        relational,
        equality,
        conditional,
        add,
        sub,
    }) !bool {
        if (b.qt.isInvalid()) {
            try a.saveValue(p);
            a.qt = .invalid;
        }
        if (a.qt.isInvalid()) {
            return false;
        }
        try a.lvalConversion(p, tok);
        try b.lvalConversion(p, tok);

        const a_vec = a.qt.is(p.comp, .vector);
        const b_vec = b.qt.is(p.comp, .vector);
        if (a_vec and b_vec) {
            if (kind == .boolean_logic) {
                return a.invalidBinTy(tok, b, p);
            }
            if (a.qt.eql(b.qt, p.comp)) {
                return a.shouldEval(b, p);
            }
            if (a.qt.sizeCompare(b.qt, p.comp) == .eq) {
                b.qt = a.qt;
                try b.implicitCast(p, .bitcast, tok);
                return a.shouldEval(b, p);
            }
            try p.err(tok, .incompatible_vec_types, .{ a.qt, b.qt });
            a.val = .{};
            b.val = .{};
            a.qt = .invalid;
            return false;
        } else if (a_vec) {
            if (b.coerceExtra(p, a.qt.childType(p.comp), tok, .test_coerce)) {
                try b.saveValue(p);
                b.qt = a.qt;
                try b.implicitCast(p, .vector_splat, tok);
                return a.shouldEval(b, p);
            } else |er| switch (er) {
                error.CoercionFailed => return a.invalidBinTy(tok, b, p),
                else => |e| return e,
            }
        } else if (b_vec) {
            if (a.coerceExtra(p, b.qt.childType(p.comp), tok, .test_coerce)) {
                try a.saveValue(p);
                a.qt = b.qt;
                try a.implicitCast(p, .vector_splat, tok);
                return a.shouldEval(b, p);
            } else |er| switch (er) {
                error.CoercionFailed => return a.invalidBinTy(tok, b, p),
                else => |e| return e,
            }
        }

        const a_sk = a.qt.scalarKind(p.comp);
        const b_sk = b.qt.scalarKind(p.comp);

        if (a_sk.isInt() and b_sk.isInt()) {
            try a.usualArithmeticConversion(b, p, tok);
            return a.shouldEval(b, p);
        }
        if (kind == .integer) return a.invalidBinTy(tok, b, p);

        if (a_sk.isArithmetic() and b_sk.isArithmetic()) {
            // <, <=, >, >= only work on real types
            if (kind == .relational and (!a_sk.isReal() or !b_sk.isReal()))
                return a.invalidBinTy(tok, b, p);

            try a.usualArithmeticConversion(b, p, tok);
            return a.shouldEval(b, p);
        }
        if (kind == .arithmetic) return a.invalidBinTy(tok, b, p);

        switch (kind) {
            .boolean_logic => {
                if (!(a_sk != .none or a_sk == .nullptr_t) or
                    !(b_sk != .none or b_sk == .nullptr_t))
                {
                    return a.invalidBinTy(tok, b, p);
                }

                // Do integer promotions but nothing else
                if (a_sk.isInt()) try a.castToInt(p, a.qt.promoteInt(p.comp), tok);
                if (b_sk.isInt()) try b.castToInt(p, b.qt.promoteInt(p.comp), tok);
                return a.shouldEval(b, p);
            },
            .relational, .equality => {
                if (kind == .equality and (a_sk == .nullptr_t or b_sk == .nullptr_t)) {
                    if (a_sk == .nullptr_t and b_sk == .nullptr_t) return a.shouldEval(b, p);

                    const nullptr_res = if (a_sk == .nullptr_t) a else b;
                    const other_res = if (a_sk == .nullptr_t) b else a;

                    if (other_res.qt.isPointer(p.comp)) {
                        try nullptr_res.nullToPointer(p, other_res.qt, tok);
                        return other_res.shouldEval(nullptr_res, p);
                    } else if (other_res.val.isZero(p.comp)) {
                        other_res.val = .null;
                        try other_res.nullToPointer(p, nullptr_res.qt, tok);
                        return other_res.shouldEval(nullptr_res, p);
                    }
                    return a.invalidBinTy(tok, b, p);
                }

                // comparisons between floats and pointes not allowed
                if (a_sk == .none or b_sk == .none or (a_sk.isFloat() and b_sk.isPointer()) or (b_sk.isFloat() and a_sk.isPointer()))
                    return a.invalidBinTy(tok, b, p);
                if (a_sk == .nullptr_t or b_sk == .nullptr_t) return a.invalidBinTy(tok, b, p);

                if ((a_sk.isInt() or b_sk.isInt()) and !(a.val.isZero(p.comp) or b.val.isZero(p.comp))) {
                    try p.err(tok, .comparison_ptr_int, .{ a.qt, b.qt });
                } else if (a_sk.isPointer() and b_sk.isPointer()) {
                    if (a_sk != .void_pointer and b_sk != .void_pointer) {
                        const a_elem = a.qt.childType(p.comp);
                        const b_elem = b.qt.childType(p.comp);
                        if (!a_elem.eql(b_elem, p.comp)) {
                            try p.err(tok, .comparison_distinct_ptr, .{ a.qt, b.qt });
                            try b.castToPointer(p, a.qt, tok);
                        }
                    } else if (a_sk == .void_pointer) {
                        try b.castToPointer(p, a.qt, tok);
                    } else if (b_sk == .void_pointer) {
                        try a.castToPointer(p, b.qt, tok);
                    }
                } else if (a_sk.isPointer()) {
                    try b.castToPointer(p, a.qt, tok);
                } else {
                    assert(b_sk.isPointer());
                    try a.castToPointer(p, b.qt, tok);
                }

                return a.shouldEval(b, p);
            },
            .conditional => {
                // doesn't matter what we return here, as the result is ignored
                if (a.qt.is(p.comp, .void) or b.qt.is(p.comp, .void)) {
                    try a.castToVoid(p, tok);
                    try b.castToVoid(p, tok);
                    return true;
                }

                if (a_sk == .nullptr_t and b_sk == .nullptr_t) return true;

                if ((a_sk.isPointer() and b_sk.isInt()) or (a_sk.isInt() and b_sk.isPointer())) {
                    if (a.val.isZero(p.comp) or b.val.isZero(p.comp)) {
                        try a.nullToPointer(p, b.qt, tok);
                        try b.nullToPointer(p, a.qt, tok);
                        return true;
                    }
                    const int_ty = if (a_sk.isInt()) a else b;
                    const ptr_ty = if (a_sk.isPointer()) a else b;
                    try p.err(tok, .implicit_int_to_ptr, .{ int_ty.qt, ptr_ty.qt });
                    try int_ty.castToPointer(p, ptr_ty.qt, tok);

                    return true;
                }
                if ((a_sk.isPointer() and b_sk == .nullptr_t) or (a_sk == .nullptr_t and b_sk.isPointer())) {
                    const nullptr_res = if (a_sk == .nullptr_t) a else b;
                    const ptr_res = if (a_sk == .nullptr_t) b else a;
                    try nullptr_res.nullToPointer(p, ptr_res.qt, tok);
                    return true;
                }
                if (a_sk.isPointer() and b_sk.isPointer()) return a.adjustCondExprPtrs(tok, b, p);

                if (a.qt.getRecord(p.comp) != null and b.qt.getRecord(p.comp) != null and a.qt.eql(b.qt, p.comp)) {
                    return true;
                }
                return a.invalidBinTy(tok, b, p);
            },
            .add => {
                // if both aren't arithmetic one should be pointer and the other an integer
                if (a_sk.isPointer() == b_sk.isPointer() or a_sk.isInt() == b_sk.isInt()) return a.invalidBinTy(tok, b, p);

                if (a_sk == .void_pointer or b_sk == .void_pointer)
                    try p.err(tok, .gnu_pointer_arith, .{});

                if (a_sk == .nullptr_t) try a.nullToPointer(p, .void_pointer, tok);
                if (b_sk == .nullptr_t) try b.nullToPointer(p, .void_pointer, tok);

                // Do integer promotions but nothing else
                if (a_sk.isInt()) try a.castToInt(p, a.qt.promoteInt(p.comp), tok);
                if (b_sk.isInt()) try b.castToInt(p, b.qt.promoteInt(p.comp), tok);

                // The result type is the type of the pointer operand
                if (a_sk.isInt()) a.qt = b.qt else b.qt = a.qt;
                return a.shouldEval(b, p);
            },
            .sub => {
                // if both aren't arithmetic then either both should be pointers or just the left one.
                if (!a_sk.isPointer() or !(b_sk.isPointer() or b_sk.isInt())) return a.invalidBinTy(tok, b, p);

                if (a_sk == .void_pointer)
                    try p.err(tok, .gnu_pointer_arith, .{});

                if (a_sk == .nullptr_t) try a.nullToPointer(p, .void_pointer, tok);
                if (b_sk == .nullptr_t) try b.nullToPointer(p, .void_pointer, tok);

                if (a_sk.isPointer() and b_sk.isPointer()) {
                    const a_child_qt = a.qt.get(p.comp, .pointer).?.child;
                    const b_child_qt = b.qt.get(p.comp, .pointer).?.child;

                    if (!a_child_qt.eql(b_child_qt, p.comp)) try p.err(tok, .incompatible_pointers, .{ a.qt, b.qt });
                    if (a.qt.childType(p.comp).sizeofOrNull(p.comp) orelse 1 == 0) try p.err(tok, .subtract_pointers_zero_elem_size, .{a.qt.childType(p.comp)});
                    a.qt = p.comp.type_store.ptrdiff;
                }

                // Do integer promotion on b if needed
                if (b_sk.isInt()) try b.castToInt(p, b.qt.promoteInt(p.comp), tok);
                return a.shouldEval(b, p);
            },
            else => return a.invalidBinTy(tok, b, p),
        }
    }

    fn lvalConversion(res: *Result, p: *Parser, tok: TokenIndex) Error!void {
        if (res.qt.is(p.comp, .func)) {
            res.val = try p.pointerValue(res.node, .zero);

            res.qt = try res.qt.decay(p.comp);
            try res.implicitCast(p, .function_to_pointer, tok);
        } else if (res.qt.is(p.comp, .array)) {
            res.val = try p.pointerValue(res.node, .zero);

            res.qt = try res.qt.decay(p.comp);
            try res.implicitCast(p, .array_to_pointer, tok);
        } else if (!p.in_macro and p.tree.isLval(res.node)) {
            res.qt = res.qt.unqualified();
            try res.implicitCast(p, .lval_to_rval, tok);
        }
    }

    fn castToBool(res: *Result, p: *Parser, bool_qt: QualType, tok: TokenIndex) Error!void {
        if (res.qt.isInvalid()) return;
        std.debug.assert(!bool_qt.isInvalid());

        const src_sk = res.qt.scalarKind(p.comp);
        if (res.qt.is(p.comp, .array)) {
            if (res.val.is(.bytes, p.comp)) {
                try p.err(tok, .string_literal_to_bool, .{ res.qt, bool_qt });
            } else {
                try p.err(tok, .array_address_to_bool, .{p.tokSlice(tok)});
            }
            try res.lvalConversion(p, tok);
            res.val = .one;
            res.qt = bool_qt;
            try res.implicitCast(p, .pointer_to_bool, tok);
        } else if (src_sk.isPointer()) {
            res.val.boolCast(p.comp);
            res.qt = bool_qt;
            try res.implicitCast(p, .pointer_to_bool, tok);
        } else if (src_sk.isInt() and src_sk != .bool) {
            res.val.boolCast(p.comp);
            if (!src_sk.isReal()) {
                res.qt = res.qt.toReal(p.comp);
                try res.implicitCast(p, .complex_int_to_real, tok);
            }
            res.qt = bool_qt;
            try res.implicitCast(p, .int_to_bool, tok);
        } else if (src_sk.isFloat()) {
            const old_val = res.val;
            const value_change_kind = try res.val.floatToInt(bool_qt, p.comp);
            try res.floatToIntWarning(p, bool_qt, old_val, value_change_kind, tok);
            if (!src_sk.isReal()) {
                res.qt = res.qt.toReal(p.comp);
                try res.implicitCast(p, .complex_float_to_real, tok);
            }
            res.qt = bool_qt;
            try res.implicitCast(p, .float_to_bool, tok);
        }
    }

    fn castToInt(res: *Result, p: *Parser, int_qt: QualType, tok: TokenIndex) Error!void {
        if (res.qt.isInvalid()) return;
        std.debug.assert(!int_qt.isInvalid());
        if (int_qt.hasIncompleteSize(p.comp)) {
            return error.ParsingFailed; // Cast to incomplete enum, diagnostic already issued
        }

        const src_sk = res.qt.scalarKind(p.comp);
        const dest_sk = int_qt.scalarKind(p.comp);

        if (src_sk == .bool) {
            res.qt = int_qt.toReal(p.comp);
            try res.implicitCast(p, .bool_to_int, tok);
            if (!dest_sk.isReal()) {
                res.qt = int_qt;
                try res.implicitCast(p, .real_to_complex_int, tok);
            }
        } else if (src_sk.isPointer()) {
            res.val = .{};
            res.qt = int_qt.toReal(p.comp);
            try res.implicitCast(p, .pointer_to_int, tok);
            if (!dest_sk.isReal()) {
                res.qt = int_qt;
                try res.implicitCast(p, .real_to_complex_int, tok);
            }
        } else if (res.qt.isFloat(p.comp)) {
            const old_val = res.val;
            const value_change_kind = try res.val.floatToInt(int_qt, p.comp);
            try res.floatToIntWarning(p, int_qt, old_val, value_change_kind, tok);
            if (src_sk.isReal() and dest_sk.isReal()) {
                res.qt = int_qt;
                try res.implicitCast(p, .float_to_int, tok);
            } else if (src_sk.isReal()) {
                res.qt = int_qt.toReal(p.comp);
                try res.implicitCast(p, .float_to_int, tok);
                res.qt = int_qt;
                try res.implicitCast(p, .real_to_complex_int, tok);
            } else if (dest_sk.isReal()) {
                res.qt = res.qt.toReal(p.comp);
                try res.implicitCast(p, .complex_float_to_real, tok);
                res.qt = int_qt;
                try res.implicitCast(p, .float_to_int, tok);
            } else {
                res.qt = int_qt;
                try res.implicitCast(p, .complex_float_to_complex_int, tok);
            }
        } else if (!res.qt.eql(int_qt, p.comp)) {
            const old_val = res.val;
            const value_change_kind = try res.val.intCast(int_qt, p.comp);
            switch (value_change_kind) {
                .none => {},
                .truncated => try p.errValueChanged(tok, .int_value_changed, res.*, old_val, int_qt),
                .sign_changed => try p.err(tok, .sign_conversion, .{ res.qt, int_qt }),
            }

            if (src_sk.isReal() and dest_sk.isReal()) {
                res.qt = int_qt;
                try res.implicitCast(p, .int_cast, tok);
            } else if (src_sk.isReal()) {
                const real_int_qt = int_qt.toReal(p.comp);
                if (!res.qt.eql(real_int_qt, p.comp)) {
                    res.qt = real_int_qt;
                    try res.implicitCast(p, .int_cast, tok);
                }
                res.qt = int_qt;
                try res.implicitCast(p, .real_to_complex_int, tok);
            } else if (dest_sk.isReal()) {
                res.qt = res.qt.toReal(p.comp);
                try res.implicitCast(p, .complex_int_to_real, tok);
                res.qt = int_qt;
                try res.implicitCast(p, .int_cast, tok);
            } else {
                res.qt = int_qt;
                try res.implicitCast(p, .complex_int_cast, tok);
            }
        }
    }

    fn floatToIntWarning(
        res: Result,
        p: *Parser,
        int_qt: QualType,
        old_val: Value,
        change_kind: Value.FloatToIntChangeKind,
        tok: TokenIndex,
    ) !void {
        switch (change_kind) {
            .none => return p.err(tok, .float_to_int, .{ res.qt, int_qt }),
            .out_of_range => return p.err(tok, .float_out_of_range, .{ res.qt, int_qt }),
            .overflow => return p.err(tok, .float_overflow_conversion, .{ res.qt, int_qt }),
            .nonzero_to_zero => return p.errValueChanged(tok, .float_zero_conversion, res, old_val, int_qt),
            .value_changed => return p.errValueChanged(tok, .float_value_changed, res, old_val, int_qt),
        }
    }

    fn castToFloat(res: *Result, p: *Parser, float_qt: QualType, tok: TokenIndex) Error!void {
        const src_sk = res.qt.scalarKind(p.comp);
        const dest_sk = float_qt.scalarKind(p.comp);

        if (src_sk == .bool) {
            try res.val.intToFloat(float_qt, p.comp);
            res.qt = float_qt.toReal(p.comp);
            try res.implicitCast(p, .bool_to_float, tok);
            if (!dest_sk.isReal()) {
                res.qt = float_qt;
                try res.implicitCast(p, .real_to_complex_float, tok);
            }
        } else if (src_sk.isInt()) {
            try res.val.intToFloat(float_qt, p.comp);
            if (src_sk.isReal() and dest_sk.isReal()) {
                res.qt = float_qt;
                try res.implicitCast(p, .int_to_float, tok);
            } else if (src_sk.isReal()) {
                res.qt = float_qt.toReal(p.comp);
                try res.implicitCast(p, .int_to_float, tok);
                res.qt = float_qt;
                try res.implicitCast(p, .real_to_complex_float, tok);
            } else if (dest_sk.isReal()) {
                res.qt = res.qt.toReal(p.comp);
                try res.implicitCast(p, .complex_int_to_real, tok);
                res.qt = float_qt;
                try res.implicitCast(p, .int_to_float, tok);
            } else {
                res.qt = float_qt;
                try res.implicitCast(p, .complex_int_to_complex_float, tok);
            }
        } else if (!res.qt.eql(float_qt, p.comp)) {
            try res.val.floatCast(float_qt, p.comp);
            if (src_sk.isReal() and dest_sk.isReal()) {
                res.qt = float_qt;
                try res.implicitCast(p, .float_cast, tok);
            } else if (src_sk.isReal()) {
                if (res.qt.floatRank(p.comp) != float_qt.floatRank(p.comp)) {
                    res.qt = float_qt.toReal(p.comp);
                    try res.implicitCast(p, .float_cast, tok);
                }
                res.qt = float_qt;
                try res.implicitCast(p, .real_to_complex_float, tok);
            } else if (dest_sk.isReal()) {
                res.qt = res.qt.toReal(p.comp);
                try res.implicitCast(p, .complex_float_to_real, tok);
                if (res.qt.floatRank(p.comp) != float_qt.floatRank(p.comp)) {
                    res.qt = float_qt;
                    try res.implicitCast(p, .float_cast, tok);
                }
            } else {
                res.qt = float_qt;
                try res.implicitCast(p, .complex_float_cast, tok);
            }
        }
    }

    /// Converts a bool or integer to a pointer
    fn castToPointer(res: *Result, p: *Parser, ptr_qt: QualType, tok: TokenIndex) Error!void {
        const src_sk = res.qt.scalarKind(p.comp);
        if (src_sk == .bool) {
            res.qt = ptr_qt;
            try res.implicitCast(p, .bool_to_pointer, tok);
        } else if (src_sk.isInt()) {
            _ = try res.val.intCast(ptr_qt, p.comp);
            res.qt = ptr_qt;
            try res.implicitCast(p, .int_to_pointer, tok);
        } else if (src_sk == .nullptr_t) {
            try res.nullToPointer(p, ptr_qt, tok);
        } else if (src_sk.isPointer() and !res.qt.eql(ptr_qt, p.comp)) {
            if (ptr_qt.is(p.comp, .nullptr_t)) {
                res.qt = .invalid;
                return;
            }

            const src_elem = res.qt.childType(p.comp);
            const dest_elem = ptr_qt.childType(p.comp);
            res.qt = ptr_qt;

            if (dest_elem.eql(src_elem, p.comp) and
                (dest_elem.@"const" == src_elem.@"const" or dest_elem.@"const") and
                (dest_elem.@"volatile" == src_elem.@"volatile" or dest_elem.@"volatile"))
            {
                // Gaining qualifiers is a no-op.
                try res.implicitCast(p, .no_op, tok);
            } else {
                try res.implicitCast(p, .bitcast, tok);
            }
        }
    }

    fn castToVoid(res: *Result, p: *Parser, tok: TokenIndex) Error!void {
        if (!res.qt.is(p.comp, .void)) {
            res.qt = .void;
            try res.implicitCast(p, .to_void, tok);
        }
    }

    fn nullToPointer(res: *Result, p: *Parser, ptr_ty: QualType, tok: TokenIndex) Error!void {
        if (!res.qt.is(p.comp, .nullptr_t) and !res.val.isZero(p.comp)) return;
        res.val = .null;
        res.qt = ptr_ty;
        try res.implicitCast(p, .null_to_pointer, tok);
    }

    fn usualUnaryConversion(res: *Result, p: *Parser, tok: TokenIndex) Error!void {
        if (res.qt.isInvalid()) return;
        if (res.qt.isFloat(p.comp)) fp_eval: {
            const eval_method = p.comp.langopts.fp_eval_method orelse break :fp_eval;
            switch (eval_method) {
                .source => {},
                .indeterminate => unreachable,
                .double => {
                    if (res.qt.floatRank(p.comp) < QualType.double.floatRank(p.comp)) {
                        var res_qt: QualType = .double;
                        if (res.qt.is(p.comp, .complex)) res_qt = try res_qt.toComplex(p.comp);
                        return res.castToFloat(p, res_qt, tok);
                    }
                },
                .extended => {
                    if (res.qt.floatRank(p.comp) < QualType.long_double.floatRank(p.comp)) {
                        var res_qt: QualType = .long_double;
                        if (res.qt.is(p.comp, .complex)) res_qt = try res_qt.toComplex(p.comp);
                        return res.castToFloat(p, res_qt, tok);
                    }
                },
            }
        }

        if (!p.comp.langopts.use_native_half_type) {
            if (res.qt.get(p.comp, .float)) |float_ty| {
                if (float_ty == .fp16) {
                    return res.castToFloat(p, .float, tok);
                }
            }
        }

        if (res.qt.isInt(p.comp) and !p.in_macro) {
            if (p.tree.bitfieldWidth(res.node, true)) |width| {
                if (res.qt.promoteBitfield(p.comp, width)) |promotion_ty| {
                    return res.castToInt(p, promotion_ty, tok);
                }
            }
            return res.castToInt(p, res.qt.promoteInt(p.comp), tok);
        }
    }

    fn usualArithmeticConversion(a: *Result, b: *Result, p: *Parser, tok: TokenIndex) Error!void {
        try a.usualUnaryConversion(p, tok);
        try b.usualUnaryConversion(p, tok);

        // if either is a float cast to that type
        const a_float = a.qt.isFloat(p.comp);
        const b_float = b.qt.isFloat(p.comp);
        if (a_float and b_float) {
            const a_complex = a.qt.is(p.comp, .complex);
            const b_complex = b.qt.is(p.comp, .complex);
            const a_rank = a.qt.floatRank(p.comp);
            const b_rank = b.qt.floatRank(p.comp);
            if ((a_rank >= QualType.decimal_float_rank) != (b_rank >= QualType.decimal_float_rank)) {
                try p.err(tok, .mixing_decimal_floats, .{});
                return;
            }

            const res_qt = if (a_rank > b_rank)
                (if (!a_complex and b_complex)
                    try a.qt.toComplex(p.comp)
                else
                    a.qt)
            else
                (if (!b_complex and a_complex)
                    try b.qt.toComplex(p.comp)
                else
                    b.qt);

            try a.castToFloat(p, res_qt, tok);
            try b.castToFloat(p, res_qt, tok);
            return;
        } else if (a_float) {
            try b.castToFloat(p, a.qt, tok);
            return;
        } else if (b_float) {
            try a.castToFloat(p, b.qt, tok);
            return;
        }

        if (a.qt.eql(b.qt, p.comp)) {
            // cast to promoted type
            try a.castToInt(p, a.qt, tok);
            try b.castToInt(p, b.qt, tok);
            return;
        }

        const a_real = a.qt.toReal(p.comp);
        const b_real = b.qt.toReal(p.comp);

        const type_order = a.qt.intRankOrder(b.qt, p.comp);
        const a_signed = a.qt.signedness(p.comp) == .signed;
        const b_signed = b.qt.signedness(p.comp) == .signed;

        var target_qt: QualType = .invalid;
        if (a_signed == b_signed) {
            // If both have the same sign, use higher-rank type.
            target_qt = switch (type_order) {
                .lt => b.qt,
                .eq, .gt => a_real,
            };
        } else if (type_order != if (a_signed) std.math.Order.gt else std.math.Order.lt) {
            // Only one is signed; and the unsigned type has rank >= the signed type
            // Use the unsigned type
            target_qt = if (b_signed) a_real else b_real;
        } else if (a_real.bitSizeof(p.comp) != b_real.bitSizeof(p.comp)) {
            // Signed type is higher rank and sizes are not equal
            // Use the signed type
            target_qt = if (a_signed) a_real else b_real;
        } else {
            // Signed type is higher rank but same size as unsigned type
            // e.g. `long` and `unsigned` on x86-linux-gnu
            // Use unsigned version of the signed type
            target_qt = if (a_signed) try a_real.makeIntUnsigned(p.comp) else try b_real.makeIntUnsigned(p.comp);
        }

        if (a.qt.is(p.comp, .complex) or b.qt.is(p.comp, .complex)) {
            target_qt = try target_qt.toComplex(p.comp);
        }

        if (target_qt.is(p.comp, .complex)) {
            // TODO implement complex int values
            try a.saveValue(p);
            try b.saveValue(p);
        }
        try a.castToInt(p, target_qt, tok);
        try b.castToInt(p, target_qt, tok);
    }

    fn invalidBinTy(a: *Result, tok: TokenIndex, b: *Result, p: *Parser) Error!bool {
        try p.err(tok, .invalid_bin_types, .{ a.qt, b.qt });
        a.val = .{};
        b.val = .{};
        a.qt = .invalid;
        return false;
    }

    fn shouldEval(a: *Result, b: *Result, p: *Parser) Error!bool {
        if (p.no_eval) return false;
        if (a.val.opt_ref != .none and b.val.opt_ref != .none)
            return true;

        try a.saveValue(p);
        try b.saveValue(p);
        return p.no_eval;
    }

    /// Saves value and replaces it with `.unavailable`.
    fn saveValue(res: *Result, p: *Parser) !void {
        assert(!p.in_macro);
        try res.putValue(p);
        res.val = .{};
    }

    /// Saves value without altering the result.
    fn putValue(res: *const Result, p: *Parser) !void {
        if (res.val.opt_ref == .none or res.val.opt_ref == .null) return;
        if (!p.in_macro) try p.tree.value_map.put(p.comp.gpa, res.node, res.val);
    }

    fn castType(res: *Result, p: *Parser, dest_qt: QualType, operand_tok: TokenIndex, l_paren: TokenIndex) !void {
        if (res.qt.isInvalid()) {
            res.val = .{};
            return;
        } else if (dest_qt.isInvalid()) {
            res.val = .{};
            res.qt = .invalid;
            return;
        }
        var cast_kind: Node.Cast.Kind = undefined;

        const dest_sk = dest_qt.scalarKind(p.comp);
        const src_sk = res.qt.scalarKind(p.comp);

        const dest_vec = dest_qt.is(p.comp, .vector);
        const src_vec = res.qt.is(p.comp, .vector);

        if (dest_qt.is(p.comp, .void)) {
            // everything can cast to void
            cast_kind = .to_void;
            res.val = .{};
        } else if (res.qt.is(p.comp, .void)) {
            try p.err(operand_tok, .invalid_cast_operand_type, .{res.qt});
            return error.ParsingFailed;
        } else if (dest_vec and src_vec) {
            if (dest_qt.eql(res.qt, p.comp)) {
                cast_kind = .no_op;
            } else if (dest_qt.sizeCompare(res.qt, p.comp) == .eq) {
                cast_kind = .bitcast;
            } else {
                try p.err(l_paren, .invalid_vec_conversion, .{ dest_qt, res.qt });
                return error.ParsingFailed;
            }
        } else if (dest_vec or src_vec) {
            const non_vec_sk = if (dest_vec) src_sk else dest_sk;
            const vec_qt = if (dest_vec) dest_qt else res.qt;
            const non_vec_qt = if (dest_vec) res.qt else dest_qt;
            const non_vec_tok = if (dest_vec) operand_tok else l_paren;
            if (non_vec_sk == .none) {
                try p.err(non_vec_tok, .invalid_cast_operand_type, .{non_vec_qt});
                return error.ParsingFailed;
            } else if (!non_vec_sk.isInt()) {
                try p.err(non_vec_tok, .invalid_vec_conversion_scalar, .{ vec_qt, non_vec_qt });
                return error.ParsingFailed;
            } else if (dest_qt.sizeCompare(res.qt, p.comp) != .eq) {
                try p.err(non_vec_tok, .invalid_vec_conversion_int, .{ vec_qt, non_vec_qt });
                return error.ParsingFailed;
            } else {
                cast_kind = .bitcast;
            }
        } else if (dest_sk == .nullptr_t) {
            res.val = .{};
            if (src_sk == .nullptr_t) {
                cast_kind = .no_op;
            } else {
                try p.err(l_paren, .invalid_object_cast, .{ res.qt, dest_qt });
                return error.ParsingFailed;
            }
        } else if (src_sk == .nullptr_t) {
            if (dest_sk == .bool) {
                try res.nullToPointer(p, res.qt, l_paren);
                res.val.boolCast(p.comp);
                res.qt = .bool;
                try res.implicitCast(p, .pointer_to_bool, l_paren);
                try res.saveValue(p);
            } else if (dest_sk.isPointer()) {
                try res.nullToPointer(p, dest_qt, l_paren);
            } else {
                try p.err(l_paren, .invalid_object_cast, .{ res.qt, dest_qt });
                return error.ParsingFailed;
            }
            cast_kind = .no_op;
        } else if (res.val.isZero(p.comp) and dest_sk.isPointer()) {
            cast_kind = .null_to_pointer;
        } else if (dest_sk != .none) cast: {
            if (dest_sk.isFloat() and src_sk.isPointer()) {
                try p.err(l_paren, .invalid_cast_to_float, .{dest_qt});
                return error.ParsingFailed;
            } else if ((src_sk.isFloat() or !src_sk.isReal()) and dest_sk.isPointer()) {
                try p.err(l_paren, .invalid_cast_to_pointer, .{res.qt});
                return error.ParsingFailed;
            }

            if (dest_qt.eql(res.qt, p.comp)) {
                cast_kind = .no_op;
            } else if (dest_sk == .bool) {
                if (src_sk.isPointer()) {
                    cast_kind = .pointer_to_bool;
                } else if (src_sk.isInt()) {
                    if (!src_sk.isReal()) {
                        res.qt = res.qt.toReal(p.comp);
                        try res.implicitCast(p, .complex_int_to_real, l_paren);
                    }
                    cast_kind = .int_to_bool;
                } else if (src_sk.isFloat()) {
                    if (!src_sk.isReal()) {
                        res.qt = res.qt.toReal(p.comp);
                        try res.implicitCast(p, .complex_float_to_real, l_paren);
                    }
                    cast_kind = .float_to_bool;
                }
            } else if (dest_sk.isInt()) {
                if (src_sk == .bool) {
                    if (!dest_sk.isReal()) {
                        res.qt = dest_qt.toReal(p.comp);
                        try res.implicitCast(p, .bool_to_int, l_paren);
                        cast_kind = .real_to_complex_int;
                    } else {
                        cast_kind = .bool_to_int;
                    }
                } else if (src_sk.isInt()) {
                    if (src_sk.isReal() and dest_sk.isReal()) {
                        cast_kind = .int_cast;
                    } else if (src_sk.isReal()) {
                        res.qt = dest_qt.toReal(p.comp);
                        try res.implicitCast(p, .int_cast, l_paren);
                        cast_kind = .real_to_complex_int;
                    } else if (dest_sk.isReal()) {
                        res.qt = res.qt.toReal(p.comp);
                        try res.implicitCast(p, .complex_int_to_real, l_paren);
                        cast_kind = .int_cast;
                    } else {
                        cast_kind = .complex_int_cast;
                    }
                } else if (src_sk.isPointer()) {
                    res.val = .{};
                    if (!dest_sk.isReal()) {
                        res.qt = dest_qt.toReal(p.comp);
                        try res.implicitCast(p, .pointer_to_int, l_paren);
                        cast_kind = .real_to_complex_int;
                    } else {
                        cast_kind = .pointer_to_int;
                    }
                } else if (src_sk.isReal() and dest_sk.isReal()) {
                    cast_kind = .float_to_int;
                } else if (src_sk.isReal()) {
                    res.qt = dest_qt.toReal(p.comp);
                    try res.implicitCast(p, .float_to_int, l_paren);
                    cast_kind = .real_to_complex_int;
                } else if (dest_sk.isReal()) {
                    res.qt = res.qt.toReal(p.comp);
                    try res.implicitCast(p, .complex_float_to_real, l_paren);
                    cast_kind = .float_to_int;
                } else {
                    cast_kind = .complex_float_to_complex_int;
                }
            } else if (dest_sk.isPointer()) {
                if (src_sk.isPointer()) {
                    cast_kind = .bitcast;
                } else if (src_sk.isInt()) {
                    if (!src_sk.isReal()) {
                        res.qt = res.qt.toReal(p.comp);
                        try res.implicitCast(p, .complex_int_to_real, l_paren);
                    }
                    cast_kind = .int_to_pointer;
                } else if (src_sk == .bool) {
                    cast_kind = .bool_to_pointer;
                } else if (res.qt.is(p.comp, .array)) {
                    cast_kind = .array_to_pointer;
                } else if (res.qt.is(p.comp, .func)) {
                    cast_kind = .function_to_pointer;
                } else {
                    try p.err(operand_tok, .invalid_cast_operand_type, .{res.qt});
                    return error.ParsingFailed;
                }
            } else if (dest_sk.isFloat()) {
                if (src_sk == .bool) {
                    if (!dest_sk.isReal()) {
                        res.qt = dest_qt.toReal(p.comp);
                        try res.implicitCast(p, .bool_to_float, l_paren);
                        cast_kind = .real_to_complex_float;
                    } else {
                        cast_kind = .bool_to_float;
                    }
                } else if (src_sk.isInt()) {
                    if (src_sk.isReal() and dest_sk.isReal()) {
                        cast_kind = .int_to_float;
                    } else if (src_sk.isReal()) {
                        res.qt = dest_qt.toReal(p.comp);
                        try res.implicitCast(p, .int_to_float, l_paren);
                        cast_kind = .real_to_complex_float;
                    } else if (dest_sk.isReal()) {
                        res.qt = res.qt.toReal(p.comp);
                        try res.implicitCast(p, .complex_int_to_real, l_paren);
                        cast_kind = .int_to_float;
                    } else {
                        cast_kind = .complex_int_to_complex_float;
                    }
                } else if (src_sk.isReal() and dest_sk.isReal()) {
                    cast_kind = .float_cast;
                } else if (src_sk.isReal()) {
                    res.qt = dest_qt.toReal(p.comp);
                    try res.implicitCast(p, .float_cast, l_paren);
                    cast_kind = .real_to_complex_float;
                } else if (dest_sk.isReal()) {
                    res.qt = res.qt.toReal(p.comp);
                    try res.implicitCast(p, .complex_float_to_real, l_paren);
                    cast_kind = .float_cast;
                } else {
                    cast_kind = .complex_float_cast;
                }
            }
            if (res.val.opt_ref == .none) break :cast;

            const src_int = src_sk.isInt() or src_sk.isPointer();
            const dest_int = dest_sk.isInt() or dest_sk.isPointer();
            if (dest_sk == .bool) {
                res.val.boolCast(p.comp);
            } else if (src_sk.isFloat() and dest_int) {
                if (dest_qt.hasIncompleteSize(p.comp)) {
                    try p.err(l_paren, .cast_to_incomplete_type, .{dest_qt});
                    return error.ParsingFailed;
                }
                // Explicit cast, no conversion warning
                _ = try res.val.floatToInt(dest_qt, p.comp);
            } else if (dest_sk.isFloat() and src_int) {
                try res.val.intToFloat(dest_qt, p.comp);
            } else if (dest_sk.isFloat() and src_sk.isFloat()) {
                try res.val.floatCast(dest_qt, p.comp);
            } else if (src_int and dest_int) {
                if (dest_qt.hasIncompleteSize(p.comp)) {
                    try p.err(l_paren, .cast_to_incomplete_type, .{dest_qt});
                    return error.ParsingFailed;
                }
                _ = try res.val.intCast(dest_qt, p.comp);
            }
        } else if (dest_qt.get(p.comp, .@"union")) |union_ty| {
            if (union_ty.layout == null) {
                try p.err(l_paren, .cast_to_incomplete_type, .{dest_qt});
                return error.ParsingFailed;
            }

            for (union_ty.fields) |field| {
                if (field.qt.eql(res.qt, p.comp)) {
                    cast_kind = .union_cast;
                    try p.err(l_paren, .gnu_union_cast, .{});
                    break;
                }
            } else {
                try p.err(l_paren, .invalid_union_cast, .{res.qt});
                return error.ParsingFailed;
            }
        } else if (dest_qt.eql(res.qt, p.comp)) {
            try p.err(l_paren, .cast_to_same_type, .{dest_qt});
            cast_kind = .no_op;
        } else {
            try p.err(l_paren, .invalid_cast_type, .{dest_qt});
            return error.ParsingFailed;
        }

        if (dest_qt.isQualified()) try p.err(l_paren, .qual_cast, .{dest_qt});
        if (dest_sk.isInt() and src_sk.isPointer() and dest_qt.sizeCompare(res.qt, p.comp) == .lt) {
            try p.err(l_paren, .cast_to_smaller_int, .{ dest_qt, res.qt });
        }

        res.qt = dest_qt.unqualified();
        res.node = try p.addNode(.{
            .cast = .{
                .l_paren = l_paren,
                .qt = res.qt,
                .operand = res.node,
                .kind = cast_kind,
                .implicit = false,
            },
        });
    }

    fn intFitsInType(res: Result, p: *Parser, ty: QualType) !bool {
        const max_int = try Value.maxInt(ty, p.comp);
        const min_int = try Value.minInt(ty, p.comp);
        return res.val.compare(.lte, max_int, p.comp) and
            (res.qt.signedness(p.comp) == .unsigned or res.val.compare(.gte, min_int, p.comp));
    }

    const CoerceContext = union(enum) {
        assign,
        init,
        ret,
        arg: ?TokenIndex,
        test_coerce,

        fn note(c: CoerceContext, p: *Parser) !void {
            switch (c) {
                .arg => |opt_tok| if (opt_tok) |tok| try p.err(tok, .parameter_here, .{}),
                .test_coerce => unreachable,
                else => {},
            }
        }
    };

    /// Perform assignment-like coercion to `dest_ty`.
    fn coerce(res: *Result, p: *Parser, dest_ty: QualType, tok: TokenIndex, c: CoerceContext) Error!void {
        if (dest_ty.isInvalid()) {
            res.qt = .invalid;
            return;
        }
        return res.coerceExtra(p, dest_ty, tok, c) catch |er| switch (er) {
            error.CoercionFailed => unreachable,
            else => |e| return e,
        };
    }

    fn coerceExtra(
        res: *Result,
        p: *Parser,
        dest_qt: QualType,
        tok: TokenIndex,
        c: CoerceContext,
    ) (Error || error{CoercionFailed})!void {
        // Subject of the coercion does not need to be qualified.
        const src_original_qt = res.qt;
        switch (c) {
            .init, .ret, .assign => try res.lvalConversion(p, tok),
            else => {},
        }
        if (res.qt.isInvalid()) return;
        const dest_unqual = dest_qt.unqualified();
        const dest_sk = dest_unqual.scalarKind(p.comp);
        const src_sk = res.qt.scalarKind(p.comp);

        if (dest_qt.is(p.comp, .vector) and res.qt.is(p.comp, .vector)) {
            if (dest_unqual.eql(res.qt, p.comp)) return;
            if (dest_unqual.sizeCompare(res.qt, p.comp) == .eq) {
                res.qt = dest_unqual;
                return res.implicitCast(p, .bitcast, tok);
            }
        } else if (dest_sk == .nullptr_t) {
            if (src_sk == .nullptr_t) return;
        } else if (dest_sk == .bool) {
            if (src_sk != .none and src_sk != .nullptr_t) {
                // this is ridiculous but it's what clang does
                try res.castToBool(p, dest_unqual, tok);
                return;
            }
        } else if (dest_sk.isInt()) {
            if (src_sk.isInt() or src_sk.isFloat()) {
                try res.castToInt(p, dest_unqual, tok);
                return;
            } else if (src_sk.isPointer()) {
                if (c == .test_coerce) return error.CoercionFailed;
                try p.err(tok, .implicit_ptr_to_int, .{ src_original_qt, dest_unqual });
                try c.note(p);
                try res.castToInt(p, dest_unqual, tok);
                return;
            }
        } else if (dest_sk.isFloat()) {
            if (src_sk.isInt() or src_sk.isFloat()) {
                try res.castToFloat(p, dest_unqual, tok);
                return;
            }
        } else if (dest_sk.isPointer()) {
            if (src_sk == .nullptr_t or res.val.isZero(p.comp)) {
                try res.nullToPointer(p, dest_unqual, tok);
                return;
            } else if (src_sk.isInt() and src_sk.isReal()) {
                if (c == .test_coerce) return error.CoercionFailed;
                try p.err(tok, .implicit_int_to_ptr, .{ src_original_qt, dest_unqual });
                try c.note(p);
                try res.castToPointer(p, dest_unqual, tok);
                return;
            } else if (src_sk == .void_pointer or dest_unqual.eql(res.qt, p.comp)) {
                return res.castToPointer(p, dest_unqual, tok);
            } else if (dest_sk == .void_pointer and src_sk.isPointer()) {
                return res.castToPointer(p, dest_unqual, tok);
            } else if (src_sk.isPointer()) {
                const src_child = res.qt.childType(p.comp);
                const dest_child = dest_unqual.childType(p.comp);
                if (src_child.eql(dest_child, p.comp)) {
                    if ((src_child.@"const" and !dest_child.@"const") or
                        (src_child.@"volatile" and !dest_child.@"volatile") or
                        (src_child.restrict and !dest_child.restrict))
                    {
                        try p.err(tok, switch (c) {
                            .assign => .ptr_assign_discards_quals,
                            .init => .ptr_init_discards_quals,
                            .ret => .ptr_ret_discards_quals,
                            .arg => .ptr_arg_discards_quals,
                            .test_coerce => return error.CoercionFailed,
                        }, .{ dest_qt, src_original_qt });
                    }
                    try res.castToPointer(p, dest_unqual, tok);
                    return;
                }

                const different_sign_only = src_child.sameRankDifferentSign(dest_child, p.comp);
                switch (c) {
                    .assign => try p.err(tok, if (different_sign_only) .incompatible_ptr_assign_sign else .incompatible_ptr_assign, .{ dest_qt, src_original_qt }),
                    .init => try p.err(tok, if (different_sign_only) .incompatible_ptr_init_sign else .incompatible_ptr_init, .{ dest_qt, src_original_qt }),
                    .ret => try p.err(tok, if (different_sign_only) .incompatible_return_sign else .incompatible_return, .{ src_original_qt, dest_qt }),
                    .arg => try p.err(tok, if (different_sign_only) .incompatible_ptr_arg_sign else .incompatible_ptr_arg, .{ src_original_qt, dest_qt }),
                    .test_coerce => return error.CoercionFailed,
                }
                try c.note(p);

                res.qt = dest_unqual;
                return res.implicitCast(p, .bitcast, tok);
            }
        } else if (dest_unqual.getRecord(p.comp) != null) {
            if (dest_unqual.eql(res.qt, p.comp)) {
                return; // ok
            }

            if (c == .arg) if (dest_unqual.get(p.comp, .@"union")) |union_ty| {
                if (dest_unqual.hasAttribute(p.comp, .transparent_union)) transparent_union: {
                    res.coerceExtra(p, union_ty.fields[0].qt, tok, .test_coerce) catch |er| switch (er) {
                        error.CoercionFailed => break :transparent_union,
                        else => |e| return e,
                    };
                    res.node = try p.addNode(.{ .union_init_expr = .{
                        .field_index = 0,
                        .initializer = res.node,
                        .l_brace_tok = tok,
                        .union_qt = dest_unqual,
                    } });
                    res.qt = dest_unqual;
                    return;
                }
            };
        } else if (dest_unqual.is(p.comp, .vector)) {
            if (dest_unqual.eql(res.qt, p.comp)) {
                return; // ok
            }
        } else {
            if (c == .assign) {
                const base_type = dest_unqual.base(p.comp);
                switch (base_type.type) {
                    .array => return p.err(tok, .array_not_assignable, .{base_type.qt}),
                    .func => return p.err(tok, .non_object_not_assignable, .{base_type.qt}),
                    else => {},
                }
            } else if (c == .test_coerce) {
                return error.CoercionFailed;
            }
            // This case should not be possible and an error should have already been emitted but we
            // might still have attempted to parse further so return error.ParsingFailed here to stop.
            return error.ParsingFailed;
        }

        switch (c) {
            .assign => try p.err(tok, .incompatible_assign, .{ dest_unqual, res.qt }),
            .init => try p.err(tok, .incompatible_init, .{ dest_unqual, res.qt }),
            .ret => try p.err(tok, .incompatible_return, .{ res.qt, dest_unqual }),
            .arg => try p.err(tok, .incompatible_arg, .{ res.qt, dest_unqual }),
            .test_coerce => return error.CoercionFailed,
        }
        try c.note(p);
    }
};

fn expect(p: *Parser, comptime func: fn (*Parser) Error!?Result) Error!Result {
    return p.expectResult(try func(p));
}

fn expectResult(p: *Parser, res: ?Result) Error!Result {
    return res orelse {
        try p.err(p.tok_i, .expected_expr, .{});
        return error.ParsingFailed;
    };
}

/// expr : assignExpr (',' assignExpr)*
fn expr(p: *Parser) Error!?Result {
    var expr_start = p.tok_i;
    var prev_total = p.diagnostics.total;
    var lhs = (try p.assignExpr()) orelse {
        if (p.tok_ids[p.tok_i] == .comma) _ = try p.expectResult(null);
        return null;
    };
    while (p.eatToken(.comma)) |comma| {
        try lhs.maybeWarnUnused(p, expr_start, prev_total);
        expr_start = p.tok_i;
        prev_total = p.diagnostics.total;

        var rhs = try p.expect(assignExpr);
        try rhs.lvalConversion(p, expr_start);
        lhs.val = rhs.val;
        lhs.qt = rhs.qt;
        try lhs.bin(p, .comma_expr, rhs, comma);
    }
    return lhs;
}

fn eatTag(p: *Parser, id: Token.Id) ?std.meta.Tag(Node) {
    if (p.eatToken(id)) |_| return switch (id) {
        .equal => .assign_expr,
        .asterisk_equal => .mul_assign_expr,
        .slash_equal => .div_assign_expr,
        .percent_equal => .mod_assign_expr,
        .plus_equal => .add_assign_expr,
        .minus_equal => .sub_assign_expr,
        .angle_bracket_angle_bracket_left_equal => .shl_assign_expr,
        .angle_bracket_angle_bracket_right_equal => .shr_assign_expr,
        .ampersand_equal => .bit_and_assign_expr,
        .caret_equal => .bit_xor_assign_expr,
        .pipe_equal => .bit_or_assign_expr,
        .equal_equal => .equal_expr,
        .bang_equal => .not_equal_expr,
        .angle_bracket_left => .less_than_expr,
        .angle_bracket_left_equal => .less_than_equal_expr,
        .angle_bracket_right => .greater_than_expr,
        .angle_bracket_right_equal => .greater_than_equal_expr,
        .angle_bracket_angle_bracket_left => .shl_expr,
        .angle_bracket_angle_bracket_right => .shr_expr,
        .plus => .add_expr,
        .minus => .sub_expr,
        .asterisk => .mul_expr,
        .slash => .div_expr,
        .percent => .mod_expr,
        else => unreachable,
    } else return null;
}

fn nonAssignExpr(assign_node: std.meta.Tag(Node)) std.meta.Tag(Node) {
    return switch (assign_node) {
        .mul_assign_expr => .mul_expr,
        .div_assign_expr => .div_expr,
        .mod_assign_expr => .mod_expr,
        .add_assign_expr => .add_expr,
        .sub_assign_expr => .sub_expr,
        .shl_assign_expr => .shl_expr,
        .shr_assign_expr => .shr_expr,
        .bit_and_assign_expr => .bit_and_expr,
        .bit_xor_assign_expr => .bit_xor_expr,
        .bit_or_assign_expr => .bit_or_expr,
        else => unreachable,
    };
}

fn unwrapNestedOperation(p: *Parser, node_idx: Node.Index) ?Node.DeclRef {
    return loop: switch (node_idx.get(&p.tree)) {
        inline .array_access_expr,
        .member_access_ptr_expr,
        .member_access_expr,
        => |memb_or_arr_access| continue :loop memb_or_arr_access.base.get(&p.tree),
        inline .cast,
        .paren_expr,
        .pre_inc_expr,
        .post_inc_expr,
        .pre_dec_expr,
        .post_dec_expr,
        => |cast_or_unary| continue :loop cast_or_unary.operand.get(&p.tree),
        .sub_expr,
        .add_expr,
        => |bin| continue :loop bin.lhs.get(&p.tree),
        .call_expr => |call| continue :loop call.callee.get(&p.tree),
        .decl_ref_expr => |decl_ref| decl_ref,
        else => null,
    };
}

fn issueDeclaredConstHereNote(p: *Parser, decl_ref: Tree.Node.DeclRef, var_name: []const u8) Compilation.Error!void {
    const location = switch (decl_ref.decl.get(&p.tree)) {
        .variable => |variable| variable.name_tok,
        .param => |param| param.name_tok,
        else => return,
    };
    try p.err(location, .declared_const_here, .{var_name});
}

fn issueConstAssignmetDiagnostics(p: *Parser, node_idx: Node.Index, tok: TokenIndex) Compilation.Error!void {
    if (p.unwrapNestedOperation(node_idx)) |unwrapped| {
        const name = p.tokSlice(unwrapped.name_tok);
        try p.err(tok, .const_var_assignment, .{ name, unwrapped.qt });
        try p.issueDeclaredConstHereNote(unwrapped, name);
    } else {
        try p.err(tok, .not_assignable, .{});
    }
}

/// assignExpr
///  : condExpr
///  | unExpr ('=' | '*=' | '/=' | '%=' | '+=' | '-=' | '<<=' | '>>=' | '&=' | '^=' | '|=') assignExpr
fn assignExpr(p: *Parser) Error!?Result {
    var lhs = (try p.condExpr()) orelse return null;

    const tok = p.tok_i;
    const tag = p.eatTag(.equal) orelse
        p.eatTag(.asterisk_equal) orelse
        p.eatTag(.slash_equal) orelse
        p.eatTag(.percent_equal) orelse
        p.eatTag(.plus_equal) orelse
        p.eatTag(.minus_equal) orelse
        p.eatTag(.angle_bracket_angle_bracket_left_equal) orelse
        p.eatTag(.angle_bracket_angle_bracket_right_equal) orelse
        p.eatTag(.ampersand_equal) orelse
        p.eatTag(.caret_equal) orelse
        p.eatTag(.pipe_equal) orelse return lhs;

    var rhs = try p.expect(assignExpr);

    var is_const: bool = undefined;
    if (!p.tree.isLvalExtra(lhs.node, &is_const) or is_const) {
        try p.issueConstAssignmetDiagnostics(lhs.node, tok);
        lhs.qt = .invalid;
    }

    if (tag == .assign_expr) {
        try rhs.coerce(p, lhs.qt, tok, .assign);

        try lhs.bin(p, tag, rhs, tok);
        return lhs;
    }

    var lhs_dummy = blk: {
        var lhs_copy = lhs;
        try lhs_copy.un(p, .compound_assign_dummy_expr, tok);
        try lhs_copy.lvalConversion(p, tok);
        break :blk lhs_copy;
    };
    switch (tag) {
        .mul_assign_expr,
        .div_assign_expr,
        .mod_assign_expr,
        => {
            if (!lhs.qt.isInvalid() and rhs.val.isZero(p.comp) and lhs.qt.isInt(p.comp) and rhs.qt.isInt(p.comp)) {
                switch (tag) {
                    .div_assign_expr => try p.err(tok, .division_by_zero, .{"division"}),
                    .mod_assign_expr => try p.err(tok, .division_by_zero, .{"remainder"}),
                    else => {},
                }
            }
            _ = try lhs_dummy.adjustTypes(tok, &rhs, p, if (tag == .mod_assign_expr) .integer else .arithmetic);
        },
        .sub_assign_expr => {
            _ = try lhs_dummy.adjustTypes(tok, &rhs, p, .sub);
        },
        .add_assign_expr => {
            _ = try lhs_dummy.adjustTypes(tok, &rhs, p, .add);
        },
        .shl_assign_expr,
        .shr_assign_expr,
        .bit_and_assign_expr,
        .bit_xor_assign_expr,
        .bit_or_assign_expr,
        => {
            _ = try lhs_dummy.adjustTypes(tok, &rhs, p, .integer);
        },
        else => unreachable,
    }

    _ = try lhs_dummy.bin(p, nonAssignExpr(tag), rhs, tok);
    try lhs_dummy.coerce(p, lhs.qt, tok, .assign);
    try lhs.bin(p, tag, lhs_dummy, tok);
    return lhs;
}

/// Returns a parse error if the expression is not an integer constant
/// integerConstExpr : constExpr
fn integerConstExpr(p: *Parser, decl_folding: ConstDeclFoldingMode) Error!Result {
    const start = p.tok_i;
    const res = try p.constExpr(decl_folding);
    if (!res.qt.isInvalid() and !res.qt.isRealInt(p.comp)) {
        try p.err(start, .expected_integer_constant_expr, .{});
        return error.ParsingFailed;
    }
    return res;
}

/// Caller is responsible for issuing a diagnostic if result is invalid/unavailable
/// constExpr : condExpr
fn constExpr(p: *Parser, decl_folding: ConstDeclFoldingMode) Error!Result {
    const const_decl_folding = p.const_decl_folding;
    defer p.const_decl_folding = const_decl_folding;
    p.const_decl_folding = decl_folding;

    const res = try p.expect(condExpr);

    if (res.qt.isInvalid() or res.val.opt_ref == .none) return res;

    try res.putValue(p);
    return res;
}

/// condExpr : lorExpr ('?' expression? ':' condExpr)?
fn condExpr(p: *Parser) Error!?Result {
    const cond_tok = p.tok_i;
    var cond = (try p.lorExpr()) orelse return null;
    if (p.eatToken(.question_mark) == null) return cond;
    try cond.lvalConversion(p, cond_tok);
    const saved_eval = p.no_eval;

    if (cond.qt.scalarKind(p.comp) == .none) {
        try p.err(cond_tok, .cond_expr_type, .{cond.qt});
        return error.ParsingFailed;
    }

    // Prepare for possible binary conditional expression.
    const maybe_colon = p.eatToken(.colon);

    // Depending on the value of the condition, avoid evaluating unreachable branches.
    var then_expr = blk: {
        defer p.no_eval = saved_eval;
        if (cond.val.opt_ref != .none and !cond.val.toBool(p.comp)) p.no_eval = true;
        break :blk try p.expect(expr);
    };

    // If we saw a colon then this is a binary conditional expression.
    if (maybe_colon) |colon| {
        var cond_then = cond;
        cond_then.node = try p.addNode(.{
            .cond_dummy_expr = .{
                .op_tok = colon,
                .operand = cond.node,
                .qt = cond.qt,
            },
        });
        _ = try cond_then.adjustTypes(colon, &then_expr, p, .conditional);
        cond.qt = then_expr.qt;
        cond.node = try p.addNode(.{
            .binary_cond_expr = .{
                .cond_tok = cond_tok,
                .cond = cond.node,
                .then_expr = cond_then.node,
                .else_expr = then_expr.node,
                .qt = cond.qt,
            },
        });
        return cond;
    }

    const colon = try p.expectToken(.colon);
    var else_expr = blk: {
        defer p.no_eval = saved_eval;
        if (cond.val.opt_ref != .none and cond.val.toBool(p.comp)) p.no_eval = true;
        break :blk try p.expect(condExpr);
    };

    _ = try then_expr.adjustTypes(colon, &else_expr, p, .conditional);

    if (cond.val.opt_ref != .none) {
        cond.val = if (cond.val.toBool(p.comp)) then_expr.val else else_expr.val;
    } else {
        try then_expr.saveValue(p);
        try else_expr.saveValue(p);
    }
    cond.qt = then_expr.qt;
    cond.node = try p.addNode(.{
        .cond_expr = .{
            .cond_tok = cond_tok,
            .qt = cond.qt,
            .cond = cond.node,
            .then_expr = then_expr.node,
            .else_expr = else_expr.node,
        },
    });
    return cond;
}

/// lorExpr : landExpr ('||' landExpr)*
fn lorExpr(p: *Parser) Error!?Result {
    var lhs = (try p.landExpr()) orelse return null;
    const saved_eval = p.no_eval;
    defer p.no_eval = saved_eval;

    while (p.eatToken(.pipe_pipe)) |tok| {
        if (lhs.val.opt_ref != .none and lhs.val.toBool(p.comp)) p.no_eval = true;
        var rhs = try p.expect(landExpr);

        if (try lhs.adjustTypes(tok, &rhs, p, .boolean_logic)) {
            const res = lhs.val.toBool(p.comp) or rhs.val.toBool(p.comp);
            lhs.val = Value.fromBool(res);
        } else {
            lhs.val.boolCast(p.comp);
        }
        try lhs.boolRes(p, .bool_or_expr, rhs, tok);
    }
    return lhs;
}

/// landExpr : orExpr ('&&' orExpr)*
fn landExpr(p: *Parser) Error!?Result {
    var lhs = (try p.orExpr()) orelse return null;
    const saved_eval = p.no_eval;
    defer p.no_eval = saved_eval;

    while (p.eatToken(.ampersand_ampersand)) |tok| {
        if (lhs.val.opt_ref != .none and !lhs.val.toBool(p.comp)) p.no_eval = true;
        var rhs = try p.expect(orExpr);

        if (try lhs.adjustTypes(tok, &rhs, p, .boolean_logic)) {
            const res = lhs.val.toBool(p.comp) and rhs.val.toBool(p.comp);
            lhs.val = Value.fromBool(res);
        } else {
            lhs.val.boolCast(p.comp);
        }
        try lhs.boolRes(p, .bool_and_expr, rhs, tok);
    }
    return lhs;
}

/// orExpr : xorExpr ('|' xorExpr)*
fn orExpr(p: *Parser) Error!?Result {
    var lhs = (try p.xorExpr()) orelse return null;
    while (p.eatToken(.pipe)) |tok| {
        var rhs = try p.expect(xorExpr);

        if (try lhs.adjustTypes(tok, &rhs, p, .integer)) {
            lhs.val = try lhs.val.bitOr(rhs.val, p.comp);
        }
        try lhs.bin(p, .bit_or_expr, rhs, tok);
    }
    return lhs;
}

/// xorExpr : andExpr ('^' andExpr)*
fn xorExpr(p: *Parser) Error!?Result {
    var lhs = (try p.andExpr()) orelse return null;
    while (p.eatToken(.caret)) |tok| {
        var rhs = try p.expect(andExpr);

        if (try lhs.adjustTypes(tok, &rhs, p, .integer)) {
            lhs.val = try lhs.val.bitXor(rhs.val, p.comp);
        }
        try lhs.bin(p, .bit_xor_expr, rhs, tok);
    }
    return lhs;
}

/// andExpr : eqExpr ('&' eqExpr)*
fn andExpr(p: *Parser) Error!?Result {
    var lhs = (try p.eqExpr()) orelse return null;
    while (p.eatToken(.ampersand)) |tok| {
        var rhs = try p.expect(eqExpr);

        if (try lhs.adjustTypes(tok, &rhs, p, .integer)) {
            lhs.val = try lhs.val.bitAnd(rhs.val, p.comp);
        }
        try lhs.bin(p, .bit_and_expr, rhs, tok);
    }
    return lhs;
}

/// eqExpr : compExpr (('==' | '!=') compExpr)*
fn eqExpr(p: *Parser) Error!?Result {
    var lhs = (try p.compExpr()) orelse return null;
    while (true) {
        const tok = p.tok_i;
        const tag = p.eatTag(.equal_equal) orelse
            p.eatTag(.bang_equal) orelse break;
        var rhs = try p.expect(compExpr);

        if (try lhs.adjustTypes(tok, &rhs, p, .equality)) {
            const op: std.math.CompareOperator = if (tag == .equal_expr) .eq else .neq;

            const res: ?bool = if (lhs.qt.isPointer(p.comp) or rhs.qt.isPointer(p.comp))
                lhs.val.comparePointers(op, rhs.val, p.comp)
            else
                lhs.val.compare(op, rhs.val, p.comp);

            lhs.val = if (res) |val| Value.fromBool(val) else .{};
        } else {
            lhs.val.boolCast(p.comp);
        }
        try lhs.boolRes(p, tag, rhs, tok);
    }
    return lhs;
}

/// compExpr : shiftExpr (('<' | '<=' | '>' | '>=') shiftExpr)*
fn compExpr(p: *Parser) Error!?Result {
    var lhs = (try p.shiftExpr()) orelse return null;
    while (true) {
        const tok = p.tok_i;
        const tag = p.eatTag(.angle_bracket_left) orelse
            p.eatTag(.angle_bracket_left_equal) orelse
            p.eatTag(.angle_bracket_right) orelse
            p.eatTag(.angle_bracket_right_equal) orelse break;
        var rhs = try p.expect(shiftExpr);

        if (try lhs.adjustTypes(tok, &rhs, p, .relational)) {
            const op: std.math.CompareOperator = switch (tag) {
                .less_than_expr => .lt,
                .less_than_equal_expr => .lte,
                .greater_than_expr => .gt,
                .greater_than_equal_expr => .gte,
                else => unreachable,
            };

            const res: ?bool = if (lhs.qt.isPointer(p.comp) or rhs.qt.isPointer(p.comp))
                lhs.val.comparePointers(op, rhs.val, p.comp)
            else
                lhs.val.compare(op, rhs.val, p.comp);
            lhs.val = if (res) |val| Value.fromBool(val) else .{};
        } else {
            lhs.val.boolCast(p.comp);
        }
        try lhs.boolRes(p, tag, rhs, tok);
    }
    return lhs;
}

/// shiftExpr : addExpr (('<<' | '>>') addExpr)*
fn shiftExpr(p: *Parser) Error!?Result {
    var lhs = (try p.addExpr()) orelse return null;
    while (true) {
        const tok = p.tok_i;
        const tag = p.eatTag(.angle_bracket_angle_bracket_left) orelse
            p.eatTag(.angle_bracket_angle_bracket_right) orelse break;
        var rhs = try p.expect(addExpr);

        if (try lhs.adjustTypes(tok, &rhs, p, .integer)) {
            if (rhs.val.compare(.lt, .zero, p.comp)) {
                try p.err(tok, .negative_shift_count, .{});
            }
            if (rhs.val.compare(.gte, try Value.int(lhs.qt.bitSizeof(p.comp), p.comp), p.comp)) {
                try p.err(tok, .too_big_shift_count, .{});
            }
            if (tag == .shl_expr) {
                if (try lhs.val.shl(lhs.val, rhs.val, lhs.qt, p.comp) and
                    lhs.qt.signedness(p.comp) != .unsigned) try p.err(tok, .overflow, .{lhs});
            } else {
                lhs.val = try lhs.val.shr(rhs.val, lhs.qt, p.comp);
            }
        }
        try lhs.bin(p, tag, rhs, tok);
    }
    return lhs;
}

/// addExpr : mulExpr (('+' | '-') mulExpr)*
fn addExpr(p: *Parser) Error!?Result {
    var lhs = (try p.mulExpr()) orelse return null;
    while (true) {
        const tok = p.tok_i;
        const tag = p.eatTag(.plus) orelse
            p.eatTag(.minus) orelse break;
        var rhs = try p.expect(mulExpr);

        // We'll want to check this for invalid pointer arithmetic.
        const original_lhs_qt = lhs.qt;

        if (try lhs.adjustTypes(tok, &rhs, p, if (tag == .add_expr) .add else .sub)) {
            const lhs_sk = lhs.qt.scalarKind(p.comp);
            if (tag == .add_expr) {
                if (try lhs.val.add(lhs.val, rhs.val, lhs.qt, p.comp)) {
                    if (lhs_sk.isPointer()) {
                        const increment = lhs;
                        const ptr_bits = p.comp.type_store.intptr.bitSizeof(p.comp);
                        const element_size = increment.qt.childType(p.comp).sizeofOrNull(p.comp) orelse 1;
                        const max_elems = p.comp.maxArrayBytes() / element_size;

                        try p.err(tok, .array_overflow, .{ increment, ptr_bits, element_size * 8, element_size, max_elems });
                    } else if (lhs.qt.signedness(p.comp) != .unsigned) {
                        try p.err(tok, .overflow, .{lhs});
                    }
                }
            } else {
                const elem_size = if (original_lhs_qt.isPointer(p.comp)) original_lhs_qt.childType(p.comp).sizeofOrNull(p.comp) orelse 1 else 1;
                if (elem_size == 0 and rhs.qt.isPointer(p.comp)) {
                    lhs.val = .{};
                } else {
                    if (try lhs.val.sub(lhs.val, rhs.val, lhs.qt, elem_size, p.comp) and
                        lhs.qt.signedness(p.comp) != .unsigned)
                    {
                        try p.err(tok, .overflow, .{lhs});
                    }
                }
            }
        }
        if (!lhs.qt.isInvalid()) {
            const lhs_sk = original_lhs_qt.scalarKind(p.comp);
            if (lhs_sk == .pointer and original_lhs_qt.childType(p.comp).hasIncompleteSize(p.comp)) {
                try p.err(tok, .ptr_arithmetic_incomplete, .{original_lhs_qt.childType(p.comp)});
                lhs.qt = .invalid;
            }
        }
        try lhs.bin(p, tag, rhs, tok);
    }
    return lhs;
}

/// mulExpr : castExpr (('*' | '/' | '%') castExpr)*´
fn mulExpr(p: *Parser) Error!?Result {
    var lhs = (try p.castExpr()) orelse return null;
    while (true) {
        const tok = p.tok_i;
        const tag = p.eatTag(.asterisk) orelse
            p.eatTag(.slash) orelse
            p.eatTag(.percent) orelse break;
        var rhs = try p.expect(castExpr);

        if (rhs.val.isZero(p.comp) and tag != .mul_expr and !p.no_eval and lhs.qt.isInt(p.comp) and rhs.qt.isInt(p.comp)) {
            lhs.val = .{};
            try p.err(tok, if (p.in_macro) .division_by_zero_macro else .division_by_zero, if (tag == .div_expr) .{"division"} else .{"remainder"});
            if (p.in_macro) return error.ParsingFailed;
        }

        if (try lhs.adjustTypes(tok, &rhs, p, if (tag == .mod_expr) .integer else .arithmetic)) {
            switch (tag) {
                .mul_expr => if (try lhs.val.mul(lhs.val, rhs.val, lhs.qt, p.comp) and
                    lhs.qt.signedness(p.comp) != .unsigned) try p.err(tok, .overflow, .{lhs}),
                .div_expr => if (try lhs.val.div(lhs.val, rhs.val, lhs.qt, p.comp) and
                    lhs.qt.signedness(p.comp) != .unsigned) try p.err(tok, .overflow, .{lhs}),
                .mod_expr => {
                    var res = try Value.rem(lhs.val, rhs.val, lhs.qt, p.comp);
                    if (res.opt_ref == .none) {
                        if (p.in_macro) {
                            // match clang behavior by defining invalid remainder to be zero in macros
                            res = .zero;
                        } else {
                            try lhs.saveValue(p);
                            try rhs.saveValue(p);
                        }
                    }
                    lhs.val = res;
                },
                else => unreachable,
            }
        }

        try lhs.bin(p, tag, rhs, tok);
    }
    return lhs;
}

/// castExpr
///  :  '(' compoundStmt ')' suffixExpr*
///  |  '(' typeName ')' castExpr
///  | '(' typeName ')' '{' initializerItems '}'
///  | unExpr
fn castExpr(p: *Parser) Error!?Result {
    if (p.eatToken(.l_paren)) |l_paren| cast_expr: {
        if (p.tok_ids[p.tok_i] == .l_brace) {
            const tok = p.tok_i;
            try p.err(p.tok_i, .gnu_statement_expression, .{});
            if (p.func.qt == null) {
                try p.err(p.tok_i, .stmt_expr_not_allowed_file_scope, .{});
                return error.ParsingFailed;
            }
            var stmt_expr_state: StmtExprState = .{};
            const body_node = (try p.compoundStmt(false, &stmt_expr_state)).?; // compoundStmt only returns null if .l_brace isn't the first token

            var res: Result = .{
                .node = body_node,
                .qt = stmt_expr_state.last_expr_qt,
            };
            try p.expectClosing(l_paren, .r_paren);
            try res.un(p, .stmt_expr, tok);
            while (try p.suffixExpr(res)) |suffix| {
                res = suffix;
            }
            return res;
        }
        const ty = (try p.typeName()) orelse {
            p.tok_i -= 1;
            break :cast_expr;
        };
        try p.expectClosing(l_paren, .r_paren);

        if (p.tok_ids[p.tok_i] == .l_brace) {
            var lhs = (try p.compoundLiteral(ty, l_paren)).?;
            while (try p.suffixExpr(lhs)) |suffix| {
                lhs = suffix;
            }
            return lhs;
        }

        const operand_tok = p.tok_i;
        var operand = try p.expect(castExpr);
        try operand.lvalConversion(p, operand_tok);
        try operand.castType(p, ty, operand_tok, l_paren);
        return operand;
    }
    return p.unExpr();
}

/// builtinBitCast : __builtin_bit_cast '(' typeName ',' assignExpr ')'
fn builtinBitCast(p: *Parser, builtin_tok: TokenIndex) Error!Result {
    const l_paren = try p.expectToken(.l_paren);

    const res_qt = (try p.typeName()) orelse {
        try p.err(p.tok_i, .expected_type, .{});
        return error.ParsingFailed;
    };

    _ = try p.expectToken(.comma);

    const operand_tok = p.tok_i;
    var operand = try p.expect(assignExpr);
    try operand.lvalConversion(p, operand_tok);

    try p.expectClosing(l_paren, .r_paren);

    return .{
        .qt = res_qt,
        .node = try p.addNode(.{
            .cast = .{
                .l_paren = builtin_tok,
                .qt = res_qt,
                .kind = .bitcast,
                .operand = operand.node,
                .implicit = false,
            },
        }),
    };
}

/// shufflevector : __builtin_shufflevector '(' assignExpr ',' assignExpr (',' integerConstExpr)* ')'
fn shufflevector(p: *Parser, builtin_tok: TokenIndex) Error!Result {
    const l_paren = try p.expectToken(.l_paren);

    const first_tok = p.tok_i;
    const lhs = try p.expect(assignExpr);
    _ = try p.expectToken(.comma);
    const second_tok = p.tok_i;
    const rhs = try p.expect(assignExpr);

    const max_index: ?Value = blk: {
        if (lhs.qt.isInvalid() or rhs.qt.isInvalid()) break :blk null;
        const lhs_vec = lhs.qt.get(p.comp, .vector) orelse break :blk null;
        const rhs_vec = rhs.qt.get(p.comp, .vector) orelse break :blk null;

        break :blk try Value.int(lhs_vec.len + rhs_vec.len, p.comp);
    };
    const negative_one = try Value.intern(p.comp, .{ .int = .{ .i64 = -1 } });

    const gpa = p.comp.gpa;
    const list_buf_top = p.list_buf.items.len;
    defer p.list_buf.items.len = list_buf_top;
    while (p.eatToken(.comma)) |_| {
        const index_tok = p.tok_i;
        const index = try p.integerConstExpr(.gnu_folding_extension);
        try p.list_buf.append(gpa, index.node);
        if (index.val.compare(.lt, negative_one, p.comp)) {
            try p.err(index_tok, .shufflevector_negative_index, .{});
        } else if (max_index != null and index.val.compare(.gte, max_index.?, p.comp)) {
            try p.err(index_tok, .shufflevector_index_too_big, .{});
        }
    }

    try p.expectClosing(l_paren, .r_paren);

    var res_qt: QualType = .invalid;
    if (!lhs.qt.isInvalid() and !lhs.qt.is(p.comp, .vector)) {
        try p.err(first_tok, .shufflevector_arg, .{"first"});
    } else if (!rhs.qt.isInvalid() and !rhs.qt.is(p.comp, .vector)) {
        try p.err(second_tok, .shufflevector_arg, .{"second"});
    } else if (!lhs.qt.eql(rhs.qt, p.comp)) {
        try p.err(builtin_tok, .shufflevector_same_type, .{});
    } else if (p.list_buf.items.len == list_buf_top) {
        res_qt = lhs.qt;
    } else {
        res_qt = try p.comp.type_store.put(gpa, .{ .vector = .{
            .elem = lhs.qt.childType(p.comp),
            .len = @intCast(p.list_buf.items.len - list_buf_top),
        } });
    }

    return .{
        .qt = res_qt,
        .node = try p.addNode(.{
            .builtin_shufflevector = .{
                .builtin_tok = builtin_tok,
                .qt = res_qt,
                .lhs = lhs.node,
                .rhs = rhs.node,
                .indexes = p.list_buf.items[list_buf_top..],
            },
        }),
    };
}

/// convertvector : __builtin_convertvector '(' assignExpr ',' typeName ')'
fn convertvector(p: *Parser, builtin_tok: TokenIndex) Error!Result {
    const l_paren = try p.expectToken(.l_paren);

    const operand = try p.expect(assignExpr);
    _ = try p.expectToken(.comma);

    var dest_qt = (try p.typeName()) orelse {
        try p.err(p.tok_i, .expected_type, .{});
        p.skipTo(.r_paren);
        return error.ParsingFailed;
    };

    try p.expectClosing(l_paren, .r_paren);

    if (operand.qt.isInvalid() or operand.qt.isInvalid()) {
        dest_qt = .invalid;
    } else check: {
        const operand_vec = operand.qt.get(p.comp, .vector) orelse {
            try p.err(builtin_tok, .convertvector_arg, .{"first"});
            dest_qt = .invalid;
            break :check;
        };
        const dest_vec = dest_qt.get(p.comp, .vector) orelse {
            try p.err(builtin_tok, .convertvector_arg, .{"second"});
            dest_qt = .invalid;
            break :check;
        };
        if (operand_vec.len != dest_vec.len) {
            try p.err(builtin_tok, .convertvector_size, .{});
            dest_qt = .invalid;
        }
    }

    return .{
        .qt = dest_qt,
        .node = try p.addNode(.{
            .builtin_convertvector = .{
                .builtin_tok = builtin_tok,
                .dest_qt = dest_qt,
                .operand = operand.node,
            },
        }),
    };
}

/// typesCompatible : __builtin_types_compatible_p '(' typeName ',' typeName ')'
fn typesCompatible(p: *Parser, builtin_tok: TokenIndex) Error!Result {
    const l_paren = try p.expectToken(.l_paren);

    const lhs = (try p.typeName()) orelse {
        try p.err(p.tok_i, .expected_type, .{});
        p.skipTo(.r_paren);
        return error.ParsingFailed;
    };
    _ = try p.expectToken(.comma);

    const rhs = (try p.typeName()) orelse {
        try p.err(p.tok_i, .expected_type, .{});
        p.skipTo(.r_paren);
        return error.ParsingFailed;
    };

    try p.expectClosing(l_paren, .r_paren);

    const compatible = lhs.eql(rhs, p.comp);
    const res: Result = .{
        .val = Value.fromBool(compatible),
        .qt = .int,
        .node = try p.addNode(.{
            .builtin_types_compatible_p = .{
                .builtin_tok = builtin_tok,
                .lhs = lhs,
                .rhs = rhs,
            },
        }),
    };
    try res.putValue(p);
    return res;
}

/// chooseExpr : __builtin_choose_expr '(' integerConstExpr ',' assignExpr ',' assignExpr ')'
fn builtinChooseExpr(p: *Parser) Error!Result {
    const l_paren = try p.expectToken(.l_paren);
    const cond_tok = p.tok_i;
    var cond = try p.integerConstExpr(.no_const_decl_folding);
    if (cond.val.opt_ref == .none) {
        try p.err(cond_tok, .builtin_choose_cond, .{});
        return error.ParsingFailed;
    }

    _ = try p.expectToken(.comma);

    const then_expr = if (cond.val.toBool(p.comp))
        try p.expect(assignExpr)
    else
        try p.parseNoEval(assignExpr);

    _ = try p.expectToken(.comma);

    const else_expr = if (!cond.val.toBool(p.comp))
        try p.expect(assignExpr)
    else
        try p.parseNoEval(assignExpr);

    try p.expectClosing(l_paren, .r_paren);

    if (cond.val.toBool(p.comp)) {
        cond.val = then_expr.val;
        cond.qt = then_expr.qt;
    } else {
        cond.val = else_expr.val;
        cond.qt = else_expr.qt;
    }
    cond.node = try p.addNode(.{
        .builtin_choose_expr = .{
            .cond_tok = cond_tok,
            .qt = cond.qt,
            .cond = cond.node,
            .then_expr = then_expr.node,
            .else_expr = else_expr.node,
        },
    });
    return cond;
}

/// vaStart : __builtin_va_arg '(' assignExpr ',' typeName ')'
fn builtinVaArg(p: *Parser, builtin_tok: TokenIndex) Error!Result {
    const l_paren = try p.expectToken(.l_paren);
    const va_list_tok = p.tok_i;
    var va_list = try p.expect(assignExpr);
    try va_list.lvalConversion(p, va_list_tok);

    _ = try p.expectToken(.comma);

    const ty = (try p.typeName()) orelse {
        try p.err(p.tok_i, .expected_type, .{});
        return error.ParsingFailed;
    };
    try p.expectClosing(l_paren, .r_paren);

    if (!va_list.qt.eql(p.comp.type_store.va_list, p.comp)) {
        try p.err(va_list_tok, .incompatible_va_arg, .{va_list.qt});
        return error.ParsingFailed;
    }

    return .{
        .qt = ty,
        .node = try p.addNode(.{
            .builtin_call_expr = .{
                .builtin_tok = builtin_tok,
                .qt = ty,
                .args = &.{va_list.node},
            },
        }),
    };
}

const OffsetKind = enum { bits, bytes };

/// offsetof
///  : __builtin_offsetof '(' typeName ',' offsetofMemberDesignator ')'
///  | __builtin_bitoffsetof '(' typeName ',' offsetofMemberDesignator ')'
fn builtinOffsetof(p: *Parser, builtin_tok: TokenIndex, offset_kind: OffsetKind) Error!Result {
    const l_paren = try p.expectToken(.l_paren);
    const ty_tok = p.tok_i;

    const operand_qt = (try p.typeName()) orelse {
        try p.err(p.tok_i, .expected_type, .{});
        p.skipTo(.r_paren);
        return error.ParsingFailed;
    };

    const record_ty = operand_qt.getRecord(p.comp) orelse {
        try p.err(ty_tok, .offsetof_ty, .{operand_qt});
        p.skipTo(.r_paren);
        return error.ParsingFailed;
    };

    if (record_ty.layout == null) {
        try p.err(ty_tok, .offsetof_incomplete, .{operand_qt});
        p.skipTo(.r_paren);
        return error.ParsingFailed;
    }

    _ = try p.expectToken(.comma);

    const offsetof_expr = try p.offsetofMemberDesignator(record_ty, operand_qt, offset_kind, builtin_tok);

    try p.expectClosing(l_paren, .r_paren);

    const res: Result = .{
        .qt = p.comp.type_store.size,
        .val = offsetof_expr.val,
        .node = try p.addNode(.{
            .builtin_call_expr = .{
                .builtin_tok = builtin_tok,
                .qt = p.comp.type_store.size,
                .args = &.{offsetof_expr.node},
            },
        }),
    };
    try res.putValue(p);
    return res;
}

/// offsetofMemberDesignator : IDENTIFIER ('.' IDENTIFIER | '[' expr ']' )*
fn offsetofMemberDesignator(
    p: *Parser,
    base_record_ty: Type.Record,
    base_qt: QualType,
    offset_kind: OffsetKind,
    base_access_tok: TokenIndex,
) Error!Result {
    errdefer p.skipTo(.r_paren);
    const base_field_name_tok = try p.expectIdentifier();
    const base_field_name = try p.comp.internString(p.tokSlice(base_field_name_tok));

    const base_node = try p.addNode(.{ .default_init_expr = .{
        .last_tok = p.tok_i,
        .qt = base_qt,
    } });

    var lhs, const initial_offset = try p.fieldAccessExtra(base_node, base_record_ty, false, &.{
        .base_qt = base_qt,
        .target_name = base_field_name,
        .access_tok = base_access_tok,
        .name_tok = base_field_name_tok,
        .check_deprecated = false,
    });

    var total_offset: i64 = @intCast(initial_offset);
    var runtime_offset = false;
    while (true) switch (p.tok_ids[p.tok_i]) {
        .period => {
            const access_tok = p.tok_i;
            p.tok_i += 1;
            const field_name_tok = try p.expectIdentifier();
            const field_name = try p.comp.internString(p.tokSlice(field_name_tok));

            const lhs_record_ty = lhs.qt.getRecord(p.comp) orelse {
                try p.err(field_name_tok, .offsetof_ty, .{lhs.qt});
                return error.ParsingFailed;
            };
            lhs, const offset_bits = try p.fieldAccessExtra(lhs.node, lhs_record_ty, false, &.{
                .base_qt = base_qt,
                .target_name = field_name,
                .access_tok = access_tok,
                .name_tok = field_name_tok,
                .check_deprecated = false,
            });
            total_offset += @intCast(offset_bits);
        },
        .l_bracket => {
            const l_bracket_tok = p.tok_i;
            p.tok_i += 1;
            var index = try p.expect(expr);
            _ = try p.expectClosing(l_bracket_tok, .r_bracket);

            const array_ty = lhs.qt.get(p.comp, .array) orelse {
                try p.err(l_bracket_tok, .offsetof_array, .{lhs.qt});
                return error.ParsingFailed;
            };
            var ptr = lhs;
            try ptr.lvalConversion(p, l_bracket_tok);
            try index.lvalConversion(p, l_bracket_tok);

            if (!index.qt.isInvalid() and index.qt.isRealInt(p.comp)) {
                try p.checkArrayBounds(index, lhs, l_bracket_tok);
            } else if (!index.qt.isInvalid()) {
                try p.err(l_bracket_tok, .invalid_index, .{});
            }

            if (index.val.toInt(i64, p.comp)) |index_int| {
                total_offset += @as(i64, @intCast(array_ty.elem.bitSizeof(p.comp))) * index_int;
            } else {
                runtime_offset = true;
            }

            try index.saveValue(p);
            ptr.node = try p.addNode(.{ .array_access_expr = .{
                .l_bracket_tok = l_bracket_tok,
                .base = ptr.node,
                .index = index.node,
                .qt = ptr.qt,
            } });
            lhs = ptr;
        },
        else => break,
    };
    return .{
        .qt = base_qt,
        .val = if (runtime_offset)
            .{}
        else
            try Value.int(if (offset_kind == .bits) total_offset else @divExact(total_offset, 8), p.comp),
        .node = lhs.node,
    };
}

fn computeOffsetExtra(p: *Parser, node: Node.Index, offset_so_far: *Value) !Value {
    switch (node.get(&p.tree)) {
        .cast => |cast| {
            return switch (cast.kind) {
                .lval_to_rval => .{},
                else => p.computeOffsetExtra(cast.operand, offset_so_far),
            };
        },
        .paren_expr => |un| return p.computeOffsetExtra(un.operand, offset_so_far),
        .decl_ref_expr => return p.pointerValue(node, offset_so_far.*),
        .array_access_expr => |access| {
            const index_val = p.tree.value_map.get(access.index) orelse return .{};
            var size = try Value.int(access.qt.sizeof(p.comp), p.comp);
            const mul_overflow = try size.mul(size, index_val, p.comp.type_store.ptrdiff, p.comp);

            const add_overflow = try offset_so_far.add(size, offset_so_far.*, p.comp.type_store.ptrdiff, p.comp);
            _ = mul_overflow;
            _ = add_overflow;
            return p.computeOffsetExtra(access.base, offset_so_far);
        },
        .member_access_expr, .member_access_ptr_expr => |access| {
            var ty = access.base.qt(&p.tree);
            if (ty.isPointer(p.comp)) ty = ty.childType(p.comp);
            const record_ty = ty.getRecord(p.comp).?;

            const field_offset = try Value.int(@divExact(record_ty.fields[access.member_index].layout.offset_bits, 8), p.comp);
            _ = try offset_so_far.add(field_offset, offset_so_far.*, p.comp.type_store.ptrdiff, p.comp);
            return p.computeOffsetExtra(access.base, offset_so_far);
        },
        else => return .{},
    }
}

/// Compute the offset (in bytes) of an expression from a base pointer.
fn computeOffset(p: *Parser, res: Result) !Value {
    var val: Value = if (res.val.opt_ref == .none) .zero else res.val;
    return p.computeOffsetExtra(res.node, &val);
}

/// unExpr
///  : (compoundLiteral | primaryExpr) suffixExpr*
///  | '&&' IDENTIFIER
///  | ('&' | '*' | '+' | '-' | '~' | '!' | '++' | '--' | keyword_extension | keyword_imag | keyword_real) castExpr
///  | keyword_sizeof unExpr
///  | keyword_sizeof '(' typeName ')'
///  | keyword_alignof '(' typeName ')'
///  | keyword_c23_alignof '(' typeName ')'
fn unExpr(p: *Parser) Error!?Result {
    const gpa = p.comp.gpa;
    const tok = p.tok_i;
    switch (p.tok_ids[tok]) {
        .ampersand_ampersand => {
            const address_tok = p.tok_i;
            p.tok_i += 1;
            const name_tok = try p.expectIdentifier();
            try p.err(address_tok, .gnu_label_as_value, .{});
            p.contains_address_of_label = true;

            const str = p.tokSlice(name_tok);
            if (p.findLabel(str) == null) {
                try p.labels.append(gpa, .{ .unresolved_goto = name_tok });
            }

            return .{
                .node = try p.addNode(.{
                    .addr_of_label = .{
                        .label_tok = name_tok,
                        .qt = .void_pointer,
                    },
                }),
                .qt = .void_pointer,
            };
        },
        .ampersand => {
            if (p.in_macro) {
                try p.err(p.tok_i, .invalid_preproc_operator, .{});
                return error.ParsingFailed;
            }
            const orig_tok_i = p.tok_i;
            p.tok_i += 1;
            var operand = try p.expect(castExpr);
            var addr_val: Value = .{};

            if (p.getNode(operand.node, .member_access_expr) orelse
                p.getNode(operand.node, .member_access_ptr_expr)) |access|
            {
                if (access.isBitFieldWidth(&p.tree) != null) try p.err(tok, .addr_of_bitfield, .{});
                const lhs_qt = access.base.qt(&p.tree);
                if (lhs_qt.hasAttribute(p.comp, .@"packed")) {
                    const record_ty = lhs_qt.getRecord(p.comp).?;
                    try p.err(orig_tok_i, .packed_member_address, .{
                        record_ty.fields[access.member_index].name.lookup(p.comp),
                        record_ty.name.lookup(p.comp),
                    });
                }
            }
            if (!operand.qt.isInvalid()) {
                if (!p.tree.isLval(operand.node)) {
                    try p.err(tok, .addr_of_rvalue, .{});
                }
                addr_val = try p.computeOffset(operand);

                operand.qt = try p.comp.type_store.put(gpa, .{ .pointer = .{
                    .child = operand.qt,
                    .decayed = null,
                } });
            }
            if (p.getNode(operand.node, .decl_ref_expr)) |decl_ref| {
                switch (decl_ref.decl.get(&p.tree)) {
                    .variable => |variable| {
                        if (variable.storage_class == .register) try p.err(tok, .addr_of_register, .{});
                    },
                    else => {},
                }
            } else if (p.getNode(operand.node, .compound_literal_expr)) |literal| {
                switch (literal.storage_class) {
                    .register => try p.err(tok, .addr_of_register, .{}),
                    else => {},
                }
            }

            try operand.saveValue(p);
            try operand.un(p, .addr_of_expr, tok);
            operand.val = addr_val;
            return operand;
        },
        .asterisk => {
            p.tok_i += 1;
            var operand = try p.expect(castExpr);

            switch (operand.qt.base(p.comp).type) {
                .array, .func, .pointer => {
                    try operand.lvalConversion(p, tok);
                    operand.qt = operand.qt.childType(p.comp);
                    operand.val = .{};
                },
                else => {
                    try p.err(tok, .indirection_ptr, .{});
                },
            }

            if (operand.qt.hasIncompleteSize(p.comp) and !operand.qt.is(p.comp, .void)) {
                try p.err(tok, .deref_incomplete_ty_ptr, .{operand.qt});
            }

            operand.qt = operand.qt.unqualified();
            try operand.un(p, .deref_expr, tok);
            return operand;
        },
        .plus => {
            p.tok_i += 1;

            var operand = try p.expect(castExpr);
            try operand.lvalConversion(p, tok);
            const scalar_qt = if (operand.qt.get(p.comp, .vector)) |vec| vec.elem else operand.qt;
            if (!scalar_qt.isInt(p.comp) and !scalar_qt.isFloat(p.comp))
                try p.err(tok, .invalid_argument_un, .{operand.qt});

            try operand.usualUnaryConversion(p, tok);

            return operand;
        },
        .minus => {
            p.tok_i += 1;

            var operand = try p.expect(castExpr);
            try operand.lvalConversion(p, tok);
            const scalar_qt = if (operand.qt.get(p.comp, .vector)) |vec| vec.elem else operand.qt;
            if (!scalar_qt.isInt(p.comp) and !scalar_qt.isFloat(p.comp))
                try p.err(tok, .invalid_argument_un, .{operand.qt});

            try operand.usualUnaryConversion(p, tok);
            if (operand.val.isArithmetic(p.comp)) {
                _ = try operand.val.negate(operand.val, operand.qt, p.comp);
            } else {
                operand.val = .{};
            }
            try operand.un(p, .negate_expr, tok);
            return operand;
        },
        .plus_plus => {
            p.tok_i += 1;

            var operand = try p.expect(castExpr);
            const scalar_kind = operand.qt.scalarKind(p.comp);
            if (scalar_kind == .void_pointer)
                try p.err(tok, .gnu_pointer_arith, .{});
            if (scalar_kind == .none)
                try p.err(tok, .invalid_argument_un, .{operand.qt});
            if (!scalar_kind.isReal())
                try p.err(p.tok_i, .complex_prefix_postfix_op, .{operand.qt});

            if (!p.tree.isLval(operand.node) or operand.qt.@"const") {
                try p.err(tok, .not_assignable, .{});
                return error.ParsingFailed;
            }
            try operand.usualUnaryConversion(p, tok);

            if (operand.val.is(.int, p.comp) or operand.val.is(.int, p.comp)) {
                if (try operand.val.add(operand.val, .one, operand.qt, p.comp))
                    try p.err(tok, .overflow, .{operand});
            } else {
                operand.val = .{};
            }

            try operand.un(p, .pre_inc_expr, tok);
            return operand;
        },
        .minus_minus => {
            p.tok_i += 1;

            var operand = try p.expect(castExpr);
            const scalar_kind = operand.qt.scalarKind(p.comp);
            if (scalar_kind == .void_pointer)
                try p.err(tok, .gnu_pointer_arith, .{});
            if (scalar_kind == .none)
                try p.err(tok, .invalid_argument_un, .{operand.qt});
            if (!scalar_kind.isReal())
                try p.err(p.tok_i, .complex_prefix_postfix_op, .{operand.qt});

            if (!p.tree.isLval(operand.node) or operand.qt.@"const") {
                try p.err(tok, .not_assignable, .{});
                return error.ParsingFailed;
            }
            try operand.usualUnaryConversion(p, tok);

            if (operand.val.is(.int, p.comp) or operand.val.is(.int, p.comp)) {
                if (try operand.val.decrement(operand.val, operand.qt, p.comp))
                    try p.err(tok, .overflow, .{operand});
            } else {
                operand.val = .{};
            }

            try operand.un(p, .pre_dec_expr, tok);
            return operand;
        },
        .tilde => {
            p.tok_i += 1;

            var operand = try p.expect(castExpr);
            try operand.lvalConversion(p, tok);
            try operand.usualUnaryConversion(p, tok);

            const scalar_qt = if (operand.qt.get(p.comp, .vector)) |vec| vec.elem else operand.qt;
            const scalar_kind = scalar_qt.scalarKind(p.comp);
            if (!scalar_kind.isReal()) {
                try p.err(tok, .complex_conj, .{operand.qt});
                if (operand.val.is(.complex, p.comp)) {
                    operand.val = try operand.val.complexConj(operand.qt, p.comp);
                }
            } else if (scalar_kind.isInt()) {
                if (operand.val.is(.int, p.comp)) {
                    operand.val = try operand.val.bitNot(operand.qt, p.comp);
                }
            } else {
                try p.err(tok, .invalid_argument_un, .{operand.qt});
                operand.val = .{};
            }
            try operand.un(p, .bit_not_expr, tok);
            return operand;
        },
        .bang => {
            p.tok_i += 1;

            var operand = try p.expect(castExpr);
            try operand.lvalConversion(p, tok);
            if (operand.qt.scalarKind(p.comp) == .none)
                try p.err(tok, .invalid_argument_un, .{operand.qt});

            try operand.usualUnaryConversion(p, tok);
            if (operand.val.is(.int, p.comp)) {
                operand.val = Value.fromBool(!operand.val.toBool(p.comp));
            } else if (operand.val.opt_ref == .null) {
                operand.val = .one;
            } else {
                operand.val = .{};
                if (operand.qt.get(p.comp, .pointer)) |pointer_ty| {
                    if (pointer_ty.decayed != null) operand.val = .zero;
                }
            }
            operand.qt = .int;
            try operand.un(p, .bool_not_expr, tok);
            return operand;
        },
        .keyword_sizeof => {
            p.tok_i += 1;
            const expected_paren = p.tok_i;

            var has_expr = false;
            var res: Result = .{
                .node = undefined, // check has_expr
            };
            if (try p.typeName()) |qt| {
                res.qt = qt;
                try p.err(expected_paren, .expected_parens_around_typename, .{});
            } else if (p.eatToken(.l_paren)) |l_paren| {
                if (try p.typeName()) |ty| {
                    res.qt = ty;
                    try p.expectClosing(l_paren, .r_paren);
                } else {
                    p.tok_i = expected_paren;
                    res = try p.parseNoEval(unExpr);
                    has_expr = true;
                }
            } else {
                res = try p.parseNoEval(unExpr);
                has_expr = true;
            }
            const operand_qt = res.qt;

            if (res.qt.isInvalid()) {
                res.val = .{};
            } else {
                const base_type = res.qt.base(p.comp);
                switch (base_type.type) {
                    .void => try p.err(tok, .pointer_arith_void, .{"sizeof"}),
                    .pointer => |pointer_ty| if (pointer_ty.decayed) |decayed_qt| {
                        try p.err(tok, .sizeof_array_arg, .{ res.qt, decayed_qt });
                    },
                    else => {},
                }

                if (base_type.qt.sizeofOrNull(p.comp)) |size| {
                    if (size == 0 and p.comp.langopts.emulate == .msvc) {
                        try p.err(tok, .sizeof_returns_zero, .{});
                    }
                    res.val = try Value.int(size, p.comp);
                    res.qt = p.comp.type_store.size;
                } else {
                    res.val = .{};
                    if (res.qt.hasIncompleteSize(p.comp)) {
                        try p.err(expected_paren - 1, .invalid_sizeof, .{res.qt});
                        res.qt = .invalid;
                    } else {
                        res.qt = p.comp.type_store.size;
                    }
                }
            }

            res.node = try p.addNode(.{ .sizeof_expr = .{
                .op_tok = tok,
                .qt = res.qt,
                .expr = if (has_expr) res.node else null,
                .operand_qt = operand_qt,
            } });
            return res;
        },
        .keyword_alignof,
        .keyword_alignof1,
        .keyword_alignof2,
        .keyword_c23_alignof,
        => {
            p.tok_i += 1;
            const expected_paren = p.tok_i;

            var has_expr = false;
            var res: Result = .{
                .node = undefined, // check has_expr
            };
            if (try p.typeName()) |qt| {
                res.qt = qt;
                try p.err(expected_paren, .expected_parens_around_typename, .{});
            } else if (p.eatToken(.l_paren)) |l_paren| {
                if (try p.typeName()) |qt| {
                    res.qt = qt;
                    try p.expectClosing(l_paren, .r_paren);
                } else {
                    p.tok_i = expected_paren;
                    res = try p.parseNoEval(unExpr);
                    has_expr = true;

                    try p.err(expected_paren, .alignof_expr, .{});
                }
            } else {
                res = try p.parseNoEval(unExpr);
                has_expr = true;

                try p.err(expected_paren, .alignof_expr, .{});
            }
            const operand_qt = res.qt;

            if (res.qt.is(p.comp, .void)) {
                try p.err(tok, .pointer_arith_void, .{"alignof"});
            }

            if (res.qt.sizeofOrNull(p.comp) != null) {
                res.val = try Value.int(res.qt.alignof(p.comp), p.comp);
                res.qt = p.comp.type_store.size;
            } else if (!res.qt.isInvalid()) {
                try p.err(expected_paren, .invalid_alignof, .{res.qt});
                res.qt = .invalid;
            }

            res.node = try p.addNode(.{ .alignof_expr = .{
                .op_tok = tok,
                .qt = res.qt,
                .expr = if (has_expr) res.node else null,
                .operand_qt = operand_qt,
            } });
            return res;
        },
        .keyword_extension => {
            p.tok_i += 1;
            const saved_extension = p.extension_suppressed;
            defer p.extension_suppressed = saved_extension;
            p.extension_suppressed = true;

            return try p.expect(castExpr);
        },
        .keyword_imag1, .keyword_imag2 => {
            const imag_tok = p.tok_i;
            p.tok_i += 1;

            var operand = try p.expect(castExpr);
            try operand.lvalConversion(p, tok);
            if (operand.qt.isInvalid()) return operand;

            const scalar_kind = operand.qt.scalarKind(p.comp);
            if (!scalar_kind.isArithmetic()) {
                try p.err(imag_tok, .invalid_imag, .{operand.qt});
            }
            if (!scalar_kind.isReal()) {
                operand.val = try operand.val.imaginaryPart(p.comp);
            } else switch (p.comp.langopts.emulate) {
                .msvc => {}, // Doesn't support `_Complex` or `__imag` in the first place
                .gcc => operand.val = .zero,
                .clang => {
                    if (operand.val.is(.int, p.comp) or operand.val.is(.float, p.comp)) {
                        operand.val = .zero;
                    } else {
                        operand.val = .{};
                    }
                },
            }
            // convert _Complex T to T
            operand.qt = operand.qt.toReal(p.comp);
            try operand.un(p, .imag_expr, tok);
            return operand;
        },
        .keyword_real1, .keyword_real2 => {
            const real_tok = p.tok_i;
            p.tok_i += 1;

            var operand = try p.expect(castExpr);
            try operand.lvalConversion(p, tok);
            if (operand.qt.isInvalid()) return operand;
            if (!operand.qt.isInt(p.comp) and !operand.qt.isFloat(p.comp)) {
                try p.err(real_tok, .invalid_real, .{operand.qt});
            }
            // convert _Complex T to T
            operand.qt = operand.qt.toReal(p.comp);
            operand.val = try operand.val.realPart(p.comp);
            try operand.un(p, .real_expr, tok);
            return operand;
        },
        else => {
            var lhs = (try p.compoundLiteral(null, null)) orelse
                (try p.primaryExpr()) orelse
                return null;

            while (try p.suffixExpr(lhs)) |suffix| {
                lhs = suffix;
            }
            return lhs;
        },
    }
}

/// compoundLiteral
///  : '(' storageClassSpec* type_name ')' '{' initializer_list '}'
///  | '(' storageClassSpec* type_name ')' '{' initializer_list ',' '}'
fn compoundLiteral(p: *Parser, qt_opt: ?QualType, opt_l_paren: ?TokenIndex) Error!?Result {
    const l_paren, const d = if (qt_opt) |some| .{ opt_l_paren.?, DeclSpec{ .qt = some } } else blk: {
        const l_paren = p.eatToken(.l_paren) orelse return null;

        var d: DeclSpec = .{ .qt = .invalid };
        const any = if (p.comp.langopts.standard.atLeast(.c23))
            try p.storageClassSpec(&d)
        else
            false;

        switch (d.storage_class) {
            .auto, .@"extern", .typedef => |tok| {
                try p.err(tok, .invalid_compound_literal_storage_class, .{@tagName(d.storage_class)});
                d.storage_class = .none;
            },
            .register => if (p.func.qt == null) try p.err(p.tok_i, .illegal_storage_on_global, .{}),
            else => {},
        }

        d.qt = (try p.typeName()) orelse {
            p.tok_i = l_paren;
            if (any) {
                try p.err(p.tok_i, .expected_type, .{});
                return error.ParsingFailed;
            }
            return null;
        };
        try p.expectClosing(l_paren, .r_paren);
        break :blk .{ l_paren, d };
    };
    var qt = d.qt;

    var incomplete_array_ty = false;
    switch (qt.base(p.comp).type) {
        .func => try p.err(p.tok_i, .func_init, .{}),
        .array => |array_ty| if (array_ty.len == .variable) {
            try p.err(p.tok_i, .vla_init, .{});
        } else {
            incomplete_array_ty = array_ty.len == .incomplete;
        },
        else => if (qt.hasIncompleteSize(p.comp)) {
            try p.err(p.tok_i, .variable_incomplete_ty, .{qt});
            return error.ParsingFailed;
        },
    }

    const init_context = p.init_context;
    defer p.init_context = init_context;
    p.init_context = d.initContext(p);
    var init_list_expr = try p.initializer(qt);
    if (d.constexpr) |_| {
        // TODO error if not constexpr
    }

    if (!incomplete_array_ty) init_list_expr.qt = qt;

    init_list_expr.node = try p.addNode(.{ .compound_literal_expr = .{
        .l_paren_tok = l_paren,
        .storage_class = switch (d.storage_class) {
            .register => .register,
            .static => .static,
            else => .auto,
        },
        .thread_local = d.thread_local != null,
        .initializer = init_list_expr.node,
        .qt = init_list_expr.qt,
    } });
    return init_list_expr;
}

/// suffixExpr
///  : '[' expr ']'
///  | '(' argumentExprList? ')'
///  | '.' IDENTIFIER
///  | '->' IDENTIFIER
///  | '++'
///  | '--'
/// argumentExprList : assignExpr (',' assignExpr)*
fn suffixExpr(p: *Parser, lhs: Result) Error!?Result {
    switch (p.tok_ids[p.tok_i]) {
        .l_paren => return try p.callExpr(lhs),
        .plus_plus => {
            defer p.tok_i += 1;

            var operand = lhs;
            const scalar_kind = operand.qt.scalarKind(p.comp);
            if (scalar_kind == .void_pointer)
                try p.err(p.tok_i, .gnu_pointer_arith, .{});
            if (scalar_kind == .none)
                try p.err(p.tok_i, .invalid_argument_un, .{operand.qt});
            if (!scalar_kind.isReal())
                try p.err(p.tok_i, .complex_prefix_postfix_op, .{operand.qt});

            if (!p.tree.isLval(operand.node) or operand.qt.@"const") {
                try p.err(p.tok_i, .not_assignable, .{});
                return error.ParsingFailed;
            }
            try operand.usualUnaryConversion(p, p.tok_i);

            try operand.un(p, .post_inc_expr, p.tok_i);
            return operand;
        },
        .minus_minus => {
            defer p.tok_i += 1;

            var operand = lhs;
            const scalar_kind = operand.qt.scalarKind(p.comp);
            if (scalar_kind == .void_pointer)
                try p.err(p.tok_i, .gnu_pointer_arith, .{});
            if (scalar_kind == .none)
                try p.err(p.tok_i, .invalid_argument_un, .{operand.qt});
            if (!scalar_kind.isReal())
                try p.err(p.tok_i, .complex_prefix_postfix_op, .{operand.qt});

            if (!p.tree.isLval(operand.node) or operand.qt.@"const") {
                try p.err(p.tok_i, .not_assignable, .{});
                return error.ParsingFailed;
            }
            try operand.usualUnaryConversion(p, p.tok_i);

            try operand.un(p, .post_dec_expr, p.tok_i);
            return operand;
        },
        .l_bracket => {
            const l_bracket = p.tok_i;
            p.tok_i += 1;
            var index = try p.expect(expr);
            try p.expectClosing(l_bracket, .r_bracket);

            const array_before_conversion = lhs;
            const index_before_conversion = index;
            var ptr = lhs;
            try ptr.lvalConversion(p, l_bracket);
            try index.lvalConversion(p, l_bracket);
            if (ptr.qt.get(p.comp, .pointer)) |pointer_ty| {
                ptr.qt = pointer_ty.child;
                if (index.qt.isRealInt(p.comp)) {
                    try p.checkArrayBounds(index_before_conversion, array_before_conversion, l_bracket);
                } else {
                    try p.err(l_bracket, .invalid_index, .{});
                }
            } else if (index.qt.get(p.comp, .pointer)) |pointer_ty| {
                index.qt = pointer_ty.child;
                if (ptr.qt.isRealInt(p.comp)) {
                    try p.checkArrayBounds(array_before_conversion, index_before_conversion, l_bracket);
                } else {
                    try p.err(l_bracket, .invalid_index, .{});
                }
                std.mem.swap(Result, &ptr, &index);
            } else if (ptr.qt.get(p.comp, .vector)) |vector_ty| {
                ptr = array_before_conversion;
                ptr.qt = vector_ty.elem;
                if (!index.qt.isRealInt(p.comp)) {
                    try p.err(l_bracket, .invalid_index, .{});
                }
            } else if (!index.qt.isInvalid() and !ptr.qt.isInvalid()) {
                try p.err(l_bracket, .invalid_subscript, .{});
            }

            try ptr.saveValue(p);
            try index.saveValue(p);
            ptr.node = try p.addNode(.{ .array_access_expr = .{
                .l_bracket_tok = l_bracket,
                .base = ptr.node,
                .index = index.node,
                .qt = ptr.qt,
            } });
            return ptr;
        },
        .period => {
            const period = p.tok_i;
            p.tok_i += 1;
            const name = try p.expectIdentifier();
            return try p.fieldAccess(lhs, name, false, period);
        },
        .arrow => {
            const arrow = p.tok_i;
            p.tok_i += 1;
            const name = try p.expectIdentifier();
            if (lhs.qt.is(p.comp, .array)) {
                var copy = lhs;
                copy.qt = try copy.qt.decay(p.comp);
                try copy.implicitCast(p, .array_to_pointer, arrow);
                return try p.fieldAccess(copy, name, true, arrow);
            }
            return try p.fieldAccess(lhs, name, true, arrow);
        },
        else => return null,
    }
}

fn fieldAccess(
    p: *Parser,
    lhs: Result,
    field_name_tok: TokenIndex,
    is_arrow: bool,
    access_tok: TokenIndex,
) !Result {
    if (lhs.qt.isInvalid()) {
        const access: Node.MemberAccess = .{
            .access_tok = access_tok,
            .qt = .invalid,
            .base = lhs.node,
            .member_index = std.math.maxInt(u32),
        };
        return .{
            .qt = .invalid,
            .node = try p.addNode(if (is_arrow)
                .{ .member_access_ptr_expr = access }
            else
                .{ .member_access_expr = access }),
        };
    }

    const expr_qt = if (lhs.qt.get(p.comp, .atomic)) |atomic| atomic else lhs.qt;
    const is_ptr = expr_qt.isPointer(p.comp);
    const expr_base_qt = if (is_ptr) expr_qt.childType(p.comp) else expr_qt;
    const record_qt = if (expr_base_qt.get(p.comp, .atomic)) |atomic| atomic else expr_base_qt;
    const record_ty = record_qt.getRecord(p.comp) orelse {
        try p.err(field_name_tok, .expected_record_ty, .{expr_qt});
        return error.ParsingFailed;
    };

    if (record_ty.layout == null) {
        // Invalid use of incomplete type, error reported elsewhere.
        if (!is_ptr) return error.ParsingFailed;

        try p.err(field_name_tok - 2, .deref_incomplete_ty_ptr, .{expr_base_qt});
        return error.ParsingFailed;
    }
    if (expr_qt != lhs.qt) try p.err(field_name_tok, .member_expr_atomic, .{lhs.qt});
    if (expr_base_qt != record_qt) try p.err(field_name_tok, .member_expr_atomic, .{expr_base_qt});

    if (is_arrow and !is_ptr) try p.err(field_name_tok, .member_expr_not_ptr, .{expr_qt});
    if (!is_arrow and is_ptr) try p.err(field_name_tok, .member_expr_ptr, .{expr_qt});

    const field_name = try p.comp.internString(p.tokSlice(field_name_tok));
    const result, _ = try p.fieldAccessExtra(lhs.node, record_ty, is_arrow, &.{
        .base_qt = record_qt,
        .target_name = field_name,
        .access_tok = access_tok,
        .name_tok = field_name_tok,
        .check_deprecated = true,
    });
    return result;
}

fn fieldAccessExtra(
    p: *Parser,
    base: Node.Index,
    record_ty: Type.Record,
    is_arrow: bool,
    ctx: *const struct {
        base_qt: QualType,
        target_name: StringId,
        access_tok: TokenIndex,
        name_tok: TokenIndex,
        check_deprecated: bool,
    },
) Error!struct { Result, u64 } {
    for (record_ty.fields, 0..) |field, field_index| {
        if (field.name_tok == 0) if (field.qt.getRecord(p.comp)) |field_record_ty| {
            if (!field_record_ty.hasField(p.comp, ctx.target_name)) continue;

            const access: Node.MemberAccess = .{
                .access_tok = ctx.access_tok,
                .qt = field.qt,
                .base = base,
                .member_index = @intCast(field_index),
            };
            const inner = try p.addNode(if (is_arrow)
                .{ .member_access_ptr_expr = access }
            else
                .{ .member_access_expr = access });

            const ret, const offset_bits = try p.fieldAccessExtra(inner, field_record_ty, false, ctx);
            return .{ ret, offset_bits + field.layout.offset_bits };
        };
        if (ctx.target_name == field.name) {
            if (ctx.check_deprecated) try p.checkDeprecatedUnavailable(field.qt, ctx.name_tok, field.name_tok);

            const access: Node.MemberAccess = .{
                .access_tok = ctx.access_tok,
                .qt = field.qt,
                .base = base,
                .member_index = @intCast(field_index),
            };
            const result_node = try p.addNode(if (is_arrow)
                .{ .member_access_ptr_expr = access }
            else
                .{ .member_access_expr = access });
            return .{ .{ .qt = field.qt, .node = result_node }, field.layout.offset_bits };
        }
    }
    try p.err(ctx.name_tok, .no_such_member, .{ p.tokSlice(ctx.name_tok), ctx.base_qt });
    return error.ParsingFailed;
}

fn checkVaStartArg(p: *Parser, builtin_tok: TokenIndex, first_after: TokenIndex, param_tok: TokenIndex, arg: *Result, idx: u32) !void {
    assert(idx != 0);
    if (idx > 1) {
        try p.err(first_after, .closing_paren, .{});
        return error.ParsingFailed;
    }

    const func_qt = p.func.qt orelse {
        try p.err(builtin_tok, .va_start_not_in_func, .{});
        return;
    };
    const func_ty = func_qt.get(p.comp, .func) orelse return;
    if (func_ty.kind != .variadic or func_ty.params.len == 0) {
        return p.err(builtin_tok, .va_start_fixed_args, .{});
    }
    const last_param_name = func_ty.params[func_ty.params.len - 1].name;
    const decl_ref = p.getNode(arg.node, .decl_ref_expr);
    if (decl_ref == null or last_param_name != try p.comp.internString(p.tokSlice(decl_ref.?.name_tok))) {
        try p.err(param_tok, .va_start_not_last_param, .{});
    }
}

fn checkArithOverflowArg(p: *Parser, builtin_tok: TokenIndex, first_after: TokenIndex, param_tok: TokenIndex, arg: *Result, idx: u32) !void {
    _ = builtin_tok;
    _ = first_after;
    if (idx <= 1) {
        if (!arg.qt.isRealInt(p.comp)) {
            return p.err(param_tok, .overflow_builtin_requires_int, .{arg.qt});
        }
    } else if (idx == 2) {
        if (!arg.qt.isPointer(p.comp)) return p.err(param_tok, .overflow_result_requires_ptr, .{arg.qt});
        const child = arg.qt.childType(p.comp);
        if (child.scalarKind(p.comp) != .int or child.@"const") return p.err(param_tok, .overflow_result_requires_ptr, .{arg.qt});
    }
}

fn checkComplexArg(p: *Parser, builtin_tok: TokenIndex, first_after: TokenIndex, param_tok: TokenIndex, arg: *Result, idx: u32) !void {
    _ = builtin_tok;
    _ = first_after;
    if (idx <= 1 and !arg.qt.isFloat(p.comp)) {
        try p.err(param_tok, .not_floating_type, .{arg.qt});
    } else if (idx == 1) {
        const prev_idx = p.list_buf.items[p.list_buf.items.len - 1];
        const prev_qt = prev_idx.qt(&p.tree);
        if (!prev_qt.eql(arg.qt, p.comp)) {
            try p.err(param_tok, .argument_types_differ, .{ prev_qt, arg.qt });
        }
    }
}

fn checkElementwiseArg(
    p: *Parser,
    param_tok: TokenIndex,
    arg: *Result,
    idx: u32,
    kind: enum { sint_float, float, int, both },
) !void {
    if (idx == 0) {
        const scarlar_qt = if (arg.qt.get(p.comp, .vector)) |vec| vec.elem else arg.qt;
        const sk = scarlar_qt.scalarKind(p.comp);
        switch (kind) {
            .float => if (!sk.isFloat() or !sk.isReal()) {
                try p.err(param_tok, .elementwise_type, .{ " or a floating point type", arg.qt });
            },
            .int => if (!sk.isInt() or !sk.isReal()) {
                try p.err(param_tok, .elementwise_type, .{ " or an integer point type", arg.qt });
            },
            .sint_float => if (!((sk.isInt() and scarlar_qt.signedness(p.comp) == .signed) or sk.isFloat()) or !sk.isReal()) {
                try p.err(param_tok, .elementwise_type, .{ ", a signed integer or a floating point type", arg.qt });
            },
            .both => if (!(sk.isInt() or sk.isFloat()) or !sk.isReal()) {
                try p.err(param_tok, .elementwise_type, .{ ", an integer or a floating point type", arg.qt });
            },
        }
    } else {
        const prev_idx = p.list_buf.items[p.list_buf.items.len - 1];
        const prev_qt = prev_idx.qt(&p.tree);
        arg.coerceExtra(p, prev_qt, param_tok, .{ .arg = null }) catch |er| switch (er) {
            error.CoercionFailed => {
                try p.err(param_tok, .argument_types_differ, .{ prev_qt, arg.qt });
            },
            else => |e| return e,
        };
    }
}

fn checkNonTemporalArg(
    p: *Parser,
    param_tok: TokenIndex,
    arg: *Result,
    idx: u32,
    kind: enum { store, load },
) !void {
    if (kind == .store and idx == 0) return;
    const base_qt = if (arg.qt.get(p.comp, .pointer)) |ptr|
        ptr.child
    else
        return p.err(param_tok, .nontemporal_address_pointer, .{arg.qt});

    const scarlar_qt = if (base_qt.get(p.comp, .vector)) |vec| vec.elem else base_qt;
    const sk = scarlar_qt.scalarKind(p.comp);
    if (!(sk.isInt() or sk.isFloat()) or !sk.isReal() or sk.isPointer()) {
        try p.err(param_tok, .nontemporal_address_type, .{arg.qt});
    }

    if (kind == .store) {
        const prev_idx = p.list_buf.items[p.list_buf.items.len - 1];
        var prev_arg: Result = .{
            .node = prev_idx,
            .qt = prev_idx.qt(&p.tree),
        };
        try prev_arg.coerce(p, base_qt, prev_idx.tok(&p.tree), .{ .arg = null });
        p.list_buf.items[p.list_buf.items.len - 1] = prev_arg.node;
    }
}

fn checkSyncArg(
    p: *Parser,
    param_tok: TokenIndex,
    arg: *Result,
    idx: u32,
    max_count: u8,
) !void {
    if (idx >= max_count) return;
    if (idx == 0) {
        const ptr_ty = arg.qt.get(p.comp, .pointer) orelse
            return p.err(param_tok, .atomic_address_pointer, .{arg.qt});

        const child_sk = ptr_ty.child.scalarKind(p.comp);
        if (!((child_sk.isInt() and child_sk.isReal()) or child_sk.isPointer()))
            return p.err(param_tok, .atomic_address_type, .{arg.qt});
    } else {
        const first_idx = p.list_buf.items[p.list_buf.items.len - idx];
        const ptr_ty = first_idx.qt(&p.tree).get(p.comp, .pointer) orelse return;
        const prev_qt = ptr_ty.child;
        arg.coerceExtra(p, prev_qt, param_tok, .{ .arg = null }) catch |er| switch (er) {
            error.CoercionFailed => {
                try p.err(param_tok, .argument_types_differ, .{ prev_qt, arg.qt });
            },
            else => |e| return e,
        };
    }
}

fn callExpr(p: *Parser, lhs: Result) Error!Result {
    const gpa = p.comp.gpa;
    const l_paren = p.tok_i;
    p.tok_i += 1;

    // We cannot refer to the function type here because the pointer to
    // type_store.extra might get invalidated while parsing args.
    const func_qt, const params_len, const func_kind = blk: {
        var base_qt = lhs.qt;
        if (base_qt.get(p.comp, .pointer)) |pointer_ty| base_qt = pointer_ty.child;
        if (base_qt.isInvalid()) break :blk .{ base_qt, std.math.maxInt(usize), undefined };

        const func_type_qt = base_qt.base(p.comp);
        if (func_type_qt.type != .func) {
            try p.err(l_paren, .not_callable, .{lhs.qt});
            return error.ParsingFailed;
        }
        break :blk .{ func_type_qt.qt, func_type_qt.type.func.params.len, func_type_qt.type.func.kind };
    };

    var func = lhs;
    try func.lvalConversion(p, l_paren);

    const list_buf_top = p.list_buf.items.len;
    defer p.list_buf.items.len = list_buf_top;
    var arg_count: u32 = 0;
    var first_after = l_paren;

    const call_expr = CallExpr.init(p, lhs.node, func.node);

    while (p.eatToken(.r_paren) == null) {
        const param_tok = p.tok_i;
        if (arg_count == params_len) first_after = p.tok_i;
        var arg = try p.expect(assignExpr);

        if (call_expr.shouldPerformLvalConversion(arg_count)) {
            try arg.lvalConversion(p, param_tok);
        }
        if ((arg.qt.hasIncompleteSize(p.comp) and !arg.qt.is(p.comp, .void)) or arg.qt.isInvalid()) return error.ParsingFailed;

        if (arg_count >= params_len) {
            if (call_expr.shouldPromoteVarArg(arg_count)) switch (arg.qt.base(p.comp).type) {
                .int => |int_ty| if (int_ty == .int) try arg.castToInt(p, arg.qt.promoteInt(p.comp), param_tok),
                .float => |float_ty| if (float_ty == .double) try arg.castToFloat(p, .double, param_tok),
                else => {},
            };

            try call_expr.checkVarArg(p, first_after, param_tok, &arg, arg_count);
            try arg.saveValue(p);
            try p.list_buf.append(gpa, arg.node);
            arg_count += 1;

            _ = p.eatToken(.comma) orelse {
                try p.expectClosing(l_paren, .r_paren);
                break;
            };
            continue;
        }

        if (func_qt.get(p.comp, .func)) |func_ty| {
            const param = func_ty.params[arg_count];

            if (param.qt.get(p.comp, .pointer)) |pointer_ty| static_check: {
                const decayed_child_qt = pointer_ty.decayed orelse break :static_check;
                const param_array_ty = decayed_child_qt.get(p.comp, .array).?;
                if (param_array_ty.len != .static) break :static_check;
                const param_array_len = param_array_ty.len.static;
                const arg_array_len = arg.qt.arrayLen(p.comp);

                if (arg_array_len != null and arg_array_len.? < param_array_len) {
                    try p.err(param_tok, .array_argument_too_small, .{ arg_array_len.?, param_array_len });
                    try p.err(param.name_tok, .callee_with_static_array, .{});
                }
                if (arg.val.isZero(p.comp)) {
                    try p.err(param_tok, .non_null_argument, .{});
                    try p.err(param.name_tok, .callee_with_static_array, .{});
                }
            }

            if (call_expr.shouldCoerceArg(arg_count)) {
                try arg.coerce(p, param.qt, param_tok, .{ .arg = param.name_tok });
            }
        }
        try arg.saveValue(p);
        try p.list_buf.append(gpa, arg.node);
        arg_count += 1;

        _ = p.eatToken(.comma) orelse {
            try p.expectClosing(l_paren, .r_paren);
            break;
        };
    }
    if (func_qt.isInvalid()) {
        // Skip argument count checks.
        return try call_expr.finish(p, func_qt, list_buf_top, l_paren);
    }

    if (call_expr.paramCountOverride()) |expected| {
        if (expected != arg_count) {
            try p.err(first_after, .expected_arguments, .{ expected, arg_count });
        }
    } else switch (func_kind) {
        .normal => if (params_len != arg_count) {
            try p.err(first_after, .expected_arguments, .{ params_len, arg_count });
        },
        .variadic => if (arg_count < params_len) {
            try p.err(first_after, .expected_at_least_arguments, .{ params_len, arg_count });
        },
        .old_style => if (params_len != arg_count) {
            if (params_len == 0)
                try p.err(first_after, .passing_args_to_kr, .{})
            else
                try p.err(first_after, .expected_arguments_old, .{ params_len, arg_count });
        },
    }

    return try call_expr.finish(p, func_qt, list_buf_top, l_paren);
}

fn checkArrayBounds(p: *Parser, index: Result, array: Result, tok: TokenIndex) !void {
    if (index.val.opt_ref == .none) return;

    const array_len = array.qt.arrayLen(p.comp) orelse return;
    if (array_len == 0) return;

    if (array_len == 1) {
        if (p.getNode(array.node, .member_access_expr) orelse p.getNode(array.node, .member_access_ptr_expr)) |access| {
            var base_ty = access.base.qt(&p.tree);
            if (base_ty.get(p.comp, .pointer)) |pointer_ty| {
                base_ty = pointer_ty.child;
            }
            if (base_ty.getRecord(p.comp)) |record_ty| {
                if (access.member_index + 1 == record_ty.fields.len) {
                    if (!index.val.isZero(p.comp)) {
                        try p.err(tok, .old_style_flexible_struct, .{index});
                    }
                    return;
                }
            }
        }
    }
    const index_int = index.val.toInt(u64, p.comp) orelse std.math.maxInt(u64);
    if (index.qt.signedness(p.comp) == .unsigned) {
        if (index_int >= array_len) {
            try p.err(tok, .array_after, .{index});
        }
    } else {
        if (index.val.compare(.lt, .zero, p.comp)) {
            try p.err(tok, .array_before, .{index});
        } else if (index_int >= array_len) {
            try p.err(tok, .array_after, .{index});
        }
    }
}

/// primaryExpr
///  : IDENTIFIER
///  | keyword_true
///  | keyword_false
///  | keyword_nullptr
///  | INTEGER_LITERAL
///  | FLOAT_LITERAL
///  | IMAGINARY_LITERAL
///  | CHAR_LITERAL
///  | STRING_LITERAL
///  | '(' expr ')'
///  | genericSelection
///  | shufflevector
///  | convertvector
///  | typesCompatible
///  | chooseExpr
///  | vaStart
///  | offsetof
fn primaryExpr(p: *Parser) Error!?Result {
    if (p.eatToken(.l_paren)) |l_paren| {
        var grouped_expr = try p.expect(expr);
        try p.expectClosing(l_paren, .r_paren);
        try grouped_expr.un(p, .paren_expr, l_paren);
        return grouped_expr;
    }

    const gpa = p.comp.gpa;
    switch (p.tok_ids[p.tok_i]) {
        .identifier, .extended_identifier => {
            const name_tok = try p.expectIdentifier();
            const name = p.tokSlice(name_tok);
            const interned_name = try p.comp.internString(name);
            if (interned_name == p.auto_type_decl_name) {
                try p.err(name_tok, .auto_type_self_initialized, .{name});
                return error.ParsingFailed;
            }

            if (p.syms.findSymbol(interned_name)) |sym| {
                if (sym.kind == .typedef) {
                    try p.err(name_tok, .unexpected_type_name, .{name});
                    return error.ParsingFailed;
                }
                if (sym.out_of_scope) {
                    try p.err(name_tok, .out_of_scope_use, .{name});
                    try p.err(sym.tok, .previous_definition, .{});
                }
                try p.checkDeprecatedUnavailable(sym.qt, name_tok, sym.tok);
                if (sym.kind == .constexpr) {
                    return .{
                        .val = sym.val,
                        .qt = sym.qt,
                        .node = try p.addNode(.{
                            .decl_ref_expr = .{
                                .name_tok = name_tok,
                                .qt = sym.qt,
                                .decl = sym.node.unpack().?,
                            },
                        }),
                    };
                }
                if (sym.val.is(.int, p.comp)) {
                    switch (p.const_decl_folding) {
                        .gnu_folding_extension => try p.err(name_tok, .const_decl_folded, .{}),
                        .gnu_vla_folding_extension => try p.err(name_tok, .const_decl_folded_vla, .{}),
                        else => {},
                    }
                }

                const node = try p.addNode(if (sym.kind == .enumeration)
                    .{ .enumeration_ref = .{
                        .name_tok = name_tok,
                        .qt = sym.qt,
                        .decl = sym.node.unpack().?,
                    } }
                else
                    .{ .decl_ref_expr = .{
                        .name_tok = name_tok,
                        .qt = sym.qt,
                        .decl = sym.node.unpack().?,
                    } });

                const res: Result = .{
                    .val = if (p.const_decl_folding == .no_const_decl_folding and sym.kind != .enumeration) Value{} else sym.val,
                    .qt = sym.qt,
                    .node = node,
                };
                try res.putValue(p);
                return res;
            }

            // Check if this is a builtin call.
            if (try p.comp.builtins.getOrCreate(p.comp, name)) |some| {
                for (p.tok_ids[p.tok_i..]) |id| switch (id) {
                    .r_paren => {}, // closing grouped expr
                    .l_paren => break, // beginning of a call
                    else => {
                        try p.err(name_tok, .builtin_must_be_called, .{});
                        return error.ParsingFailed;
                    },
                };
                if (some.header != .none) {
                    try p.err(name_tok, .implicit_builtin, .{name});
                    try p.err(name_tok, .implicit_builtin_header_note, .{
                        @tagName(some.header), name,
                    });
                }

                switch (some.tag) {
                    .common => |tag| switch (tag) {
                        .__builtin_choose_expr => return try p.builtinChooseExpr(),
                        .__builtin_va_arg => return try p.builtinVaArg(name_tok),
                        .__builtin_offsetof => return try p.builtinOffsetof(name_tok, .bytes),
                        .__builtin_bitoffsetof => return try p.builtinOffsetof(name_tok, .bits),
                        .__builtin_types_compatible_p => return try p.typesCompatible(name_tok),
                        .__builtin_convertvector => return try p.convertvector(name_tok),
                        .__builtin_shufflevector => return try p.shufflevector(name_tok),
                        .__builtin_bit_cast => return try p.builtinBitCast(name_tok),
                        else => {},
                    },
                    else => {},
                }

                return .{
                    .qt = some.qt,
                    .node = try p.addNode(.{
                        .builtin_ref = .{
                            .name_tok = name_tok,
                            .qt = some.qt,
                        },
                    }),
                };
            }

            // Check for unknown builtin or implicit function declaration.
            if (p.tok_ids[p.tok_i] == .l_paren and !p.comp.langopts.standard.atLeast(.c23)) {
                // allow implicitly declaring functions before C99 like `puts("foo")`
                if (mem.startsWith(u8, name, "__builtin_"))
                    try p.err(name_tok, .unknown_builtin, .{name})
                else
                    try p.err(name_tok, .implicit_func_decl, .{name});

                const func_qt = try p.comp.type_store.put(gpa, .{ .func = .{
                    .return_type = .int,
                    .kind = .old_style,
                    .params = &.{},
                } });
                const node = try p.addNode(.{
                    .function = .{
                        .name_tok = name_tok,
                        .qt = func_qt,
                        .static = false,
                        .@"inline" = false,
                        .definition = null,
                        .body = null,
                    },
                });

                try p.decl_buf.append(gpa, node);
                try p.syms.declareSymbol(p, interned_name, func_qt, name_tok, node);

                return .{
                    .qt = func_qt,
                    .node = try p.addNode(.{
                        .decl_ref_expr = .{
                            .name_tok = name_tok,
                            .qt = func_qt,
                            .decl = node,
                        },
                    }),
                };
            }

            try p.err(name_tok, .undeclared_identifier, .{p.tokSlice(name_tok)});
            return error.ParsingFailed;
        },
        .keyword_true, .keyword_false => |id| {
            const tok_i = p.tok_i;
            p.tok_i += 1;
            const res: Result = .{
                .val = .fromBool(id == .keyword_true),
                .qt = .bool,
                .node = try p.addNode(.{
                    .bool_literal = .{
                        .qt = .bool,
                        .literal_tok = tok_i,
                    },
                }),
            };
            std.debug.assert(!p.in_macro); // Should have been replaced with .one / .zero
            try res.putValue(p);
            return res;
        },
        .keyword_nullptr => {
            defer p.tok_i += 1;
            try p.err(p.tok_i, .pre_c23_compat, .{"'nullptr'"});
            return .{
                .val = .null,
                .qt = .nullptr_t,
                .node = try p.addNode(.{
                    .nullptr_literal = .{
                        .qt = .nullptr_t,
                        .literal_tok = p.tok_i,
                    },
                }),
            };
        },
        .macro_func, .macro_function => {
            defer p.tok_i += 1;
            var ty: QualType = undefined;
            var tok = p.tok_i;

            if (p.func.ident) |some| {
                ty = some.qt;
                tok = some.node.get(&p.tree).variable.name_tok;
            } else if (p.func.qt) |_| {
                const strings_top = p.strings.items.len;
                defer p.strings.items.len = strings_top;

                const name = p.tokSlice(p.func.name);
                try p.strings.ensureUnusedCapacity(gpa, name.len + 1);

                p.strings.appendSliceAssumeCapacity(name);
                p.strings.appendAssumeCapacity(0);
                const predef = try p.makePredefinedIdentifier(p.strings.items[strings_top..]);
                ty = predef.qt;
                p.func.ident = predef;
            } else {
                const predef = try p.makePredefinedIdentifier("\x00");
                ty = predef.qt;
                p.func.ident = predef;
                try p.decl_buf.append(gpa, predef.node);
            }
            if (p.func.qt == null) try p.err(p.tok_i, .predefined_top_level, .{});

            return .{
                .qt = ty,
                .node = try p.addNode(.{
                    .decl_ref_expr = .{
                        .name_tok = tok,
                        .qt = ty,
                        .decl = p.func.ident.?.node,
                    },
                }),
            };
        },
        .macro_pretty_func => {
            defer p.tok_i += 1;
            var qt: QualType = undefined;
            if (p.func.pretty_ident) |some| {
                qt = some.qt;
            } else if (p.func.qt) |func_qt| {
                var sf = std.heap.stackFallback(1024, gpa);
                var allocating: Io.Writer.Allocating = .init(sf.get());
                defer allocating.deinit();

                func_qt.printNamed(p.tokSlice(p.func.name), p.comp, &allocating.writer) catch return error.OutOfMemory;
                allocating.writer.writeByte(0) catch return error.OutOfMemory;

                const predef = try p.makePredefinedIdentifier(allocating.written());
                qt = predef.qt;
                p.func.pretty_ident = predef;
            } else {
                const predef = try p.makePredefinedIdentifier("top level\x00");
                qt = predef.qt;
                p.func.pretty_ident = predef;
                try p.decl_buf.append(gpa, predef.node);
            }
            if (p.func.qt == null) try p.err(p.tok_i, .predefined_top_level, .{});
            return .{
                .qt = qt,
                .node = try p.addNode(.{
                    .decl_ref_expr = .{
                        .name_tok = p.tok_i,
                        .qt = qt,
                        .decl = p.func.pretty_ident.?.node,
                    },
                }),
            };
        },
        .string_literal,
        .string_literal_utf_16,
        .string_literal_utf_8,
        .string_literal_utf_32,
        .string_literal_wide,
        .unterminated_string_literal,
        => return try p.stringLiteral(),
        .char_literal,
        .char_literal_utf_8,
        .char_literal_utf_16,
        .char_literal_utf_32,
        .char_literal_wide,
        .empty_char_literal,
        .unterminated_char_literal,
        => return try p.charLiteral(),
        .zero => {
            defer p.tok_i += 1;
            const int_qt: QualType = if (p.in_macro) p.comp.type_store.intmax else .int;
            const res: Result = .{
                .val = .zero,
                .qt = int_qt,
                .node = try p.addNode(.{ .int_literal = .{ .qt = int_qt, .literal_tok = p.tok_i } }),
            };
            try res.putValue(p);
            return res;
        },
        .one => {
            defer p.tok_i += 1;
            const int_qt: QualType = if (p.in_macro) p.comp.type_store.intmax else .int;
            const res: Result = .{
                .val = .one,
                .qt = int_qt,
                .node = try p.addNode(.{ .int_literal = .{ .qt = int_qt, .literal_tok = p.tok_i } }),
            };
            try res.putValue(p);
            return res;
        },
        .pp_num => return try p.ppNum(),
        .embed_byte => {
            assert(!p.in_macro);
            const loc = p.pp.tokens.items(.loc)[p.tok_i];
            defer p.tok_i += 1;
            const buf = p.comp.getSource(.generated).buf[loc.byte_offset..];
            var byte: u8 = buf[0] - '0';
            for (buf[1..]) |c| {
                if (!std.ascii.isDigit(c)) break;
                byte *= 10;
                byte += c - '0';
            }
            const res: Result = .{
                .val = try Value.int(byte, p.comp),
                .qt = .int,
                .node = try p.addNode(.{ .int_literal = .{ .qt = .int, .literal_tok = p.tok_i } }),
            };
            try res.putValue(p);
            return res;
        },
        .keyword_generic => return p.genericSelection(),
        else => return null,
    }
}

fn makePredefinedIdentifier(p: *Parser, slice: []const u8) !Result {
    const gpa = p.comp.gpa;
    const array_qt = try p.comp.type_store.put(gpa, .{ .array = .{
        .elem = .{ .@"const" = true, ._index = .int_char },
        .len = .{ .fixed = slice.len },
    } });

    const val = try Value.intern(p.comp, .{ .bytes = slice });

    const str_lit = try p.addNode(.{ .string_literal_expr = .{ .qt = array_qt, .literal_tok = p.tok_i, .kind = .ascii } });
    if (!p.in_macro) try p.tree.value_map.put(gpa, str_lit, val);

    return .{ .qt = array_qt, .node = try p.addNode(.{
        .variable = .{
            .name_tok = p.tok_i,
            .qt = array_qt,
            .storage_class = .static,
            .thread_local = false,
            .implicit = true,
            .initializer = str_lit,
            .definition = null,
        },
    }) };
}

fn stringLiteral(p: *Parser) Error!Result {
    const gpa = p.comp.gpa;
    const string_start = p.tok_i;
    var string_end = p.tok_i;
    var string_kind: text_literal.Kind = .char;
    while (text_literal.Kind.classify(p.tok_ids[string_end], .string_literal)) |next| : (string_end += 1) {
        string_kind = string_kind.concat(next) catch {
            try p.err(string_end, .unsupported_str_cat, .{});
            while (p.tok_ids[p.tok_i].isStringLiteral()) : (p.tok_i += 1) {}
            return error.ParsingFailed;
        };
        if (string_kind == .unterminated) {
            // Diagnostic issued in preprocessor.
            p.tok_i = string_end + 1;
            return error.ParsingFailed;
        }
    }
    const count = string_end - p.tok_i;
    assert(count > 0);

    const char_width = string_kind.charUnitSize(p.comp);

    const strings_top = p.strings.items.len;
    defer p.strings.items.len = strings_top;

    const literal_start = mem.alignForward(usize, strings_top, @intFromEnum(char_width));
    try p.strings.resize(gpa, literal_start);

    while (p.tok_i < string_end) : (p.tok_i += 1) {
        const this_kind = text_literal.Kind.classify(p.tok_ids[p.tok_i], .string_literal).?;
        const slice = this_kind.contentSlice(p.tokSlice(p.tok_i));
        var char_literal_parser: text_literal.Parser = .{
            .comp = p.comp,
            .literal = slice,
            .kind = this_kind,
            .max_codepoint = 0x10ffff,
            .loc = p.pp.tokens.items(.loc)[p.tok_i],
            .expansion_locs = p.pp.expansionSlice(p.tok_i),
            .incorrect_encoding_is_error = count > 1,
        };

        try p.strings.ensureUnusedCapacity(gpa, (slice.len + 1) * @intFromEnum(char_width)); // +1 for null terminator
        while (try char_literal_parser.next()) |item| switch (item) {
            .value => |v| {
                switch (char_width) {
                    .@"1" => p.strings.appendAssumeCapacity(@intCast(v)),
                    .@"2" => {
                        const word: u16 = @intCast(v);
                        p.strings.appendSliceAssumeCapacity(mem.asBytes(&word));
                    },
                    .@"4" => p.strings.appendSliceAssumeCapacity(mem.asBytes(&v)),
                }
            },
            .codepoint => |c| {
                switch (char_width) {
                    .@"1" => {
                        var buf: [4]u8 = undefined;
                        const written = std.unicode.utf8Encode(c, &buf) catch unreachable;
                        const encoded = buf[0..written];
                        p.strings.appendSliceAssumeCapacity(encoded);
                    },
                    .@"2" => {
                        var utf16_buf: [2]u16 = undefined;
                        var utf8_buf: [4]u8 = undefined;
                        const utf8_written = std.unicode.utf8Encode(c, &utf8_buf) catch unreachable;
                        const utf16_written = std.unicode.utf8ToUtf16Le(&utf16_buf, utf8_buf[0..utf8_written]) catch unreachable;
                        const bytes = std.mem.sliceAsBytes(utf16_buf[0..utf16_written]);
                        p.strings.appendSliceAssumeCapacity(bytes);
                    },
                    .@"4" => {
                        const val: u32 = c;
                        p.strings.appendSliceAssumeCapacity(mem.asBytes(&val));
                    },
                }
            },
            .improperly_encoded => |bytes| {
                if (count > 1) {
                    return error.ParsingFailed;
                }
                p.strings.appendSliceAssumeCapacity(bytes);
            },
            .utf8_text => |view| {
                switch (char_width) {
                    .@"1" => p.strings.appendSliceAssumeCapacity(view.bytes),
                    .@"2" => {
                        const capacity_slice: []align(@alignOf(u16)) u8 = @alignCast(p.strings.allocatedSlice()[literal_start..]);
                        const dest_len = std.mem.alignBackward(usize, capacity_slice.len, 2);
                        const dest = std.mem.bytesAsSlice(u16, capacity_slice[0..dest_len]);
                        const words_written = std.unicode.utf8ToUtf16Le(dest, view.bytes) catch unreachable;
                        p.strings.resize(gpa, p.strings.items.len + words_written * 2) catch unreachable;
                    },
                    .@"4" => {
                        var it = view.iterator();
                        while (it.nextCodepoint()) |codepoint| {
                            const val: u32 = codepoint;
                            p.strings.appendSliceAssumeCapacity(mem.asBytes(&val));
                        }
                    },
                }
            },
        };
    }
    p.strings.appendNTimesAssumeCapacity(0, @intFromEnum(char_width));
    const slice = p.strings.items[literal_start..];

    // TODO this won't do anything if there is a cache hit
    const interned_align = mem.alignForward(
        usize,
        p.comp.interner.strings.items.len,
        string_kind.internalStorageAlignment(p.comp),
    );
    try p.comp.interner.strings.resize(gpa, interned_align);

    const val = try Value.intern(p.comp, .{ .bytes = slice });

    const array_qt = try p.comp.type_store.put(gpa, .{ .array = .{
        .elem = string_kind.elementType(p.comp),
        .len = .{ .fixed = @divExact(slice.len, @intFromEnum(char_width)) },
    } });
    const res: Result = .{
        .qt = array_qt,
        .val = val,
        .node = try p.addNode(.{ .string_literal_expr = .{
            .literal_tok = string_start,
            .qt = array_qt,
            .kind = switch (string_kind) {
                .char, .unterminated => .ascii,
                .wide => .wide,
                .utf_8 => .utf8,
                .utf_16 => .utf16,
                .utf_32 => .utf32,
            },
        } }),
    };
    try res.putValue(p);
    return res;
}

fn charLiteral(p: *Parser) Error!?Result {
    defer p.tok_i += 1;
    const tok_id = p.tok_ids[p.tok_i];
    const char_kind = text_literal.Kind.classify(tok_id, .char_literal) orelse {
        if (tok_id == .empty_char_literal) {
            try p.err(p.tok_i, .empty_char_literal_error, .{});
        } else if (tok_id == .unterminated_char_literal) {
            try p.err(p.tok_i, .unterminated_char_literal_error, .{});
        } else unreachable;
        return .{
            .qt = .int,
            .val = .zero,
            .node = try p.addNode(.{ .char_literal = .{ .qt = .int, .literal_tok = p.tok_i, .kind = .ascii } }),
        };
    };
    if (char_kind == .utf_8) try p.err(p.tok_i, .u8_char_lit, .{});
    var val: u32 = 0;

    const gpa = p.comp.gpa;
    const slice = char_kind.contentSlice(p.tokSlice(p.tok_i));

    var is_multichar = false;
    if (slice.len == 1 and std.ascii.isAscii(slice[0])) {
        // fast path: single unescaped ASCII char
        val = slice[0];
    } else {
        const max_codepoint = char_kind.maxCodepoint(p.comp);
        var char_literal_parser: text_literal.Parser = .{
            .comp = p.comp,
            .literal = slice,
            .kind = char_kind,
            .max_codepoint = max_codepoint,
            .loc = p.pp.tokens.items(.loc)[p.tok_i],
            .expansion_locs = p.pp.expansionSlice(p.tok_i),
        };

        const max_chars_expected = 4;
        var sf = std.heap.stackFallback(max_chars_expected * @sizeOf(u32), gpa);
        const allocator = sf.get();
        var chars: std.ArrayList(u32) = .empty;
        defer chars.deinit(allocator);

        chars.ensureUnusedCapacity(allocator, max_chars_expected) catch unreachable; // stack allocation already succeeded

        while (try char_literal_parser.next()) |item| switch (item) {
            .value => |v| try chars.append(allocator, v),
            .codepoint => |c| try chars.append(allocator, c),
            .improperly_encoded => |s| {
                try chars.ensureUnusedCapacity(allocator, s.len);
                for (s) |c| chars.appendAssumeCapacity(c);
            },
            .utf8_text => |view| {
                var it = view.iterator();
                var max_codepoint_seen: u21 = 0;
                try chars.ensureUnusedCapacity(allocator, view.bytes.len);
                while (it.nextCodepoint()) |c| {
                    max_codepoint_seen = @max(max_codepoint_seen, c);
                    chars.appendAssumeCapacity(c);
                }
                if (max_codepoint_seen > max_codepoint) {
                    try char_literal_parser.err(.char_too_large, .{});
                }
            },
        };

        is_multichar = chars.items.len > 1;
        if (is_multichar) {
            if (char_kind == .char and chars.items.len == 4) {
                try char_literal_parser.warn(.four_char_char_literal, .{});
            } else if (char_kind == .char) {
                try char_literal_parser.warn(.multichar_literal_warning, .{});
            } else {
                const kind: []const u8 = switch (char_kind) {
                    .wide => "wide",
                    .utf_8, .utf_16, .utf_32 => "Unicode",
                    else => unreachable,
                };
                try char_literal_parser.err(.invalid_multichar_literal, .{kind});
            }
        }

        var multichar_overflow = false;
        if (char_kind == .char and is_multichar) {
            for (chars.items) |item| {
                val, const overflowed = @shlWithOverflow(val, 8);
                multichar_overflow = multichar_overflow or overflowed != 0;
                val += @as(u8, @truncate(item));
            }
        } else if (chars.items.len > 0) {
            val = chars.items[chars.items.len - 1];
        }

        if (multichar_overflow) {
            try char_literal_parser.err(.char_lit_too_wide, .{});
        }
    }

    const char_literal_qt = char_kind.charLiteralType(p.comp);
    // This is the type the literal will have if we're in a macro; macros always operate on intmax_t/uintmax_t values
    const macro_qt = if (char_literal_qt.signedness(p.comp) == .unsigned or
        (char_kind == .char and p.comp.getCharSignedness() == .unsigned))
        try p.comp.type_store.intmax.makeIntUnsigned(p.comp)
    else
        p.comp.type_store.intmax;

    var value = try Value.int(val, p.comp);
    // C99 6.4.4.4.10
    // > If an integer character constant contains a single character or escape sequence,
    // > its value is the one that results when an object with type char whose value is
    // > that of the single character or escape sequence is converted to type int.
    // This conversion only matters if `char` is signed and has a high-order bit of `1`
    if (char_kind == .char and !is_multichar and val > 0x7F and p.comp.getCharSignedness() == .signed) {
        _ = try value.intCast(.char, p.comp);
    }

    const res: Result = .{
        .qt = if (p.in_macro) macro_qt else char_literal_qt,
        .val = value,
        .node = try p.addNode(.{ .char_literal = .{
            .qt = char_literal_qt,
            .literal_tok = p.tok_i,
            .kind = switch (char_kind) {
                .char, .unterminated => .ascii,
                .wide => .wide,
                .utf_8 => .utf8,
                .utf_16 => .utf16,
                .utf_32 => .utf32,
            },
        } }),
    };
    if (!p.in_macro) try p.tree.value_map.put(gpa, res.node, res.val);
    return res;
}

fn parseFloat(p: *Parser, buf: []const u8, suffix: NumberSuffix, tok_i: TokenIndex) !Result {
    const qt: QualType = switch (suffix) {
        .None, .I => .double,
        .F, .IF => .float,
        .F16, .IF16 => .float16,
        .BF16 => .bf16,
        .L, .IL => .long_double,
        .W, .IW => p.comp.float80Type().?,
        .Q, .IQ, .F128, .IF128 => .float128,
        .F32, .IF32 => .float32,
        .F64, .IF64 => .float64,
        .F32x, .IF32x => .float32x,
        .F64x, .IF64x => .float64x,
        .D32 => .dfloat32,
        .D64 => .dfloat64,
        .D128 => .dfloat128,
        .D64x => .dfloat64x,
        else => unreachable,
    };
    const val = try Value.intern(p.comp, key: {
        try p.strings.ensureUnusedCapacity(p.comp.gpa, buf.len);

        const strings_top = p.strings.items.len;
        defer p.strings.items.len = strings_top;
        for (buf) |c| {
            if (c != '\'') p.strings.appendAssumeCapacity(c);
        }

        const float = std.fmt.parseFloat(f128, p.strings.items[strings_top..]) catch unreachable;
        break :key switch (qt.bitSizeof(p.comp)) {
            16 => .{ .float = .{ .f16 = @floatCast(float) } },
            32 => .{ .float = .{ .f32 = @floatCast(float) } },
            64 => .{ .float = .{ .f64 = @floatCast(float) } },
            80 => .{ .float = .{ .f80 = @floatCast(float) } },
            128 => .{ .float = .{ .f128 = @floatCast(float) } },
            else => unreachable,
        };
    });
    var res = Result{
        .qt = qt,
        .node = try p.addNode(.{ .float_literal = .{ .qt = qt, .literal_tok = tok_i } }),
        .val = val,
    };
    if (suffix.isImaginary()) {
        try p.err(p.tok_i, .gnu_imaginary_constant, .{});
        res.qt = try qt.toComplex(p.comp);

        res.val = try Value.intern(p.comp, switch (res.qt.bitSizeof(p.comp)) {
            32 => .{ .complex = .{ .cf16 = .{ 0.0, val.toFloat(f16, p.comp) } } },
            64 => .{ .complex = .{ .cf32 = .{ 0.0, val.toFloat(f32, p.comp) } } },
            128 => .{ .complex = .{ .cf64 = .{ 0.0, val.toFloat(f64, p.comp) } } },
            160 => .{ .complex = .{ .cf80 = .{ 0.0, val.toFloat(f80, p.comp) } } },
            256 => .{ .complex = .{ .cf128 = .{ 0.0, val.toFloat(f128, p.comp) } } },
            else => unreachable,
        });
        try res.un(p, .imaginary_literal, tok_i);
    }
    return res;
}

fn getIntegerPart(p: *Parser, buf: []const u8, prefix: NumberPrefix, tok_i: TokenIndex) ![]const u8 {
    if (buf[0] == '.') return "";

    if (!prefix.digitAllowed(buf[0])) {
        switch (prefix) {
            .binary => try p.err(tok_i, .invalid_binary_digit, .{text_literal.Ascii.init(buf[0])}),
            .octal => try p.err(tok_i, .invalid_octal_digit, .{text_literal.Ascii.init(buf[0])}),
            .hex => try p.err(tok_i, .invalid_int_suffix, .{buf}),
            .decimal => unreachable,
        }
        return error.ParsingFailed;
    }

    for (buf, 0..) |c, idx| {
        if (idx == 0) continue;
        switch (c) {
            '.' => return buf[0..idx],
            'p', 'P' => return if (prefix == .hex) buf[0..idx] else {
                try p.err(tok_i, .invalid_int_suffix, .{buf[idx..]});
                return error.ParsingFailed;
            },
            'e', 'E' => {
                switch (prefix) {
                    .hex => continue,
                    .decimal => return buf[0..idx],
                    .binary => try p.err(tok_i, .invalid_binary_digit, .{text_literal.Ascii.init(c)}),
                    .octal => try p.err(tok_i, .invalid_octal_digit, .{text_literal.Ascii.init(c)}),
                }
                return error.ParsingFailed;
            },
            '0'...'9', 'a'...'d', 'A'...'D', 'f', 'F' => {
                if (!prefix.digitAllowed(c)) {
                    switch (prefix) {
                        .binary => try p.err(tok_i, .invalid_binary_digit, .{text_literal.Ascii.init(c)}),
                        .octal => try p.err(tok_i, .invalid_octal_digit, .{text_literal.Ascii.init(c)}),
                        .decimal, .hex => try p.err(tok_i, .invalid_int_suffix, .{buf[idx..]}),
                    }
                    return error.ParsingFailed;
                }
            },
            '\'' => {},
            else => return buf[0..idx],
        }
    }
    return buf;
}

fn fixedSizeInt(p: *Parser, base: u8, buf: []const u8, suffix: NumberSuffix, tok_i: TokenIndex) !Result {
    var val: u64 = 0;
    var overflow = false;
    for (buf) |c| {
        const digit: u64 = switch (c) {
            '0'...'9' => c - '0',
            'A'...'Z' => c - 'A' + 10,
            'a'...'z' => c - 'a' + 10,
            '\'' => continue,
            else => unreachable,
        };

        if (val != 0) {
            const product, const overflowed = @mulWithOverflow(val, base);
            if (overflowed != 0) {
                overflow = true;
            }
            val = product;
        }
        const sum, const overflowed = @addWithOverflow(val, digit);
        if (overflowed != 0) overflow = true;
        val = sum;
    }
    var res: Result = .{
        .val = try Value.int(val, p.comp),
        .node = undefined, // set later
    };
    if (overflow) {
        try p.err(tok_i, .int_literal_too_big, .{});
        res.qt = .ulong_long;
        res.node = try p.addNode(.{ .int_literal = .{ .qt = res.qt, .literal_tok = tok_i } });
        try res.putValue(p);
        return res;
    }
    const interned_val = try Value.int(val, p.comp);
    if (suffix.isSignedInteger() and base == 10) {
        const max_int = try Value.maxInt(p.comp.type_store.intmax, p.comp);
        if (interned_val.compare(.gt, max_int, p.comp)) {
            try p.err(tok_i, .implicitly_unsigned_literal, .{});
        }
    }

    const qts: []const QualType = if (suffix.signedness() == .unsigned)
        &.{ .uint, .ulong, .ulong_long }
    else if (base == 10)
        &.{ .int, .long, .long_long }
    else
        &.{ .int, .uint, .long, .ulong, .long_long, .ulong_long };

    const suffix_qt: QualType = switch (suffix) {
        .None, .I => .int,
        .U, .IU => .uint,
        .UL, .IUL => .ulong,
        .ULL, .IULL => .ulong_long,
        .L, .IL => .long,
        .LL, .ILL => .long_long,
        else => unreachable,
    };

    for (qts) |qt| {
        res.qt = qt;
        if (res.qt.intRankOrder(suffix_qt, p.comp).compare(.lt)) continue;
        const max_int = try Value.maxInt(res.qt, p.comp);
        if (interned_val.compare(.lte, max_int, p.comp)) break;
    } else {
        if (p.comp.langopts.emulate == .gcc) {
            if (p.comp.target.hasInt128()) {
                res.qt = .int128;
            } else {
                res.qt = .long_long;
            }
        } else {
            res.qt = .ulong_long;
        }
    }

    res.node = try p.addNode(.{ .int_literal = .{ .qt = res.qt, .literal_tok = tok_i } });
    try res.putValue(p);
    return res;
}

fn parseInt(p: *Parser, prefix: NumberPrefix, buf: []const u8, suffix: NumberSuffix, tok_i: TokenIndex) !Result {
    if (prefix == .binary) {
        try p.err(tok_i, .binary_integer_literal, .{});
    }
    const base = @intFromEnum(prefix);
    var res = if (suffix.isBitInt())
        try p.bitInt(base, buf, suffix, tok_i)
    else
        try p.fixedSizeInt(base, buf, suffix, tok_i);

    if (suffix.isImaginary()) {
        try p.err(tok_i, .gnu_imaginary_constant, .{});
        res.qt = try res.qt.toComplex(p.comp);
        res.val = .{};
        try res.un(p, .imaginary_literal, tok_i);
    }
    return res;
}

fn bitInt(p: *Parser, base: u8, buf: []const u8, suffix: NumberSuffix, tok_i: TokenIndex) Error!Result {
    const gpa = p.comp.gpa;
    try p.err(tok_i, .pre_c23_compat, .{"'_BitInt' suffix for literals"});
    try p.err(tok_i, .bitint_suffix, .{});

    var managed = try big.int.Managed.init(gpa);
    defer managed.deinit();

    {
        try p.strings.ensureUnusedCapacity(gpa, buf.len);

        const strings_top = p.strings.items.len;
        defer p.strings.items.len = strings_top;
        for (buf) |c| {
            if (c != '\'') p.strings.appendAssumeCapacity(c);
        }

        managed.setString(base, p.strings.items[strings_top..]) catch |e| switch (e) {
            error.InvalidBase => unreachable, // `base` is one of 2, 8, 10, 16
            error.InvalidCharacter => unreachable, // digits validated by Tokenizer
            else => |er| return er,
        };
    }
    const c = managed.toConst();
    const bits_needed: std.math.IntFittingRange(0, Compilation.bit_int_max_bits) = blk: {
        // Literal `0` requires at least 1 bit
        const count = @max(1, c.bitCountTwosComp());
        // The wb suffix results in a _BitInt that includes space for the sign bit even if the
        // value of the constant is positive or was specified in hexadecimal or octal notation.
        const sign_bits = @intFromBool(suffix.isSignedInteger());
        const bits_needed = count + sign_bits;
        break :blk @intCast(bits_needed);
    };

    const int_qt = try p.comp.type_store.put(gpa, .{ .bit_int = .{
        .bits = bits_needed,
        .signedness = suffix.signedness(),
    } });
    const res: Result = .{
        .val = try Value.intern(p.comp, .{ .int = .{ .big_int = c } }),
        .qt = int_qt,
        .node = try p.addNode(.{ .int_literal = .{ .qt = int_qt, .literal_tok = tok_i } }),
    };
    try res.putValue(p);
    return res;
}

fn getFracPart(p: *Parser, buf: []const u8, prefix: NumberPrefix, tok_i: TokenIndex) ![]const u8 {
    if (buf.len == 0 or buf[0] != '.') return "";
    assert(prefix != .octal);
    if (prefix == .binary) {
        try p.err(tok_i, .invalid_int_suffix, .{buf});
        return error.ParsingFailed;
    }
    for (buf, 0..) |c, idx| {
        if (idx == 0) continue;
        if (c == '\'') continue;
        if (!prefix.digitAllowed(c)) return buf[0..idx];
    }
    return buf;
}

fn getExponent(p: *Parser, buf: []const u8, prefix: NumberPrefix, tok_i: TokenIndex) ![]const u8 {
    if (buf.len == 0) return "";

    switch (buf[0]) {
        'e', 'E' => assert(prefix == .decimal),
        'p', 'P' => if (prefix != .hex) {
            try p.err(tok_i, .invalid_float_suffix, .{buf});
            return error.ParsingFailed;
        },
        else => return "",
    }
    const end = for (buf, 0..) |c, idx| {
        if (idx == 0) continue;
        if (idx == 1 and (c == '+' or c == '-')) continue;
        switch (c) {
            '0'...'9' => {},
            '\'' => continue,
            else => break idx,
        }
    } else buf.len;
    const exponent = buf[0..end];
    if (std.mem.indexOfAny(u8, exponent, "0123456789") == null) {
        try p.err(tok_i, .exponent_has_no_digits, .{});
        return error.ParsingFailed;
    }
    return exponent;
}

/// Using an explicit `tok_i` parameter instead of `p.tok_i` makes it easier
/// to parse numbers in pragma handlers.
pub fn parseNumberToken(p: *Parser, tok_i: TokenIndex) !Result {
    const buf = p.tokSlice(tok_i);
    const prefix = NumberPrefix.fromString(buf);
    const after_prefix = buf[prefix.stringLen()..];

    const int_part = try p.getIntegerPart(after_prefix, prefix, tok_i);

    const after_int = after_prefix[int_part.len..];

    const frac = try p.getFracPart(after_int, prefix, tok_i);
    const after_frac = after_int[frac.len..];

    const exponent = try p.getExponent(after_frac, prefix, tok_i);
    const suffix_str = after_frac[exponent.len..];
    const is_float = (exponent.len > 0 or frac.len > 0);
    const suffix = NumberSuffix.fromString(suffix_str, if (is_float) .float else .int) orelse {
        if (is_float) {
            try p.err(tok_i, .invalid_float_suffix, .{suffix_str});
        } else {
            try p.err(tok_i, .invalid_int_suffix, .{suffix_str});
        }
        return error.ParsingFailed;
    };
    if (suffix.isFloat80() and p.comp.float80Type() == null) {
        try p.err(tok_i, .invalid_float_suffix, .{suffix_str});
        return error.ParsingFailed;
    }

    if (is_float) {
        assert(prefix == .hex or prefix == .decimal);
        if (prefix == .hex and exponent.len == 0) {
            try p.err(tok_i, .hex_floating_constant_requires_exponent, .{});
            return error.ParsingFailed;
        }
        const number = buf[0 .. buf.len - suffix_str.len];
        return p.parseFloat(number, suffix, tok_i);
    } else {
        return p.parseInt(prefix, int_part, suffix, tok_i);
    }
}

fn ppNum(p: *Parser) Error!Result {
    defer p.tok_i += 1;
    var res = try p.parseNumberToken(p.tok_i);
    if (p.in_macro) {
        const res_sk = res.qt.scalarKind(p.comp);
        if (res_sk.isFloat() or !res_sk.isReal()) {
            try p.err(p.tok_i, .float_literal_in_pp_expr, .{});
            return error.ParsingFailed;
        }
        res.qt = if (res.qt.signedness(p.comp) == .unsigned)
            try p.comp.type_store.intmax.makeIntUnsigned(p.comp)
        else
            p.comp.type_store.intmax;
    } else if (res.val.opt_ref != .none) {
        try res.putValue(p);
    }
    return res;
}

/// Run a parser function but do not evaluate the result
fn parseNoEval(p: *Parser, comptime func: fn (*Parser) Error!?Result) Error!Result {
    const no_eval = p.no_eval;
    defer p.no_eval = no_eval;
    p.no_eval = true;

    const parsed = try func(p);
    return p.expectResult(parsed);
}

/// genericSelection : keyword_generic '(' assignExpr ',' genericAssoc (',' genericAssoc)* ')'
/// genericAssoc
///  : typeName ':' assignExpr
///  | keyword_default ':' assignExpr
fn genericSelection(p: *Parser) Error!?Result {
    const gpa = p.comp.gpa;
    const kw_generic = p.tok_i;
    p.tok_i += 1;
    const l_paren = try p.expectToken(.l_paren);
    const controlling_tok = p.tok_i;

    const controlling = try p.parseNoEval(assignExpr);
    var controlling_qt = controlling.qt;
    if (controlling_qt.is(p.comp, .array)) {
        controlling_qt = try controlling_qt.decay(p.comp);
    }
    _ = try p.expectToken(.comma);

    const list_buf_top = p.list_buf.items.len;
    defer p.list_buf.items.len = list_buf_top;

    // Use param_buf to store the token indexes of previous cases
    const param_buf_top = p.param_buf.items.len;
    defer p.param_buf.items.len = param_buf_top;

    var default_tok: ?TokenIndex = null;
    var default: Result = undefined;
    var chosen_tok: ?TokenIndex = null;
    var chosen: Result = undefined;

    while (true) {
        const start = p.tok_i;
        if (try p.typeName()) |qt| blk: {
            switch (qt.base(p.comp).type) {
                .array => try p.err(start, .generic_array_type, .{}),
                .func => try p.err(start, .generic_func_type, .{}),
                else => if (qt.isQualified()) {
                    try p.err(start, .generic_qual_type, .{});
                },
            }

            const colon = try p.expectToken(.colon);
            var res = try p.expect(assignExpr);
            res.node = try p.addNode(.{
                .generic_association_expr = .{
                    .colon_tok = colon,
                    .association_qt = qt,
                    .expr = res.node,
                },
            });
            try p.list_buf.append(gpa, res.node);
            try p.param_buf.append(gpa, .{ .name = undefined, .qt = qt, .name_tok = start, .node = .null });

            if (qt.eql(controlling_qt, p.comp)) {
                if (chosen_tok == null) {
                    chosen = res;
                    chosen_tok = start;
                    break :blk;
                }
            }

            const previous_items = p.param_buf.items[0 .. p.param_buf.items.len - 1][param_buf_top..];
            for (previous_items) |prev_item| {
                if (prev_item.qt.eql(qt, p.comp)) {
                    try p.err(start, .generic_duplicate, .{qt});
                    try p.err(prev_item.name_tok, .generic_duplicate_here, .{qt});
                }
            }
        } else if (p.eatToken(.keyword_default)) |tok| {
            _ = try p.expectToken(.colon);
            var res = try p.expect(assignExpr);
            res.node = try p.addNode(.{
                .generic_default_expr = .{
                    .default_tok = tok,
                    .expr = res.node,
                },
            });

            if (default_tok) |prev| {
                try p.err(tok, .generic_duplicate_default, .{});
                try p.err(prev, .previous_case, .{});
            }
            default = res;
            default_tok = tok;
        } else {
            if (p.list_buf.items.len == list_buf_top) {
                try p.err(p.tok_i, .expected_type, .{});
                return error.ParsingFailed;
            }
            break;
        }
        if (p.eatToken(.comma) == null) break;
    }
    try p.expectClosing(l_paren, .r_paren);

    if (chosen_tok == null) {
        if (default_tok != null) {
            chosen = default;
        } else {
            try p.err(controlling_tok, .generic_no_match, .{controlling_qt});
            return error.ParsingFailed;
        }
    } else if (default_tok != null) {
        try p.list_buf.append(gpa, default.node);
    }

    for (p.list_buf.items[list_buf_top..], list_buf_top..) |item, i| {
        if (item == chosen.node) {
            _ = p.list_buf.orderedRemove(i);
            break;
        }
    }

    return .{
        .qt = chosen.qt,
        .val = chosen.val,
        .node = try p.addNode(.{
            .generic_expr = .{
                .generic_tok = kw_generic,
                .controlling = controlling.node,
                .chosen = chosen.node,
                .qt = chosen.qt,
                .rest = p.list_buf.items[list_buf_top..],
            },
        }),
    };
}

test "Node locations" {
    var arena_state: std.heap.ArenaAllocator = .init(std.testing.allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();

    var diagnostics: Diagnostics = .{ .output = .ignore };
    var comp = Compilation.init(std.testing.allocator, arena, std.testing.io, &diagnostics, Io.Dir.cwd());
    defer comp.deinit();

    const file = try comp.addSourceFromBuffer("file.c",
        \\int foo = 5;
        \\int bar = 10;
        \\int main(void) {}
        \\
    );

    const builtin_macros = try comp.generateBuiltinMacros(.no_system_defines);

    var pp = Preprocessor.init(&comp, .default);
    defer pp.deinit();
    try pp.addBuiltinMacros();

    _ = try pp.preprocess(builtin_macros);

    const eof = try pp.preprocess(file);
    try pp.addToken(eof);

    var tree = try Parser.parse(&pp);
    defer tree.deinit();

    try std.testing.expectEqual(0, comp.diagnostics.total);
    for (tree.root_decls.items[tree.root_decls.items.len - 3 ..], 0..) |node, i| {
        const slice = tree.tokSlice(node.tok(&tree));
        const expected_slice = switch (i) {
            0 => "foo",
            1 => "bar",
            2 => "main",
            else => unreachable,
        };
        try std.testing.expectEqualStrings(expected_slice, slice);

        const loc = node.loc(&tree).expand(&comp);
        const expected_col: u32 = switch (i) {
            0 => 5,
            1 => 5,
            2 => 5,
            else => unreachable,
        };
        try std.testing.expectEqual(expected_col, loc.col);

        const expected_line_no = i + 1;
        try std.testing.expectEqual(expected_line_no, loc.line_no);

        const expected_source_path = "file.c";
        try std.testing.expectEqualStrings(expected_source_path, loc.path);

        const expected_source_kind = Source.Kind.user;
        try std.testing.expectEqual(expected_source_kind, loc.kind);
    }
}
