const std = @import("std");

const Interner = @import("../backend.zig").Interner;

const Attribute = @import("Attribute.zig");
const CodeGen = @import("CodeGen.zig");
const Compilation = @import("Compilation.zig");
const number_affixes = @import("Tree/number_affixes.zig");
const Source = @import("Source.zig");
const Tokenizer = @import("Tokenizer.zig");
const QualType = @import("TypeStore.zig").QualType;
const Value = @import("Value.zig");

pub const Token = struct {
    id: Id,
    loc: Source.Location,

    pub const List = std.MultiArrayList(Token);
    pub const Id = Tokenizer.Token.Id;
    pub const NumberPrefix = number_affixes.Prefix;
    pub const NumberSuffix = number_affixes.Suffix;
};

pub const TokenWithExpansionLocs = struct {
    id: Token.Id,
    flags: packed struct {
        expansion_disabled: bool = false,
        is_macro_arg: bool = false,
    } = .{},
    /// This location contains the actual token slice which might be generated.
    /// If it is generated then there is guaranteed to be at least one
    /// expansion location.
    loc: Source.Location,
    expansion_locs: ?[*]Source.Location = null,

    pub fn expansionSlice(tok: TokenWithExpansionLocs) []const Source.Location {
        const locs = tok.expansion_locs orelse return &[0]Source.Location{};
        var i: usize = 0;
        while (locs[i].id.index != .unused) : (i += 1) {}
        return locs[0..i];
    }

    pub fn addExpansionLocation(tok: *TokenWithExpansionLocs, gpa: std.mem.Allocator, new: []const Source.Location) !void {
        if (new.len == 0 or tok.id == .whitespace or tok.id == .macro_ws or tok.id == .placemarker) return;
        var list: std.ArrayList(Source.Location) = .empty;
        defer {
            @memset(list.items.ptr[list.items.len..list.capacity], .{});
            // Add a sentinel to indicate the end of the list since
            // the ArrayList's capacity isn't guaranteed to be exactly
            // what we ask for.
            if (list.capacity > 0) {
                list.items.ptr[list.capacity - 1].byte_offset = 1;
            }
            tok.expansion_locs = list.items.ptr;
        }

        if (tok.expansion_locs) |locs| {
            var i: usize = 0;
            while (locs[i].id.index != .unused) : (i += 1) {}
            list.items = locs[0..i];
            while (locs[i].byte_offset != 1) : (i += 1) {}
            list.capacity = i + 1;
        }

        const min_len = @max(list.items.len + new.len + 1, 4);
        const wanted_len = std.math.ceilPowerOfTwo(usize, min_len) catch
            return error.OutOfMemory;
        try list.ensureTotalCapacity(gpa, wanted_len);

        for (new) |new_loc| {
            if (new_loc.id.index == .generated) continue;
            list.appendAssumeCapacity(new_loc);
        }
    }

    pub fn free(expansion_locs: ?[*]Source.Location, gpa: std.mem.Allocator) void {
        const locs = expansion_locs orelse return;
        var i: usize = 0;
        while (locs[i].id.index != .unused) : (i += 1) {}
        while (locs[i].byte_offset != 1) : (i += 1) {}
        gpa.free(locs[0 .. i + 1]);
    }

    pub fn dupe(tok: TokenWithExpansionLocs, gpa: std.mem.Allocator) !TokenWithExpansionLocs {
        var copy = tok;
        copy.expansion_locs = null;
        try copy.addExpansionLocation(gpa, tok.expansionSlice());
        return copy;
    }

    pub fn checkMsEof(tok: TokenWithExpansionLocs, source: Source, comp: *Compilation) !void {
        std.debug.assert(tok.id == .eof);
        if (source.buf.len > tok.loc.byte_offset and source.buf[tok.loc.byte_offset] == 0x1A) {
            const diagnostic: Compilation.Diagnostic = .ctrl_z_eof;
            try comp.diagnostics.add(.{
                .text = diagnostic.fmt,
                .kind = diagnostic.kind,
                .opt = diagnostic.opt,
                .extension = diagnostic.extension,
                .location = source.lineCol(.{
                    .id = source.id,
                    .byte_offset = tok.loc.byte_offset,
                    .line = tok.loc.line,
                }),
            });
        }
    }
};

pub const TokenIndex = u32;
pub const ValueMap = std.AutoHashMapUnmanaged(Node.Index, Value);

const Tree = @This();

comp: *Compilation,

// Values from Preprocessor.
tokens: Token.List.Slice,

// Values owned by this Tree
nodes: std.MultiArrayList(Node.Repr) = .empty,
extra: std.ArrayList(u32) = .empty,
root_decls: std.ArrayList(Node.Index) = .empty,
value_map: ValueMap = .empty,

pub const genIr = CodeGen.genIr;

pub fn deinit(tree: *Tree) void {
    tree.nodes.deinit(tree.comp.gpa);
    tree.extra.deinit(tree.comp.gpa);
    tree.root_decls.deinit(tree.comp.gpa);
    tree.value_map.deinit(tree.comp.gpa);
    tree.* = undefined;
}

pub const GNUAssemblyQualifiers = packed struct(u32) {
    @"volatile": bool = false,
    @"inline": bool = false,
    goto: bool = false,
    _: u29 = 0,
};

pub const Node = union(enum) {
    empty_decl: EmptyDecl,
    static_assert: StaticAssert,
    function: Function,
    param: Param,
    variable: Variable,
    typedef: Typedef,
    global_asm: GlobalAsm,

    struct_decl: ContainerDecl,
    union_decl: ContainerDecl,
    enum_decl: ContainerDecl,
    struct_forward_decl: ContainerForwardDecl,
    union_forward_decl: ContainerForwardDecl,
    enum_forward_decl: ContainerForwardDecl,

    enum_field: EnumField,
    record_field: RecordField,

    labeled_stmt: LabeledStmt,
    compound_stmt: CompoundStmt,
    if_stmt: IfStmt,
    switch_stmt: SwitchStmt,
    case_stmt: CaseStmt,
    default_stmt: DefaultStmt,
    while_stmt: WhileStmt,
    do_while_stmt: DoWhileStmt,
    for_stmt: ForStmt,
    goto_stmt: GotoStmt,
    computed_goto_stmt: ComputedGotoStmt,
    continue_stmt: ContinueStmt,
    break_stmt: BreakStmt,
    null_stmt: NullStmt,
    return_stmt: ReturnStmt,
    asm_stmt: AsmStmt,

    assign_expr: Binary,
    mul_assign_expr: Binary,
    div_assign_expr: Binary,
    mod_assign_expr: Binary,
    add_assign_expr: Binary,
    sub_assign_expr: Binary,
    shl_assign_expr: Binary,
    shr_assign_expr: Binary,
    bit_and_assign_expr: Binary,
    bit_xor_assign_expr: Binary,
    bit_or_assign_expr: Binary,
    compound_assign_dummy_expr: Unary,

    comma_expr: Binary,
    bool_or_expr: Binary,
    bool_and_expr: Binary,
    bit_or_expr: Binary,
    bit_xor_expr: Binary,
    bit_and_expr: Binary,
    equal_expr: Binary,
    not_equal_expr: Binary,
    less_than_expr: Binary,
    less_than_equal_expr: Binary,
    greater_than_expr: Binary,
    greater_than_equal_expr: Binary,
    shl_expr: Binary,
    shr_expr: Binary,
    add_expr: Binary,
    sub_expr: Binary,
    mul_expr: Binary,
    div_expr: Binary,
    mod_expr: Binary,

    cast: Cast,

    addr_of_expr: Unary,
    deref_expr: Unary,
    plus_expr: Unary,
    negate_expr: Unary,
    bit_not_expr: Unary,
    bool_not_expr: Unary,
    pre_inc_expr: Unary,
    pre_dec_expr: Unary,
    imag_expr: Unary,
    real_expr: Unary,
    post_inc_expr: Unary,
    post_dec_expr: Unary,
    paren_expr: Unary,
    stmt_expr: Unary,

    addr_of_label: AddrOfLabel,

    array_access_expr: ArrayAccess,
    member_access_expr: MemberAccess,
    member_access_ptr_expr: MemberAccess,

    call_expr: Call,

    decl_ref_expr: DeclRef,
    enumeration_ref: DeclRef,

    builtin_call_expr: BuiltinCall,
    builtin_ref: BuiltinRef,
    builtin_types_compatible_p: TypesCompatible,
    builtin_choose_expr: Conditional,
    builtin_convertvector: Convertvector,
    builtin_shufflevector: Shufflevector,

    /// C23 bool literal `true` / `false`
    bool_literal: Literal,
    /// C23 nullptr literal
    nullptr_literal: Literal,
    /// integer literal, always unsigned
    int_literal: Literal,
    /// Same as int_literal, but originates from a char literal
    char_literal: CharLiteral,
    /// a floating point literal
    float_literal: Literal,
    string_literal_expr: CharLiteral,
    /// wraps a float or double literal
    imaginary_literal: Unary,
    /// A compound literal (type){ init }
    compound_literal_expr: CompoundLiteral,

    sizeof_expr: TypeInfo,
    alignof_expr: TypeInfo,

    generic_expr: Generic,
    generic_association_expr: Generic.Association,
    generic_default_expr: Generic.Default,

    binary_cond_expr: Conditional,
    /// Used as the base for casts of the lhs in `binary_cond_expr`.
    cond_dummy_expr: Unary,
    cond_expr: Conditional,

    array_init_expr: ContainerInit,
    struct_init_expr: ContainerInit,
    union_init_expr: UnionInit,
    /// Inserted in array_init_expr to represent unspecified elements.
    /// data.int contains the amount of elements.
    array_filler_expr: ArrayFiller,
    /// Inserted in record and scalar initializers for unspecified elements.
    default_init_expr: DefaultInit,

    pub const EmptyDecl = struct {
        semicolon: TokenIndex,
    };

    pub const StaticAssert = struct {
        assert_tok: TokenIndex,
        cond: Node.Index,
        message: ?Node.Index,
    };

    pub const Function = struct {
        name_tok: TokenIndex,
        qt: QualType,
        static: bool,
        @"inline": bool,
        body: ?Node.Index,
        /// Actual, non-tentative definition of this function.
        definition: ?Node.Index,
    };

    pub const Param = struct {
        name_tok: TokenIndex,
        qt: QualType,
        storage_class: enum {
            auto,
            register,
        },
    };

    pub const Variable = struct {
        name_tok: TokenIndex,
        qt: QualType,
        storage_class: enum {
            auto,
            static,
            @"extern",
            register,
        },
        thread_local: bool,
        /// From predefined macro  __func__, __FUNCTION__ or __PRETTY_FUNCTION__.
        /// Implies `static == true`.
        implicit: bool,
        initializer: ?Node.Index,
        /// Actual, non-tentative definition of this variable.
        definition: ?Node.Index,
    };

    pub const Typedef = struct {
        name_tok: TokenIndex,
        qt: QualType,
        implicit: bool,
    };

    pub const GlobalAsm = struct {
        asm_tok: TokenIndex,
        asm_str: Node.Index,
    };

    pub const ContainerDecl = struct {
        name_or_kind_tok: TokenIndex,
        container_qt: QualType,
        fields: []const Node.Index,
    };

    pub const ContainerForwardDecl = struct {
        name_or_kind_tok: TokenIndex,
        container_qt: QualType,
        /// The definition for this forward declaration if one exists.
        definition: ?Node.Index,
    };

    pub const EnumField = struct {
        name_tok: TokenIndex,
        qt: QualType,
        init: ?Node.Index,
    };

    pub const RecordField = struct {
        name_or_first_tok: TokenIndex,
        qt: QualType,
        bit_width: ?Node.Index,
    };

    pub const LabeledStmt = struct {
        label_tok: TokenIndex,
        body: Node.Index,
        qt: QualType,
    };

    pub const CompoundStmt = struct {
        l_brace_tok: TokenIndex,
        body: []const Node.Index,
    };

    pub const IfStmt = struct {
        if_tok: TokenIndex,
        cond: Node.Index,
        then_body: Node.Index,
        else_body: ?Node.Index,
    };

    pub const SwitchStmt = struct {
        switch_tok: TokenIndex,
        cond: Node.Index,
        body: Node.Index,
    };

    pub const CaseStmt = struct {
        case_tok: TokenIndex,
        start: Node.Index,
        end: ?Node.Index,
        body: Node.Index,
    };

    pub const DefaultStmt = struct {
        default_tok: TokenIndex,
        body: Node.Index,
    };

    pub const WhileStmt = struct {
        while_tok: TokenIndex,
        cond: Node.Index,
        body: Node.Index,
    };

    pub const DoWhileStmt = struct {
        do_tok: TokenIndex,
        cond: Node.Index,
        body: Node.Index,
    };

    pub const ForStmt = struct {
        for_tok: TokenIndex,
        init: union(enum) {
            decls: []const Node.Index,
            expr: ?Node.Index,
        },
        cond: ?Node.Index,
        incr: ?Node.Index,
        body: Node.Index,
    };

    pub const GotoStmt = struct {
        label_tok: TokenIndex,
    };

    pub const ComputedGotoStmt = struct {
        goto_tok: TokenIndex,
        expr: Node.Index,
    };

    pub const ContinueStmt = struct {
        continue_tok: TokenIndex,
    };

    pub const BreakStmt = struct {
        break_tok: TokenIndex,
    };

    pub const NullStmt = struct {
        semicolon_or_r_brace_tok: TokenIndex,
        qt: QualType,
    };

    pub const ReturnStmt = struct {
        return_tok: TokenIndex,
        return_qt: QualType,
        operand: union(enum) {
            expr: Node.Index,
            /// True if the function is called "main" and return_qt is compatible with int
            implicit: bool,
            none,
        },
    };

    pub const AsmStmt = struct {
        asm_tok: TokenIndex,
        asm_str: Node.Index,
        outputs: []const Operand,
        inputs: []const Operand,
        clobbers: []const Node.Index,
        labels: []const Node.Index,
        quals: GNUAssemblyQualifiers,

        pub const Operand = struct {
            name: TokenIndex,
            constraint: Node.Index,
            expr: Node.Index,
        };
    };

    pub const Binary = struct {
        qt: QualType,
        lhs: Node.Index,
        op_tok: TokenIndex,
        rhs: Node.Index,
    };

    pub const Cast = struct {
        qt: QualType,
        l_paren: TokenIndex,
        kind: Kind,
        operand: Node.Index,
        implicit: bool,

        pub const Kind = enum {
            /// Does nothing except possibly add qualifiers
            no_op,
            /// Interpret one bit pattern as another. Used for operands which have the same
            /// size and unrelated types, e.g. casting one pointer type to another
            bitcast,
            /// Convert T[] to T *
            array_to_pointer,
            /// Converts an lvalue to an rvalue
            lval_to_rval,
            /// Convert a function type to a pointer to a function
            function_to_pointer,
            /// Convert a pointer type to a _Bool
            pointer_to_bool,
            /// Convert a pointer type to an integer type
            pointer_to_int,
            /// Convert _Bool to an integer type
            bool_to_int,
            /// Convert _Bool to a floating type
            bool_to_float,
            /// Convert a _Bool to a pointer; will cause a  warning
            bool_to_pointer,
            /// Convert an integer type to _Bool
            int_to_bool,
            /// Convert an integer to a floating type
            int_to_float,
            /// Convert a complex integer to a complex floating type
            complex_int_to_complex_float,
            /// Convert an integer type to a pointer type
            int_to_pointer,
            /// Convert a floating type to a _Bool
            float_to_bool,
            /// Convert a floating type to an integer
            float_to_int,
            /// Convert a complex floating type to a complex integer
            complex_float_to_complex_int,
            /// Convert one integer type to another
            int_cast,
            /// Convert one complex integer type to another
            complex_int_cast,
            /// Convert real part of complex integer to a integer
            complex_int_to_real,
            /// Create a complex integer type using operand as the real part
            real_to_complex_int,
            /// Convert one floating type to another
            float_cast,
            /// Convert one complex floating type to another
            complex_float_cast,
            /// Convert real part of complex float to a float
            complex_float_to_real,
            /// Create a complex floating type using operand as the real part
            real_to_complex_float,
            /// Convert type to void
            to_void,
            /// Convert a literal 0 to a null pointer
            null_to_pointer,
            /// GNU cast-to-union extension
            union_cast,
            /// Create vector where each value is same as the input scalar.
            vector_splat,
            /// Convert an atomic type to its non atomic base type.
            atomic_to_non_atomic,
            /// Convert a non atomic type to an atomic type.
            non_atomic_to_atomic,
        };
    };

    pub const Unary = struct {
        qt: QualType,
        op_tok: TokenIndex,
        operand: Node.Index,
    };

    pub const AddrOfLabel = struct {
        label_tok: TokenIndex,
        qt: QualType,
    };

    pub const ArrayAccess = struct {
        l_bracket_tok: TokenIndex,
        qt: QualType,
        base: Node.Index,
        index: Node.Index,
    };

    pub const MemberAccess = struct {
        qt: QualType,
        base: Node.Index,
        access_tok: TokenIndex,
        member_index: u32,

        pub fn isBitFieldWidth(access: MemberAccess, tree: *const Tree) ?u32 {
            var qt = access.base.qt(tree);
            if (qt.isInvalid()) return null;
            if (qt.get(tree.comp, .pointer)) |pointer| qt = pointer.child;
            const record_ty = switch (qt.base(tree.comp).type) {
                .@"struct", .@"union" => |record| record,
                else => return null,
            };
            return record_ty.fields[access.member_index].bit_width.unpack();
        }
    };

    pub const Call = struct {
        l_paren_tok: TokenIndex,
        qt: QualType,
        callee: Node.Index,
        args: []const Node.Index,
    };

    pub const DeclRef = struct {
        name_tok: TokenIndex,
        qt: QualType,
        decl: Node.Index,
    };

    pub const BuiltinCall = struct {
        builtin_tok: TokenIndex,
        qt: QualType,
        args: []const Node.Index,
    };

    pub const BuiltinRef = struct {
        name_tok: TokenIndex,
        qt: QualType,
    };

    pub const TypesCompatible = struct {
        builtin_tok: TokenIndex,
        lhs: QualType,
        rhs: QualType,
    };

    pub const Convertvector = struct {
        builtin_tok: TokenIndex,
        dest_qt: QualType,
        operand: Node.Index,
    };

    pub const Shufflevector = struct {
        builtin_tok: TokenIndex,
        qt: QualType,
        lhs: Node.Index,
        rhs: Node.Index,
        indexes: []const Node.Index,
    };

    pub const Literal = struct {
        literal_tok: TokenIndex,
        qt: QualType,
    };

    pub const CharLiteral = struct {
        literal_tok: TokenIndex,
        qt: QualType,
        kind: enum {
            ascii,
            wide,
            utf8,
            utf16,
            utf32,
        },
    };

    pub const CompoundLiteral = struct {
        l_paren_tok: TokenIndex,
        qt: QualType,
        thread_local: bool,
        storage_class: enum {
            auto,
            static,
            register,
        },
        initializer: Node.Index,
    };

    pub const TypeInfo = struct {
        qt: QualType,
        op_tok: TokenIndex,
        expr: ?Node.Index,
        operand_qt: QualType,
    };

    pub const Generic = struct {
        generic_tok: TokenIndex,
        qt: QualType,

        // `Generic` child nodes are either an `Association` a `Default`
        controlling: Node.Index,
        chosen: Node.Index,
        rest: []const Node.Index,

        pub const Association = struct {
            colon_tok: TokenIndex,
            association_qt: QualType,
            expr: Node.Index,
        };

        pub const Default = struct {
            default_tok: TokenIndex,
            expr: Node.Index,
        };
    };

    pub const Conditional = struct {
        cond_tok: TokenIndex,
        qt: QualType,
        cond: Node.Index,
        then_expr: Node.Index,
        else_expr: Node.Index,
    };

    pub const ContainerInit = struct {
        l_brace_tok: TokenIndex,
        container_qt: QualType,
        items: []const Node.Index,
    };

    pub const UnionInit = struct {
        l_brace_tok: TokenIndex,
        union_qt: QualType,
        field_index: u32,
        initializer: ?Node.Index,
    };

    pub const ArrayFiller = struct {
        last_tok: TokenIndex,
        qt: QualType,
        count: u64,
    };

    pub const DefaultInit = struct {
        last_tok: TokenIndex,
        qt: QualType,
    };

    pub const Index = enum(u32) {
        _,

        pub fn get(index: Index, tree: *const Tree) Node {
            const node_tok = tree.nodes.items(.tok)[@intFromEnum(index)];
            const node_data = &tree.nodes.items(.data)[@intFromEnum(index)];
            return switch (tree.nodes.items(.tag)[@intFromEnum(index)]) {
                .empty_decl => .{
                    .empty_decl = .{
                        .semicolon = node_tok,
                    },
                },
                .static_assert => .{
                    .static_assert = .{
                        .assert_tok = node_tok,
                        .cond = @enumFromInt(node_data[0]),
                        .message = unpackOptIndex(node_data[1]),
                    },
                },
                .fn_proto => {
                    const attr: Node.Repr.DeclAttr = @bitCast(node_data[1]);
                    return .{
                        .function = .{
                            .name_tok = node_tok,
                            .qt = @bitCast(node_data[0]),
                            .static = attr.static,
                            .@"inline" = attr.@"inline",
                            .body = null,
                            .definition = unpackOptIndex(node_data[2]),
                        },
                    };
                },
                .fn_def => {
                    const attr: Node.Repr.DeclAttr = @bitCast(node_data[1]);
                    return .{
                        .function = .{
                            .name_tok = node_tok,
                            .qt = @bitCast(node_data[0]),
                            .static = attr.static,
                            .@"inline" = attr.@"inline",
                            .body = @enumFromInt(node_data[2]),
                            .definition = null,
                        },
                    };
                },
                .param => {
                    const attr: Node.Repr.DeclAttr = @bitCast(node_data[1]);
                    return .{
                        .param = .{
                            .name_tok = node_tok,
                            .qt = @bitCast(node_data[0]),
                            .storage_class = if (attr.register)
                                .register
                            else
                                .auto,
                        },
                    };
                },
                .variable => {
                    const attr: Node.Repr.DeclAttr = @bitCast(node_data[1]);
                    return .{
                        .variable = .{
                            .name_tok = node_tok,
                            .qt = @bitCast(node_data[0]),
                            .storage_class = if (attr.static)
                                .static
                            else if (attr.@"extern")
                                .@"extern"
                            else if (attr.register)
                                .register
                            else
                                .auto,
                            .thread_local = attr.thread_local,
                            .implicit = attr.implicit,
                            .initializer = null,
                            .definition = unpackOptIndex(node_data[2]),
                        },
                    };
                },
                .variable_def => {
                    const attr: Node.Repr.DeclAttr = @bitCast(node_data[1]);
                    return .{
                        .variable = .{
                            .name_tok = node_tok,
                            .qt = @bitCast(node_data[0]),
                            .storage_class = if (attr.static)
                                .static
                            else if (attr.@"extern")
                                .@"extern"
                            else if (attr.register)
                                .register
                            else
                                .auto,
                            .thread_local = attr.thread_local,
                            .implicit = attr.implicit,
                            .initializer = unpackOptIndex(node_data[2]),
                            .definition = null,
                        },
                    };
                },
                .typedef => .{
                    .typedef = .{
                        .name_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .implicit = node_data[1] != 0,
                    },
                },
                .global_asm => .{
                    .global_asm = .{
                        .asm_tok = node_tok,
                        .asm_str = @enumFromInt(node_data[0]),
                    },
                },
                .struct_decl => .{
                    .struct_decl = .{
                        .name_or_kind_tok = node_tok,
                        .container_qt = @bitCast(node_data[0]),
                        .fields = @ptrCast(tree.extra.items[node_data[1]..][0..node_data[2]]),
                    },
                },
                .struct_decl_two => .{
                    .struct_decl = .{
                        .name_or_kind_tok = node_tok,
                        .container_qt = @bitCast(node_data[0]),
                        .fields = unPackElems(node_data[1..]),
                    },
                },
                .union_decl => .{
                    .union_decl = .{
                        .name_or_kind_tok = node_tok,
                        .container_qt = @bitCast(node_data[0]),
                        .fields = @ptrCast(tree.extra.items[node_data[1]..][0..node_data[2]]),
                    },
                },
                .union_decl_two => .{
                    .union_decl = .{
                        .name_or_kind_tok = node_tok,
                        .container_qt = @bitCast(node_data[0]),
                        .fields = unPackElems(node_data[1..]),
                    },
                },
                .enum_decl => .{
                    .enum_decl = .{
                        .name_or_kind_tok = node_tok,
                        .container_qt = @bitCast(node_data[0]),
                        .fields = @ptrCast(tree.extra.items[node_data[1]..][0..node_data[2]]),
                    },
                },
                .enum_decl_two => .{
                    .enum_decl = .{
                        .name_or_kind_tok = node_tok,
                        .container_qt = @bitCast(node_data[0]),
                        .fields = unPackElems(node_data[1..]),
                    },
                },
                .struct_forward_decl => .{
                    .struct_forward_decl = .{
                        .name_or_kind_tok = node_tok,
                        .container_qt = @bitCast(node_data[0]),
                        .definition = null,
                    },
                },
                .union_forward_decl => .{
                    .union_forward_decl = .{
                        .name_or_kind_tok = node_tok,
                        .container_qt = @bitCast(node_data[0]),
                        .definition = null,
                    },
                },
                .enum_forward_decl => .{
                    .enum_forward_decl = .{
                        .name_or_kind_tok = node_tok,
                        .container_qt = @bitCast(node_data[0]),
                        .definition = null,
                    },
                },
                .enum_field => .{
                    .enum_field = .{
                        .name_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .init = unpackOptIndex(node_data[1]),
                    },
                },
                .record_field => .{
                    .record_field = .{
                        .name_or_first_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .bit_width = unpackOptIndex(node_data[1]),
                    },
                },
                .labeled_stmt => .{
                    .labeled_stmt = .{
                        .label_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .body = @enumFromInt(node_data[1]),
                    },
                },
                .compound_stmt => .{
                    .compound_stmt = .{
                        .l_brace_tok = node_tok,
                        .body = @ptrCast(tree.extra.items[node_data[0]..][0..node_data[1]]),
                    },
                },
                .compound_stmt_three => .{
                    .compound_stmt = .{
                        .l_brace_tok = node_tok,
                        .body = unPackElems(node_data),
                    },
                },
                .if_stmt => .{
                    .if_stmt = .{
                        .if_tok = node_tok,
                        .cond = @enumFromInt(node_data[0]),
                        .then_body = @enumFromInt(node_data[1]),
                        .else_body = unpackOptIndex(node_data[2]),
                    },
                },
                .switch_stmt => .{
                    .switch_stmt = .{
                        .switch_tok = node_tok,
                        .cond = @enumFromInt(node_data[0]),
                        .body = @enumFromInt(node_data[1]),
                    },
                },
                .case_stmt => .{
                    .case_stmt = .{
                        .case_tok = node_tok,
                        .start = @enumFromInt(node_data[0]),
                        .end = unpackOptIndex(node_data[1]),
                        .body = @enumFromInt(node_data[2]),
                    },
                },
                .default_stmt => .{
                    .default_stmt = .{
                        .default_tok = node_tok,
                        .body = @enumFromInt(node_data[0]),
                    },
                },
                .while_stmt => .{
                    .while_stmt = .{
                        .while_tok = node_tok,
                        .cond = @enumFromInt(node_data[0]),
                        .body = @enumFromInt(node_data[1]),
                    },
                },
                .do_while_stmt => .{
                    .do_while_stmt = .{
                        .do_tok = node_tok,
                        .cond = @enumFromInt(node_data[0]),
                        .body = @enumFromInt(node_data[1]),
                    },
                },
                .for_decl => .{
                    .for_stmt = .{
                        .for_tok = node_tok,
                        .init = .{ .decls = @ptrCast(tree.extra.items[node_data[0]..][0 .. node_data[1] - 2]) },
                        .cond = unpackOptIndex(tree.extra.items[node_data[0] + node_data[1] - 2]),
                        .incr = unpackOptIndex(tree.extra.items[node_data[0] + node_data[1] - 1]),
                        .body = @enumFromInt(node_data[2]),
                    },
                },
                .for_expr => .{
                    .for_stmt = .{
                        .for_tok = node_tok,
                        .init = .{ .expr = unpackOptIndex(node_data[0]) },
                        .cond = unpackOptIndex(tree.extra.items[node_data[1]]),
                        .incr = unpackOptIndex(tree.extra.items[node_data[1] + 1]),
                        .body = @enumFromInt(node_data[2]),
                    },
                },
                .goto_stmt => .{
                    .goto_stmt = .{
                        .label_tok = node_tok,
                    },
                },
                .computed_goto_stmt => .{
                    .computed_goto_stmt = .{
                        .goto_tok = node_tok,
                        .expr = @enumFromInt(node_data[0]),
                    },
                },
                .continue_stmt => .{
                    .continue_stmt = .{
                        .continue_tok = node_tok,
                    },
                },
                .break_stmt => .{
                    .break_stmt = .{
                        .break_tok = node_tok,
                    },
                },
                .null_stmt => .{
                    .null_stmt = .{
                        .semicolon_or_r_brace_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                    },
                },
                .return_stmt => .{
                    .return_stmt = .{
                        .return_tok = node_tok,
                        .return_qt = @bitCast(node_data[0]),
                        .operand = .{
                            .expr = @enumFromInt(node_data[1]),
                        },
                    },
                },
                .return_none_stmt => .{
                    .return_stmt = .{
                        .return_tok = node_tok,
                        .return_qt = @bitCast(node_data[0]),
                        .operand = .none,
                    },
                },
                .implicit_return => .{
                    .return_stmt = .{
                        .return_tok = node_tok,
                        .return_qt = @bitCast(node_data[0]),
                        .operand = .{
                            .implicit = node_data[1] != 0,
                        },
                    },
                },
                .asm_stmt, .asm_stmt_volatile, .asm_stmt_inline, .asm_stmt_inline_volatile => |tag| {
                    const extra = tree.extra.items;
                    var extra_index = node_data[2];

                    const operand_size = @sizeOf(AsmStmt.Operand) / @sizeOf(u32);

                    const outputs_len = extra[extra_index] * operand_size;
                    extra_index += 1;
                    const inputs_len = extra[extra_index] * operand_size;
                    extra_index += 1;
                    const clobbers_len = extra[extra_index];
                    extra_index += 1;

                    const labels_len = node_data[1];
                    const quals: GNUAssemblyQualifiers = .{
                        .@"inline" = tag == .asm_stmt_inline or tag == .asm_stmt_inline_volatile,
                        .@"volatile" = tag == .asm_stmt_volatile or tag == .asm_stmt_inline_volatile,
                        .goto = labels_len > 0,
                    };

                    const outputs = extra[extra_index..][0..outputs_len];
                    extra_index += outputs_len;
                    const inputs = extra[extra_index..][0..inputs_len];
                    extra_index += inputs_len;
                    const clobbers = extra[extra_index..][0..clobbers_len];
                    extra_index += clobbers_len;
                    const labels = extra[extra_index..][0..labels_len];
                    extra_index += labels_len;

                    return .{
                        .asm_stmt = .{
                            .asm_tok = node_tok,
                            .asm_str = @enumFromInt(node_data[0]),
                            .outputs = @ptrCast(outputs),
                            .inputs = @ptrCast(inputs),
                            .clobbers = @ptrCast(clobbers),
                            .labels = @ptrCast(labels),
                            .quals = quals,
                        },
                    };
                },
                .asm_stmt_simple => .{
                    .asm_stmt = .{
                        .asm_tok = node_tok,
                        .asm_str = @enumFromInt(node_data[0]),
                        .outputs = &.{},
                        .inputs = &.{},
                        .clobbers = &.{},
                        .labels = &.{},
                        .quals = @bitCast(node_data[1]),
                    },
                },
                .assign_expr => .{
                    .assign_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .mul_assign_expr => .{
                    .mul_assign_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .div_assign_expr => .{
                    .div_assign_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .mod_assign_expr => .{
                    .mod_assign_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .add_assign_expr => .{
                    .add_assign_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .sub_assign_expr => .{
                    .sub_assign_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .shl_assign_expr => .{
                    .shl_assign_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .shr_assign_expr => .{
                    .shr_assign_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .bit_and_assign_expr => .{
                    .bit_and_assign_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .bit_xor_assign_expr => .{
                    .bit_xor_assign_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .bit_or_assign_expr => .{
                    .bit_or_assign_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .compound_assign_dummy_expr => .{
                    .compound_assign_dummy_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .comma_expr => .{
                    .comma_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .bool_or_expr => .{
                    .bool_or_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .bool_and_expr => .{
                    .bool_and_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .bit_or_expr => .{
                    .bit_or_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .bit_xor_expr => .{
                    .bit_xor_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .bit_and_expr => .{
                    .bit_and_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .equal_expr => .{
                    .equal_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .not_equal_expr => .{
                    .not_equal_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .less_than_expr => .{
                    .less_than_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .less_than_equal_expr => .{
                    .less_than_equal_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .greater_than_expr => .{
                    .greater_than_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .greater_than_equal_expr => .{
                    .greater_than_equal_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .shl_expr => .{
                    .shl_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .shr_expr => .{
                    .shr_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .add_expr => .{
                    .add_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .sub_expr => .{
                    .sub_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .mul_expr => .{
                    .mul_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .div_expr => .{
                    .div_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .mod_expr => .{
                    .mod_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(node_data[1]),
                        .rhs = @enumFromInt(node_data[2]),
                    },
                },
                .explicit_cast => .{
                    .cast = .{
                        .l_paren = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .kind = @enumFromInt(node_data[1]),
                        .operand = @enumFromInt(node_data[2]),
                        .implicit = false,
                    },
                },
                .implicit_cast => .{
                    .cast = .{
                        .l_paren = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .kind = @enumFromInt(node_data[1]),
                        .operand = @enumFromInt(node_data[2]),
                        .implicit = true,
                    },
                },
                .addr_of_expr => .{
                    .addr_of_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .deref_expr => .{
                    .deref_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .plus_expr => .{
                    .plus_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .negate_expr => .{
                    .negate_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .bit_not_expr => .{
                    .bit_not_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .bool_not_expr => .{
                    .bool_not_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .pre_inc_expr => .{
                    .pre_inc_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .pre_dec_expr => .{
                    .pre_dec_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .imag_expr => .{
                    .imag_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .real_expr => .{
                    .real_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .post_inc_expr => .{
                    .post_inc_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .post_dec_expr => .{
                    .post_dec_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .paren_expr => .{
                    .paren_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .stmt_expr => .{
                    .stmt_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .cond_dummy_expr => .{
                    .cond_dummy_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .addr_of_label => .{
                    .addr_of_label = .{
                        .label_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                    },
                },
                .array_access_expr => .{
                    .array_access_expr = .{
                        .l_bracket_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .base = @enumFromInt(node_data[1]),
                        .index = @enumFromInt(node_data[2]),
                    },
                },
                .call_expr => .{
                    .call_expr = .{
                        .l_paren_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .callee = @enumFromInt(tree.extra.items[node_data[1]]),
                        .args = @ptrCast(tree.extra.items[node_data[1] + 1 ..][0 .. node_data[2] - 1]),
                    },
                },
                .call_expr_one => .{
                    .call_expr = .{
                        .l_paren_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .callee = @enumFromInt(node_data[1]),
                        .args = unPackElems(node_data[2..]),
                    },
                },
                .builtin_call_expr => .{
                    .builtin_call_expr = .{
                        .builtin_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .args = @ptrCast(tree.extra.items[node_data[1]..][0..node_data[2]]),
                    },
                },
                .builtin_call_expr_two => .{
                    .builtin_call_expr = .{
                        .builtin_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .args = unPackElems(node_data[1..]),
                    },
                },
                .member_access_expr => .{
                    .member_access_expr = .{
                        .access_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .base = @enumFromInt(node_data[1]),
                        .member_index = node_data[2],
                    },
                },
                .member_access_ptr_expr => .{
                    .member_access_ptr_expr = .{
                        .access_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .base = @enumFromInt(node_data[1]),
                        .member_index = node_data[2],
                    },
                },
                .decl_ref_expr => .{
                    .decl_ref_expr = .{
                        .name_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .decl = @enumFromInt(node_data[1]),
                    },
                },
                .enumeration_ref => .{
                    .enumeration_ref = .{
                        .name_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .decl = @enumFromInt(node_data[1]),
                    },
                },
                .builtin_ref => .{
                    .builtin_ref = .{
                        .name_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                    },
                },
                .bool_literal => .{
                    .bool_literal = .{
                        .literal_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                    },
                },
                .nullptr_literal => .{
                    .nullptr_literal = .{
                        .literal_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                    },
                },
                .int_literal => .{
                    .int_literal = .{
                        .literal_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                    },
                },
                .char_literal => .{
                    .char_literal = .{
                        .literal_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .kind = @enumFromInt(node_data[1]),
                    },
                },
                .float_literal => .{
                    .float_literal = .{
                        .literal_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                    },
                },
                .string_literal_expr => .{
                    .string_literal_expr = .{
                        .literal_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .kind = @enumFromInt(node_data[1]),
                    },
                },
                .imaginary_literal => .{
                    .imaginary_literal = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .sizeof_expr => .{
                    .sizeof_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .expr = unpackOptIndex(node_data[1]),
                        .operand_qt = @bitCast(node_data[2]),
                    },
                },
                .alignof_expr => .{
                    .alignof_expr = .{
                        .op_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .expr = unpackOptIndex(node_data[1]),
                        .operand_qt = @bitCast(node_data[2]),
                    },
                },

                .generic_expr_zero => .{
                    .generic_expr = .{
                        .generic_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .controlling = @enumFromInt(node_data[1]),
                        .chosen = @enumFromInt(node_data[2]),
                        .rest = &.{},
                    },
                },
                .generic_expr => .{
                    .generic_expr = .{
                        .generic_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .controlling = @enumFromInt(tree.extra.items[node_data[1]]),
                        .chosen = @enumFromInt(tree.extra.items[node_data[1] + 1]),
                        .rest = @ptrCast(tree.extra.items[node_data[1] + 2 ..][0 .. node_data[2] - 2]),
                    },
                },
                .generic_association_expr => .{
                    .generic_association_expr = .{
                        .colon_tok = node_tok,
                        .association_qt = @bitCast(node_data[0]),
                        .expr = @enumFromInt(node_data[1]),
                    },
                },
                .generic_default_expr => .{
                    .generic_default_expr = .{
                        .default_tok = node_tok,
                        .expr = @enumFromInt(node_data[0]),
                    },
                },
                .binary_cond_expr => .{
                    .binary_cond_expr = .{
                        .cond_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .cond = @enumFromInt(node_data[1]),
                        .then_expr = @enumFromInt(tree.extra.items[node_data[2]]),
                        .else_expr = @enumFromInt(tree.extra.items[node_data[2] + 1]),
                    },
                },
                .cond_expr => .{
                    .cond_expr = .{
                        .cond_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .cond = @enumFromInt(node_data[1]),
                        .then_expr = @enumFromInt(tree.extra.items[node_data[2]]),
                        .else_expr = @enumFromInt(tree.extra.items[node_data[2] + 1]),
                    },
                },
                .builtin_choose_expr => .{
                    .builtin_choose_expr = .{
                        .cond_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .cond = @enumFromInt(node_data[1]),
                        .then_expr = @enumFromInt(tree.extra.items[node_data[2]]),
                        .else_expr = @enumFromInt(tree.extra.items[node_data[2] + 1]),
                    },
                },
                .builtin_types_compatible_p => .{
                    .builtin_types_compatible_p = .{
                        .builtin_tok = node_tok,
                        .lhs = @bitCast(node_data[0]),
                        .rhs = @bitCast(node_data[1]),
                    },
                },
                .builtin_convertvector => .{
                    .builtin_convertvector = .{
                        .builtin_tok = node_tok,
                        .dest_qt = @bitCast(node_data[0]),
                        .operand = @enumFromInt(node_data[1]),
                    },
                },
                .builtin_shufflevector => .{
                    .builtin_shufflevector = .{
                        .builtin_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .lhs = @enumFromInt(tree.extra.items[node_data[1]]),
                        .rhs = @enumFromInt(tree.extra.items[node_data[1] + 1]),
                        .indexes = @ptrCast(tree.extra.items[node_data[1] + 2 ..][0..node_data[2]]),
                    },
                },
                .array_init_expr_two => .{
                    .array_init_expr = .{
                        .l_brace_tok = node_tok,
                        .container_qt = @bitCast(node_data[0]),
                        .items = unPackElems(node_data[1..]),
                    },
                },
                .array_init_expr => .{
                    .array_init_expr = .{
                        .l_brace_tok = node_tok,
                        .container_qt = @bitCast(node_data[0]),
                        .items = @ptrCast(tree.extra.items[node_data[1]..][0..node_data[2]]),
                    },
                },
                .struct_init_expr_two => .{
                    .struct_init_expr = .{
                        .l_brace_tok = node_tok,
                        .container_qt = @bitCast(node_data[0]),
                        .items = unPackElems(node_data[1..]),
                    },
                },
                .struct_init_expr => .{
                    .struct_init_expr = .{
                        .l_brace_tok = node_tok,
                        .container_qt = @bitCast(node_data[0]),
                        .items = @ptrCast(tree.extra.items[node_data[1]..][0..node_data[2]]),
                    },
                },
                .union_init_expr => .{
                    .union_init_expr = .{
                        .l_brace_tok = node_tok,
                        .union_qt = @bitCast(node_data[0]),
                        .field_index = node_data[1],
                        .initializer = unpackOptIndex(node_data[2]),
                    },
                },
                .array_filler_expr => .{
                    .array_filler_expr = .{
                        .last_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                        .count = @bitCast(node_data[1..].*),
                    },
                },
                .default_init_expr => .{
                    .default_init_expr = .{
                        .last_tok = node_tok,
                        .qt = @bitCast(node_data[0]),
                    },
                },
                .compound_literal_expr => {
                    const attr: Node.Repr.DeclAttr = @bitCast(node_data[1]);
                    return .{
                        .compound_literal_expr = .{
                            .l_paren_tok = node_tok,
                            .qt = @bitCast(node_data[0]),
                            .storage_class = if (attr.static)
                                .static
                            else if (attr.register)
                                .register
                            else
                                .auto,
                            .thread_local = attr.thread_local,
                            .initializer = @enumFromInt(node_data[2]),
                        },
                    };
                },
            };
        }

        pub fn tok(index: Index, tree: *const Tree) TokenIndex {
            return tree.nodes.items(.tok)[@intFromEnum(index)];
        }

        pub fn loc(index: Index, tree: *const Tree) Source.Location {
            const tok_i = index.tok(tree);
            return tree.tokens.items(.loc)[tok_i];
        }

        pub fn qt(index: Index, tree: *const Tree) QualType {
            return index.qtOrNull(tree) orelse .void;
        }

        pub fn qtOrNull(index: Index, tree: *const Tree) ?QualType {
            return switch (tree.nodes.items(.tag)[@intFromEnum(index)]) {
                .empty_decl,
                .static_assert,
                .compound_stmt,
                .compound_stmt_three,
                .if_stmt,
                .switch_stmt,
                .case_stmt,
                .default_stmt,
                .while_stmt,
                .do_while_stmt,
                .for_decl,
                .for_expr,
                .goto_stmt,
                .computed_goto_stmt,
                .continue_stmt,
                .break_stmt,
                .asm_stmt,
                .asm_stmt_volatile,
                .asm_stmt_inline,
                .asm_stmt_inline_volatile,
                .asm_stmt_simple,
                .global_asm,
                .generic_association_expr,
                .generic_default_expr,
                => null,
                .builtin_types_compatible_p => .int,
                else => {
                    // If a node is typed the type is stored in data[0].
                    return @bitCast(tree.nodes.items(.data)[@intFromEnum(index)][0]);
                },
            };
        }
    };

    pub const OptIndex = enum(u32) {
        null = std.math.maxInt(u32),
        _,

        pub fn unpack(opt: OptIndex) ?Index {
            return if (opt == .null) null else @enumFromInt(@intFromEnum(opt));
        }

        pub fn pack(index: Index) OptIndex {
            return @enumFromInt(@intFromEnum(index));
        }

        pub fn packOpt(optional: ?Index) OptIndex {
            return if (optional) |some| @enumFromInt(@intFromEnum(some)) else .null;
        }
    };

    pub const Repr = struct {
        tag: Tag,
        /// If a node is typed the type is stored in data[0]
        data: [3]u32,
        tok: TokenIndex,

        pub const DeclAttr = packed struct(u32) {
            @"extern": bool = false,
            static: bool = false,
            @"inline": bool = false,
            thread_local: bool = false,
            implicit: bool = false,
            register: bool = false,
            _: u26 = 0,
        };

        pub const Tag = enum(u8) {
            empty_decl,
            static_assert,
            fn_proto,
            fn_def,
            param,
            variable,
            variable_def,
            typedef,
            global_asm,
            struct_decl,
            union_decl,
            enum_decl,
            struct_decl_two,
            union_decl_two,
            enum_decl_two,
            struct_forward_decl,
            union_forward_decl,
            enum_forward_decl,
            enum_field,
            record_field,
            labeled_stmt,
            compound_stmt,
            compound_stmt_three,
            if_stmt,
            switch_stmt,
            case_stmt,
            default_stmt,
            while_stmt,
            do_while_stmt,
            for_expr,
            for_decl,
            goto_stmt,
            computed_goto_stmt,
            continue_stmt,
            break_stmt,
            null_stmt,
            return_stmt,
            return_none_stmt,
            implicit_return,
            asm_stmt,
            asm_stmt_inline,
            asm_stmt_volatile,
            asm_stmt_inline_volatile,
            asm_stmt_simple,
            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,
            compound_assign_dummy_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,
            explicit_cast,
            implicit_cast,
            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,
            addr_of_label,
            array_access_expr,
            call_expr_one,
            call_expr,
            builtin_call_expr,
            builtin_call_expr_two,
            member_access_expr,
            member_access_ptr_expr,
            decl_ref_expr,
            enumeration_ref,
            builtin_ref,
            bool_literal,
            nullptr_literal,
            int_literal,
            char_literal,
            float_literal,
            string_literal_expr,
            imaginary_literal,
            sizeof_expr,
            alignof_expr,
            generic_expr,
            generic_expr_zero,
            generic_association_expr,
            generic_default_expr,
            binary_cond_expr,
            cond_dummy_expr,
            cond_expr,
            builtin_choose_expr,
            builtin_types_compatible_p,
            builtin_convertvector,
            builtin_shufflevector,
            array_init_expr,
            array_init_expr_two,
            struct_init_expr,
            struct_init_expr_two,
            union_init_expr,
            array_filler_expr,
            default_init_expr,
            compound_literal_expr,
        };
    };

    pub fn isImplicit(node: Node) bool {
        return switch (node) {
            .array_filler_expr,
            .default_init_expr,
            .cond_dummy_expr,
            .compound_assign_dummy_expr,
            => true,
            .return_stmt => |ret| ret.operand == .implicit,
            .cast => |cast| cast.implicit,
            .variable => |info| info.implicit,
            .typedef => |info| info.implicit,
            else => false,
        };
    }
};

pub fn addNode(tree: *Tree, node: Node) !Node.Index {
    const index = try tree.nodes.addOne(tree.comp.gpa);
    try tree.setNode(node, index);
    return @enumFromInt(index);
}

pub fn setNode(tree: *Tree, node: Node, index: usize) !void {
    var repr: Node.Repr = undefined;
    switch (node) {
        .empty_decl => |empty| {
            repr.tag = .empty_decl;
            repr.tok = empty.semicolon;
        },
        .static_assert => |assert| {
            repr.tag = .static_assert;
            repr.data[0] = @intFromEnum(assert.cond);
            repr.data[1] = packOptIndex(assert.message);
            repr.tok = assert.assert_tok;
        },
        .function => |function| {
            repr.tag = if (function.body != null) .fn_def else .fn_proto;
            repr.data[0] = @bitCast(function.qt);
            repr.data[1] = @bitCast(Node.Repr.DeclAttr{
                .static = function.static,
                .@"inline" = function.@"inline",
            });
            if (function.body) |some| {
                repr.data[2] = @intFromEnum(some);
            } else {
                repr.data[2] = packOptIndex(function.definition);
            }
            repr.tok = function.name_tok;
        },
        .param => |param| {
            repr.tag = .param;
            repr.data[0] = @bitCast(param.qt);
            repr.data[1] = @bitCast(Node.Repr.DeclAttr{
                .register = param.storage_class == .register,
            });
            repr.tok = param.name_tok;
        },
        .variable => |variable| {
            repr.tag = if (variable.initializer != null) .variable_def else .variable;
            repr.data[0] = @bitCast(variable.qt);
            repr.data[1] = @bitCast(Node.Repr.DeclAttr{
                .@"extern" = variable.storage_class == .@"extern",
                .static = variable.storage_class == .static,
                .thread_local = variable.thread_local,
                .implicit = variable.implicit,
                .register = variable.storage_class == .register,
            });
            if (variable.initializer) |some| {
                repr.data[2] = @intFromEnum(some);
            } else {
                repr.data[2] = packOptIndex(variable.definition);
            }
            repr.tok = variable.name_tok;
        },
        .typedef => |typedef| {
            repr.tag = .typedef;
            repr.data[0] = @bitCast(typedef.qt);
            repr.data[1] = @intFromBool(typedef.implicit);
            repr.tok = typedef.name_tok;
        },
        .global_asm => |global_asm| {
            repr.tag = .global_asm;
            repr.data[0] = @intFromEnum(global_asm.asm_str);
            repr.tok = global_asm.asm_tok;
        },
        .struct_decl => |decl| {
            repr.data[0] = @bitCast(decl.container_qt);
            if (decl.fields.len > 2) {
                repr.tag = .struct_decl;
                repr.data[1], repr.data[2] = try tree.addExtra(decl.fields);
            } else {
                repr.tag = .struct_decl_two;
                repr.data[1] = packElem(decl.fields, 0);
                repr.data[2] = packElem(decl.fields, 1);
            }
            repr.tok = decl.name_or_kind_tok;
        },
        .union_decl => |decl| {
            repr.data[0] = @bitCast(decl.container_qt);
            if (decl.fields.len > 2) {
                repr.tag = .union_decl;
                repr.data[1], repr.data[2] = try tree.addExtra(decl.fields);
            } else {
                repr.tag = .union_decl_two;
                repr.data[1] = packElem(decl.fields, 0);
                repr.data[2] = packElem(decl.fields, 1);
            }
            repr.tok = decl.name_or_kind_tok;
        },
        .enum_decl => |decl| {
            repr.data[0] = @bitCast(decl.container_qt);
            if (decl.fields.len > 2) {
                repr.tag = .enum_decl;
                repr.data[1], repr.data[2] = try tree.addExtra(decl.fields);
            } else {
                repr.tag = .enum_decl_two;
                repr.data[1] = packElem(decl.fields, 0);
                repr.data[2] = packElem(decl.fields, 1);
            }
            repr.tok = decl.name_or_kind_tok;
        },
        .struct_forward_decl => |decl| {
            repr.tag = .struct_forward_decl;
            repr.data[0] = @bitCast(decl.container_qt);
            // TODO decide how to handle definition
            // repr.data[1] = decl.definition;
            repr.tok = decl.name_or_kind_tok;
        },
        .union_forward_decl => |decl| {
            repr.tag = .union_forward_decl;
            repr.data[0] = @bitCast(decl.container_qt);
            // TODO decide how to handle definition
            // repr.data[1] = decl.definition;
            repr.tok = decl.name_or_kind_tok;
        },
        .enum_forward_decl => |decl| {
            repr.tag = .enum_forward_decl;
            repr.data[0] = @bitCast(decl.container_qt);
            // TODO decide how to handle definition
            // repr.data[1] = decl.definition;
            repr.tok = decl.name_or_kind_tok;
        },
        .enum_field => |field| {
            repr.tag = .enum_field;
            repr.data[0] = @bitCast(field.qt);
            repr.data[1] = packOptIndex(field.init);
            repr.tok = field.name_tok;
        },
        .record_field => |field| {
            repr.tag = .record_field;
            repr.data[0] = @bitCast(field.qt);
            repr.data[1] = packOptIndex(field.bit_width);
            repr.tok = field.name_or_first_tok;
        },
        .labeled_stmt => |labeled| {
            repr.tag = .labeled_stmt;
            repr.data[0] = @bitCast(labeled.qt);
            repr.data[1] = @intFromEnum(labeled.body);
            repr.tok = labeled.label_tok;
        },
        .compound_stmt => |compound| {
            if (compound.body.len > 3) {
                repr.tag = .compound_stmt;
                repr.data[0], repr.data[1] = try tree.addExtra(compound.body);
            } else {
                repr.tag = .compound_stmt_three;
                for (&repr.data, 0..) |*data, idx|
                    data.* = packElem(compound.body, idx);
            }
            repr.tok = compound.l_brace_tok;
        },
        .if_stmt => |@"if"| {
            repr.tag = .if_stmt;
            repr.data[0] = @intFromEnum(@"if".cond);
            repr.data[1] = @intFromEnum(@"if".then_body);
            repr.data[2] = packOptIndex(@"if".else_body);
            repr.tok = @"if".if_tok;
        },
        .switch_stmt => |@"switch"| {
            repr.tag = .switch_stmt;
            repr.data[0] = @intFromEnum(@"switch".cond);
            repr.data[1] = @intFromEnum(@"switch".body);
            repr.tok = @"switch".switch_tok;
        },
        .case_stmt => |case| {
            repr.tag = .case_stmt;
            repr.data[0] = @intFromEnum(case.start);
            repr.data[1] = packOptIndex(case.end);
            repr.data[2] = packOptIndex(case.body);
            repr.tok = case.case_tok;
        },
        .default_stmt => |default| {
            repr.tag = .default_stmt;
            repr.data[0] = @intFromEnum(default.body);
            repr.tok = default.default_tok;
        },
        .while_stmt => |@"while"| {
            repr.tag = .while_stmt;
            repr.data[0] = @intFromEnum(@"while".cond);
            repr.data[1] = @intFromEnum(@"while".body);
            repr.tok = @"while".while_tok;
        },
        .do_while_stmt => |do_while| {
            repr.tag = .do_while_stmt;
            repr.data[0] = @intFromEnum(do_while.cond);
            repr.data[1] = @intFromEnum(do_while.body);
            repr.tok = do_while.do_tok;
        },
        .for_stmt => |@"for"| {
            switch (@"for".init) {
                .decls => |decls| {
                    repr.tag = .for_decl;
                    repr.data[0] = @intCast(tree.extra.items.len);
                    const len: u32 = @intCast(decls.len + 2);
                    try tree.extra.ensureUnusedCapacity(tree.comp.gpa, len);
                    repr.data[1] = len;
                    tree.extra.appendSliceAssumeCapacity(@ptrCast(decls));
                    tree.extra.appendAssumeCapacity(packOptIndex(@"for".cond));
                    tree.extra.appendAssumeCapacity(packOptIndex(@"for".incr));
                },
                .expr => |expr| {
                    repr.tag = .for_expr;
                    repr.data[0] = packOptIndex(expr);
                    repr.data[1] = @intCast(tree.extra.items.len);
                    try tree.extra.ensureUnusedCapacity(tree.comp.gpa, 2);
                    tree.extra.appendAssumeCapacity(packOptIndex(@"for".cond));
                    tree.extra.appendAssumeCapacity(packOptIndex(@"for".incr));
                },
            }
            repr.data[2] = @intFromEnum(@"for".body);
            repr.tok = @"for".for_tok;
        },
        .goto_stmt => |goto| {
            repr.tag = .goto_stmt;
            repr.tok = goto.label_tok;
        },
        .computed_goto_stmt => |computed_goto| {
            repr.tag = .computed_goto_stmt;
            repr.data[0] = @intFromEnum(computed_goto.expr);
            repr.tok = computed_goto.goto_tok;
        },
        .continue_stmt => |@"continue"| {
            repr.tag = .continue_stmt;
            repr.tok = @"continue".continue_tok;
        },
        .break_stmt => |@"break"| {
            repr.tag = .break_stmt;
            repr.tok = @"break".break_tok;
        },
        .null_stmt => |@"null"| {
            repr.tag = .null_stmt;
            repr.data[0] = @bitCast(@"null".qt);
            repr.tok = @"null".semicolon_or_r_brace_tok;
        },
        .return_stmt => |@"return"| {
            repr.data[0] = @bitCast(@"return".return_qt);
            switch (@"return".operand) {
                .expr => |expr| {
                    repr.tag = .return_stmt;
                    repr.data[1] = @intFromEnum(expr);
                },
                .none => {
                    repr.tag = .return_none_stmt;
                },
                .implicit => |zeroes| {
                    repr.tag = .implicit_return;
                    repr.data[1] = @intFromBool(zeroes);
                },
            }
            repr.tok = @"return".return_tok;
        },
        .asm_stmt => |asm_stmt| {
            repr.tok = asm_stmt.asm_tok;
            repr.data[0] = @intFromEnum(asm_stmt.asm_str);
            if (asm_stmt.outputs.len == 0 and asm_stmt.inputs.len == 0 and asm_stmt.clobbers.len == 0 and asm_stmt.labels.len == 0) {
                repr.tag = .asm_stmt_simple;
                repr.data[1] = @bitCast(asm_stmt.quals);
            } else {
                if (asm_stmt.quals.@"inline" and asm_stmt.quals.@"volatile") {
                    repr.tag = .asm_stmt_inline_volatile;
                } else if (asm_stmt.quals.@"inline") {
                    repr.tag = .asm_stmt_inline;
                } else if (asm_stmt.quals.@"volatile") {
                    repr.tag = .asm_stmt_volatile;
                } else {
                    repr.tag = .asm_stmt;
                }
                repr.data[1] = @intCast(asm_stmt.labels.len);
                repr.data[2] = @intCast(tree.extra.items.len);

                const operand_size = @sizeOf(Node.AsmStmt.Operand) / @sizeOf(u32);
                try tree.extra.ensureUnusedCapacity(tree.comp.gpa, 3 // lens
                    + (asm_stmt.outputs.len + asm_stmt.inputs.len) * operand_size // outputs inputs
                    + asm_stmt.clobbers.len + asm_stmt.labels.len);

                tree.extra.appendAssumeCapacity(@intCast(asm_stmt.outputs.len));
                tree.extra.appendAssumeCapacity(@intCast(asm_stmt.inputs.len));
                tree.extra.appendAssumeCapacity(@intCast(asm_stmt.clobbers.len));

                tree.extra.appendSliceAssumeCapacity(@ptrCast(asm_stmt.outputs));
                tree.extra.appendSliceAssumeCapacity(@ptrCast(asm_stmt.inputs));
                tree.extra.appendSliceAssumeCapacity(@ptrCast(asm_stmt.clobbers));
                tree.extra.appendSliceAssumeCapacity(@ptrCast(asm_stmt.labels));
            }
        },
        .assign_expr => |bin| {
            repr.tag = .assign_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .mul_assign_expr => |bin| {
            repr.tag = .mul_assign_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .div_assign_expr => |bin| {
            repr.tag = .div_assign_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .mod_assign_expr => |bin| {
            repr.tag = .mod_assign_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .add_assign_expr => |bin| {
            repr.tag = .add_assign_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .sub_assign_expr => |bin| {
            repr.tag = .sub_assign_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .shl_assign_expr => |bin| {
            repr.tag = .shl_assign_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .shr_assign_expr => |bin| {
            repr.tag = .shr_assign_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .bit_and_assign_expr => |bin| {
            repr.tag = .bit_and_assign_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .bit_xor_assign_expr => |bin| {
            repr.tag = .bit_xor_assign_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .bit_or_assign_expr => |bin| {
            repr.tag = .bit_or_assign_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .compound_assign_dummy_expr => |un| {
            repr.tag = .compound_assign_dummy_expr;
            repr.data[0] = @bitCast(un.qt);
            repr.data[1] = @intFromEnum(un.operand);
            repr.tok = un.op_tok;
        },
        .comma_expr => |bin| {
            repr.tag = .comma_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .bool_or_expr => |bin| {
            repr.tag = .bool_or_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .bool_and_expr => |bin| {
            repr.tag = .bool_and_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .bit_or_expr => |bin| {
            repr.tag = .bit_or_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .bit_xor_expr => |bin| {
            repr.tag = .bit_xor_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .bit_and_expr => |bin| {
            repr.tag = .bit_and_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .equal_expr => |bin| {
            repr.tag = .equal_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .not_equal_expr => |bin| {
            repr.tag = .not_equal_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .less_than_expr => |bin| {
            repr.tag = .less_than_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .less_than_equal_expr => |bin| {
            repr.tag = .less_than_equal_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .greater_than_expr => |bin| {
            repr.tag = .greater_than_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .greater_than_equal_expr => |bin| {
            repr.tag = .greater_than_equal_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .shl_expr => |bin| {
            repr.tag = .shl_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .shr_expr => |bin| {
            repr.tag = .shr_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .add_expr => |bin| {
            repr.tag = .add_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .sub_expr => |bin| {
            repr.tag = .sub_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .mul_expr => |bin| {
            repr.tag = .mul_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .div_expr => |bin| {
            repr.tag = .div_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .mod_expr => |bin| {
            repr.tag = .mod_expr;
            repr.data[0] = @bitCast(bin.qt);
            repr.data[1] = @intFromEnum(bin.lhs);
            repr.data[2] = @intFromEnum(bin.rhs);
            repr.tok = bin.op_tok;
        },
        .cast => |cast| {
            repr.tag = if (cast.implicit) .implicit_cast else .explicit_cast;
            repr.data[0] = @bitCast(cast.qt);
            repr.data[1] = @intFromEnum(cast.kind);
            repr.data[2] = @intFromEnum(cast.operand);
            repr.tok = cast.l_paren;
        },
        .addr_of_expr => |un| {
            repr.tag = .addr_of_expr;
            repr.data[0] = @bitCast(un.qt);
            repr.data[1] = @intFromEnum(un.operand);
            repr.tok = un.op_tok;
        },
        .deref_expr => |un| {
            repr.tag = .deref_expr;
            repr.data[0] = @bitCast(un.qt);
            repr.data[1] = @intFromEnum(un.operand);
            repr.tok = un.op_tok;
        },
        .plus_expr => |un| {
            repr.tag = .plus_expr;
            repr.data[0] = @bitCast(un.qt);
            repr.data[1] = @intFromEnum(un.operand);
            repr.tok = un.op_tok;
        },
        .negate_expr => |un| {
            repr.tag = .negate_expr;
            repr.data[0] = @bitCast(un.qt);
            repr.data[1] = @intFromEnum(un.operand);
            repr.tok = un.op_tok;
        },
        .bit_not_expr => |un| {
            repr.tag = .bit_not_expr;
            repr.data[0] = @bitCast(un.qt);
            repr.data[1] = @intFromEnum(un.operand);
            repr.tok = un.op_tok;
        },
        .bool_not_expr => |un| {
            repr.tag = .bool_not_expr;
            repr.data[0] = @bitCast(un.qt);
            repr.data[1] = @intFromEnum(un.operand);
            repr.tok = un.op_tok;
        },
        .pre_inc_expr => |un| {
            repr.tag = .pre_inc_expr;
            repr.data[0] = @bitCast(un.qt);
            repr.data[1] = @intFromEnum(un.operand);
            repr.tok = un.op_tok;
        },
        .pre_dec_expr => |un| {
            repr.tag = .pre_dec_expr;
            repr.data[0] = @bitCast(un.qt);
            repr.data[1] = @intFromEnum(un.operand);
            repr.tok = un.op_tok;
        },
        .imag_expr => |un| {
            repr.tag = .imag_expr;
            repr.data[0] = @bitCast(un.qt);
            repr.data[1] = @intFromEnum(un.operand);
            repr.tok = un.op_tok;
        },
        .real_expr => |un| {
            repr.tag = .real_expr;
            repr.data[0] = @bitCast(un.qt);
            repr.data[1] = @intFromEnum(un.operand);
            repr.tok = un.op_tok;
        },
        .post_inc_expr => |un| {
            repr.tag = .post_inc_expr;
            repr.data[0] = @bitCast(un.qt);
            repr.data[1] = @intFromEnum(un.operand);
            repr.tok = un.op_tok;
        },
        .post_dec_expr => |un| {
            repr.tag = .post_dec_expr;
            repr.data[0] = @bitCast(un.qt);
            repr.data[1] = @intFromEnum(un.operand);
            repr.tok = un.op_tok;
        },
        .paren_expr => |un| {
            repr.tag = .paren_expr;
            repr.data[0] = @bitCast(un.qt);
            repr.data[1] = @intFromEnum(un.operand);
            repr.tok = un.op_tok;
        },
        .stmt_expr => |un| {
            repr.tag = .stmt_expr;
            repr.data[0] = @bitCast(un.qt);
            repr.data[1] = @intFromEnum(un.operand);
            repr.tok = un.op_tok;
        },
        .cond_dummy_expr => |un| {
            repr.tag = .cond_dummy_expr;
            repr.data[0] = @bitCast(un.qt);
            repr.data[1] = @intFromEnum(un.operand);
            repr.tok = un.op_tok;
        },
        .addr_of_label => |addr_of| {
            repr.tag = .addr_of_label;
            repr.data[0] = @bitCast(addr_of.qt);
            repr.tok = addr_of.label_tok;
        },
        .array_access_expr => |access| {
            repr.tag = .array_access_expr;
            repr.data[0] = @bitCast(access.qt);
            repr.data[1] = @intFromEnum(access.base);
            repr.data[2] = @intFromEnum(access.index);
            repr.tok = access.l_bracket_tok;
        },
        .call_expr => |call| {
            repr.data[0] = @bitCast(call.qt);
            if (call.args.len > 1) {
                repr.tag = .call_expr;
                repr.data[1] = @intCast(tree.extra.items.len);
                const len: u32 = @intCast(call.args.len + 1);
                repr.data[2] = len;
                try tree.extra.ensureUnusedCapacity(tree.comp.gpa, len);
                tree.extra.appendAssumeCapacity(@intFromEnum(call.callee));
                tree.extra.appendSliceAssumeCapacity(@ptrCast(call.args));
            } else {
                repr.tag = .call_expr_one;
                repr.data[1] = @intFromEnum(call.callee);
                repr.data[2] = packElem(call.args, 0);
            }
            repr.tok = call.l_paren_tok;
        },
        .builtin_call_expr => |call| {
            repr.data[0] = @bitCast(call.qt);
            if (call.args.len > 2) {
                repr.tag = .builtin_call_expr;
                repr.data[1], repr.data[2] = try tree.addExtra(call.args);
            } else {
                repr.tag = .builtin_call_expr_two;
                repr.data[1] = packElem(call.args, 0);
                repr.data[2] = packElem(call.args, 1);
            }
            repr.tok = call.builtin_tok;
        },
        .member_access_expr => |access| {
            repr.tag = .member_access_expr;
            repr.data[0] = @bitCast(access.qt);
            repr.data[1] = @intFromEnum(access.base);
            repr.data[2] = access.member_index;
            repr.tok = access.access_tok;
        },
        .member_access_ptr_expr => |access| {
            repr.tag = .member_access_ptr_expr;
            repr.data[0] = @bitCast(access.qt);
            repr.data[1] = @intFromEnum(access.base);
            repr.data[2] = access.member_index;
            repr.tok = access.access_tok;
        },
        .decl_ref_expr => |decl_ref| {
            repr.tag = .decl_ref_expr;
            repr.data[0] = @bitCast(decl_ref.qt);
            repr.data[1] = @intFromEnum(decl_ref.decl);
            repr.tok = decl_ref.name_tok;
        },
        .enumeration_ref => |enumeration_ref| {
            repr.tag = .enumeration_ref;
            repr.data[0] = @bitCast(enumeration_ref.qt);
            repr.data[1] = @intFromEnum(enumeration_ref.decl);
            repr.tok = enumeration_ref.name_tok;
        },
        .builtin_ref => |builtin_ref| {
            repr.tag = .builtin_ref;
            repr.data[0] = @bitCast(builtin_ref.qt);
            repr.tok = builtin_ref.name_tok;
        },
        .bool_literal => |literal| {
            repr.tag = .bool_literal;
            repr.data[0] = @bitCast(literal.qt);
            repr.tok = literal.literal_tok;
        },
        .nullptr_literal => |literal| {
            repr.tag = .nullptr_literal;
            repr.data[0] = @bitCast(literal.qt);
            repr.tok = literal.literal_tok;
        },
        .int_literal => |literal| {
            repr.tag = .int_literal;
            repr.data[0] = @bitCast(literal.qt);
            repr.tok = literal.literal_tok;
        },
        .char_literal => |literal| {
            repr.tag = .char_literal;
            repr.data[0] = @bitCast(literal.qt);
            repr.data[1] = @intFromEnum(literal.kind);
            repr.tok = literal.literal_tok;
        },
        .float_literal => |literal| {
            repr.tag = .float_literal;
            repr.data[0] = @bitCast(literal.qt);
            repr.tok = literal.literal_tok;
        },
        .string_literal_expr => |literal| {
            repr.tag = .string_literal_expr;
            repr.data[0] = @bitCast(literal.qt);
            repr.data[1] = @intFromEnum(literal.kind);
            repr.tok = literal.literal_tok;
        },
        .imaginary_literal => |un| {
            repr.tag = .imaginary_literal;
            repr.data[0] = @bitCast(un.qt);
            repr.data[1] = @intFromEnum(un.operand);
            repr.tok = un.op_tok;
        },
        .sizeof_expr => |type_info| {
            repr.tag = .sizeof_expr;
            repr.data[0] = @bitCast(type_info.qt);
            repr.data[1] = packOptIndex(type_info.expr);
            repr.data[2] = @bitCast(type_info.operand_qt);
            repr.tok = type_info.op_tok;
        },
        .alignof_expr => |type_info| {
            repr.tag = .alignof_expr;
            repr.data[0] = @bitCast(type_info.qt);
            repr.data[1] = packOptIndex(type_info.expr);
            repr.data[2] = @bitCast(type_info.operand_qt);
            repr.tok = type_info.op_tok;
        },
        .generic_expr => |generic| {
            repr.data[0] = @bitCast(generic.qt);
            if (generic.rest.len > 0) {
                repr.tag = .generic_expr;
                repr.data[1] = @intCast(tree.extra.items.len);
                const len: u32 = @intCast(generic.rest.len + 2);
                repr.data[2] = len;
                try tree.extra.ensureUnusedCapacity(tree.comp.gpa, len);
                tree.extra.appendAssumeCapacity(@intFromEnum(generic.controlling));
                tree.extra.appendAssumeCapacity(@intFromEnum(generic.chosen));
                tree.extra.appendSliceAssumeCapacity(@ptrCast(generic.rest));
            } else {
                repr.tag = .generic_expr_zero;
                repr.data[1] = @intFromEnum(generic.controlling);
                repr.data[2] = @intFromEnum(generic.chosen);
            }
            repr.tok = generic.generic_tok;
        },
        .generic_association_expr => |association| {
            repr.tag = .generic_association_expr;
            repr.data[0] = @bitCast(association.association_qt);
            repr.data[1] = @intFromEnum(association.expr);
            repr.tok = association.colon_tok;
        },
        .generic_default_expr => |default| {
            repr.tag = .generic_default_expr;
            repr.data[0] = @intFromEnum(default.expr);
            repr.tok = default.default_tok;
        },
        .binary_cond_expr => |cond| {
            repr.tag = .binary_cond_expr;
            repr.data[0] = @bitCast(cond.qt);
            repr.data[1] = @intFromEnum(cond.cond);
            repr.data[2], _ = try tree.addExtra(&.{ cond.then_expr, cond.else_expr });
            repr.tok = cond.cond_tok;
        },
        .cond_expr => |cond| {
            repr.tag = .cond_expr;
            repr.data[0] = @bitCast(cond.qt);
            repr.data[1] = @intFromEnum(cond.cond);
            repr.data[2], _ = try tree.addExtra(&.{ cond.then_expr, cond.else_expr });
            repr.tok = cond.cond_tok;
        },
        .builtin_choose_expr => |cond| {
            repr.tag = .builtin_choose_expr;
            repr.data[0] = @bitCast(cond.qt);
            repr.data[1] = @intFromEnum(cond.cond);
            repr.data[2], _ = try tree.addExtra(&.{ cond.then_expr, cond.else_expr });
            repr.tok = cond.cond_tok;
        },
        .builtin_types_compatible_p => |builtin| {
            repr.tag = .builtin_types_compatible_p;
            repr.data[0] = @bitCast(builtin.lhs);
            repr.data[1] = @bitCast(builtin.rhs);
            repr.tok = builtin.builtin_tok;
        },
        .builtin_convertvector => |builtin| {
            repr.tag = .builtin_convertvector;
            repr.data[0] = @bitCast(builtin.dest_qt);
            repr.data[1] = @intFromEnum(builtin.operand);
            repr.tok = builtin.builtin_tok;
        },
        .builtin_shufflevector => |builtin| {
            repr.tag = .builtin_shufflevector;
            repr.data[0] = @bitCast(builtin.qt);
            repr.data[1] = @intCast(tree.extra.items.len);
            repr.data[2] = @intCast(builtin.indexes.len);
            repr.tok = builtin.builtin_tok;
            try tree.extra.ensureUnusedCapacity(tree.comp.gpa, builtin.indexes.len + 2);
            tree.extra.appendAssumeCapacity(@intFromEnum(builtin.lhs));
            tree.extra.appendAssumeCapacity(@intFromEnum(builtin.rhs));
            tree.extra.appendSliceAssumeCapacity(@ptrCast(builtin.indexes));
        },
        .array_init_expr => |init| {
            repr.data[0] = @bitCast(init.container_qt);
            if (init.items.len > 2) {
                repr.tag = .array_init_expr;
                repr.data[1], repr.data[2] = try tree.addExtra(init.items);
            } else {
                repr.tag = .array_init_expr_two;
                repr.data[1] = packElem(init.items, 0);
                repr.data[2] = packElem(init.items, 1);
            }
            repr.tok = init.l_brace_tok;
        },
        .struct_init_expr => |init| {
            repr.data[0] = @bitCast(init.container_qt);
            if (init.items.len > 2) {
                repr.tag = .struct_init_expr;
                repr.data[1], repr.data[2] = try tree.addExtra(init.items);
            } else {
                repr.tag = .struct_init_expr_two;
                repr.data[1] = packElem(init.items, 0);
                repr.data[2] = packElem(init.items, 1);
            }
            repr.tok = init.l_brace_tok;
        },
        .union_init_expr => |init| {
            repr.tag = .union_init_expr;
            repr.data[0] = @bitCast(init.union_qt);
            repr.data[1] = init.field_index;
            repr.data[2] = packOptIndex(init.initializer);
            repr.tok = init.l_brace_tok;
        },
        .array_filler_expr => |filler| {
            repr.tag = .array_filler_expr;
            repr.data[0] = @bitCast(filler.qt);
            repr.data[1], repr.data[2] = @as([2]u32, @bitCast(filler.count));
            repr.tok = filler.last_tok;
        },
        .default_init_expr => |default| {
            repr.tag = .default_init_expr;
            repr.data[0] = @bitCast(default.qt);
            repr.tok = default.last_tok;
        },
        .compound_literal_expr => |literal| {
            repr.tag = .compound_literal_expr;
            repr.data[0] = @bitCast(literal.qt);
            repr.data[1] = @bitCast(Node.Repr.DeclAttr{
                .static = literal.storage_class == .static,
                .register = literal.storage_class == .register,
                .thread_local = literal.thread_local,
            });
            repr.data[2] = @intFromEnum(literal.initializer);
            repr.tok = literal.l_paren_tok;
        },
    }
    tree.nodes.set(index, repr);
}

fn packOptIndex(opt: ?Node.Index) u32 {
    return @intFromEnum(Node.OptIndex.packOpt(opt));
}

fn unpackOptIndex(idx: u32) ?Node.Index {
    return @as(Node.OptIndex, @enumFromInt(idx)).unpack();
}

fn packElem(nodes: []const Node.Index, index: usize) u32 {
    return if (nodes.len > index) @intFromEnum(nodes[index]) else @intFromEnum(Node.OptIndex.null);
}

fn unPackElems(data: []const u32) []const Node.Index {
    const sentinel = @intFromEnum(Node.OptIndex.null);
    for (data, 0..) |item, i| {
        if (item == sentinel) return @ptrCast(data[0..i]);
    }
    return @ptrCast(data);
}

/// Returns index to `tree.extra` and length of data
fn addExtra(tree: *Tree, data: []const Node.Index) !struct { u32, u32 } {
    const index: u32 = @intCast(tree.extra.items.len);
    try tree.extra.appendSlice(tree.comp.gpa, @ptrCast(data));
    return .{ index, @intCast(data.len) };
}

pub fn isBitfield(tree: *const Tree, node: Node.Index) bool {
    return tree.bitfieldWidth(node, false) != null;
}

/// Returns null if node is not a bitfield. If inspect_lval is true, this function will
/// recurse into implicit lval_to_rval casts (useful for arithmetic conversions)
pub fn bitfieldWidth(tree: *const Tree, node: Node.Index, inspect_lval: bool) ?u32 {
    switch (node.get(tree)) {
        .member_access_expr, .member_access_ptr_expr => |access| return access.isBitFieldWidth(tree),
        .cast => |cast| {
            if (!inspect_lval) return null;

            return switch (cast.kind) {
                .lval_to_rval => tree.bitfieldWidth(cast.operand, false),
                else => null,
            };
        },
        else => return null,
    }
}

const CallableResultUsage = struct {
    /// name token of the thing being called, for diagnostics
    tok: TokenIndex,
    /// true if `nodiscard` attribute present
    nodiscard: bool,
    /// true if `warn_unused_result` attribute present
    warn_unused_result: bool,
};

pub fn callableResultUsage(tree: *const Tree, node: Node.Index) ?CallableResultUsage {
    loop: switch (node.get(tree)) {
        .decl_ref_expr => |decl_ref| return .{
            .tok = decl_ref.name_tok,
            .nodiscard = decl_ref.qt.hasAttribute(tree.comp, .nodiscard),
            .warn_unused_result = decl_ref.qt.hasAttribute(tree.comp, .warn_unused_result),
        },

        .paren_expr, .addr_of_expr, .deref_expr => |un| continue :loop un.operand.get(tree),
        .comma_expr => |bin| continue :loop bin.rhs.get(tree),
        .cast => |cast| continue :loop cast.operand.get(tree),
        .call_expr => |call| continue :loop call.callee.get(tree),
        .member_access_expr, .member_access_ptr_expr => |access| {
            var qt = access.base.qt(tree);
            if (qt.get(tree.comp, .pointer)) |pointer| qt = pointer.child;
            const record_ty = switch (qt.base(tree.comp).type) {
                .@"struct", .@"union" => |record| record,
                else => return null,
            };

            const field = record_ty.fields[access.member_index];
            return .{
                .tok = field.name_tok,
                .nodiscard = field.qt.hasAttribute(tree.comp, .nodiscard),
                .warn_unused_result = field.qt.hasAttribute(tree.comp, .warn_unused_result),
            };
        },
        else => return null,
    }
}

pub fn isLval(tree: *const Tree, node: Node.Index) bool {
    var is_const: bool = undefined;
    return tree.isLvalExtra(node, &is_const);
}

pub fn isLvalExtra(tree: *const Tree, node: Node.Index, is_const: *bool) bool {
    is_const.* = false;
    var cur_node = node;
    switch (cur_node.get(tree)) {
        .compound_literal_expr => |literal| {
            is_const.* = literal.qt.@"const";
            return true;
        },
        .string_literal_expr => return true,
        .member_access_ptr_expr => |access| {
            const ptr_qt = access.base.qt(tree);
            if (ptr_qt.get(tree.comp, .pointer)) |pointer| is_const.* = pointer.child.@"const";
            return true;
        },
        .member_access_expr => |access| {
            return tree.isLvalExtra(access.base, is_const);
        },
        .array_access_expr => |access| {
            const base_qt = access.base.qt(tree);
            // Array access operand undergoes lval conversions so the base can never
            // be a pure array type.
            if (base_qt.get(tree.comp, .pointer)) |pointer| is_const.* = pointer.child.@"const";
            return true;
        },
        .decl_ref_expr => |decl_ref| {
            is_const.* = decl_ref.qt.@"const";
            return true;
        },
        .deref_expr => |un| {
            const operand_qt = un.operand.qt(tree);
            switch (operand_qt.base(tree.comp).type) {
                .func => return false,
                .pointer => |pointer| is_const.* = pointer.child.@"const",
                else => {},
            }
            return true;
        },
        .paren_expr => |un| {
            return tree.isLvalExtra(un.operand, is_const);
        },
        .builtin_choose_expr => |conditional| {
            if (tree.value_map.get(conditional.cond)) |val| {
                if (!val.isZero(tree.comp)) {
                    return tree.isLvalExtra(conditional.then_expr, is_const);
                } else {
                    return tree.isLvalExtra(conditional.else_expr, is_const);
                }
            }
            return false;
        },
        .compound_assign_dummy_expr => return true,
        else => return false,
    }
}

pub fn tokSlice(tree: *const Tree, tok_i: TokenIndex) []const u8 {
    if (tree.tokens.items(.id)[tok_i].lexeme()) |some| return some;
    const loc = tree.tokens.items(.loc)[tok_i];
    return tree.comp.locSlice(loc);
}

pub fn dump(tree: *const Tree, config: std.Io.tty.Config, w: *std.Io.Writer) std.Io.tty.Config.SetColorError!void {
    for (tree.root_decls.items) |i| {
        try tree.dumpNode(i, 0, config, w);
        try w.writeByte('\n');
    }
    try w.flush();
}

fn dumpFieldAttributes(tree: *const Tree, attributes: []const Attribute, level: u32, w: *std.Io.Writer) !void {
    for (attributes) |attr| {
        try w.splatByteAll(' ', level);
        try w.print("field attr: {s}", .{@tagName(attr.tag)});
        try tree.dumpAttribute(attr, w);
    }
}

fn dumpAttribute(tree: *const Tree, attr: Attribute, w: *std.Io.Writer) !void {
    switch (attr.tag) {
        inline else => |tag| {
            const args = @field(attr.args, @tagName(tag));
            const fields = @typeInfo(@TypeOf(args)).@"struct".fields;
            if (fields.len == 0) {
                try w.writeByte('\n');
                return;
            }
            try w.writeByte(' ');
            inline for (fields, 0..) |f, i| {
                if (comptime std.mem.eql(u8, f.name, "__name_tok")) continue;
                if (i != 0) {
                    try w.writeAll(", ");
                }
                try w.writeAll(f.name);
                try w.writeAll(": ");
                switch (f.type) {
                    Interner.Ref => try w.print("\"{s}\"", .{tree.interner.get(@field(args, f.name)).bytes}),
                    ?Interner.Ref => try w.print("\"{?s}\"", .{if (@field(args, f.name)) |str| tree.interner.get(str).bytes else null}),
                    else => switch (@typeInfo(f.type)) {
                        .@"enum" => try w.writeAll(@tagName(@field(args, f.name))),
                        else => try w.print("{any}", .{@field(args, f.name)}),
                    },
                }
            }
            try w.writeByte('\n');
            return;
        },
    }
}

fn dumpNode(
    tree: *const Tree,
    node_index: Node.Index,
    level: u32,
    config: std.Io.tty.Config,
    w: *std.Io.Writer,
) !void {
    const delta = 2;
    const half = delta / 2;
    const TYPE = std.Io.tty.Color.bright_magenta;
    const TAG = std.Io.tty.Color.bright_cyan;
    const IMPLICIT = std.Io.tty.Color.bright_blue;
    const NAME = std.Io.tty.Color.bright_red;
    const LITERAL = std.Io.tty.Color.bright_green;
    const ATTRIBUTE = std.Io.tty.Color.bright_yellow;

    const node = node_index.get(tree);
    try w.splatByteAll(' ', level);

    if (config == .no_color) {
        if (node.isImplicit()) try w.writeAll("implicit ");
    } else {
        try config.setColor(w, if (node.isImplicit()) IMPLICIT else TAG);
    }
    try w.print("{s}", .{@tagName(node)});

    if (node_index.qtOrNull(tree)) |qt| {
        try w.writeAll(": ");
        switch (node) {
            .cast => |cast| {
                try config.setColor(w, .white);
                try w.print("({s}) ", .{@tagName(cast.kind)});
            },
            else => {},
        }

        try config.setColor(w, TYPE);
        try w.writeByte('\'');
        try qt.dump(tree.comp, w);
        try w.writeByte('\'');
    }

    if (tree.isLval(node_index)) {
        try config.setColor(w, ATTRIBUTE);
        try w.writeAll(" lvalue");
    }
    if (tree.isBitfield(node_index)) {
        try config.setColor(w, ATTRIBUTE);
        try w.writeAll(" bitfield");
    }
    if (node == .asm_stmt) {
        const quals = node.asm_stmt.quals;
        try config.setColor(w, ATTRIBUTE);
        if (quals.@"inline") try w.writeAll(" inline");
        if (quals.@"volatile") try w.writeAll(" volatile");
        if (quals.goto) try w.writeAll(" goto");
    }

    if (tree.value_map.get(node_index)) |val| {
        try config.setColor(w, LITERAL);
        try w.writeAll(" (value: ");
        if (try val.print(node_index.qt(tree), tree.comp, w)) |nested| switch (nested) {
            .pointer => |ptr| {
                switch (tree.nodes.items(.tag)[ptr.node]) {
                    .compound_literal_expr => {
                        try w.writeAll("(compound literal) ");
                        _ = try ptr.offset.print(tree.comp.type_store.ptrdiff, tree.comp, w);
                    },
                    else => {
                        const ptr_node: Node.Index = @enumFromInt(ptr.node);
                        const decl_name = tree.tokSlice(ptr_node.tok(tree));
                        try ptr.offset.printPointer(decl_name, tree.comp, w);
                    },
                }
            },
        };
        try w.writeByte(')');
    }
    if (node == .return_stmt and node.return_stmt.operand == .implicit and node.return_stmt.operand.implicit) {
        try config.setColor(w, IMPLICIT);
        try w.writeAll(" (value: 0)");
    }

    try w.writeAll("\n");
    try config.setColor(w, .reset);

    if (node_index.qtOrNull(tree)) |qt| {
        try config.setColor(w, ATTRIBUTE);
        var it = Attribute.Iterator.initType(qt, tree.comp);
        while (it.next()) |item| {
            const attr, _ = item;
            try w.splatByteAll(' ', level + half);
            try w.print("attr: {s}", .{@tagName(attr.tag)});
            try tree.dumpAttribute(attr, w);
        }
        try config.setColor(w, .reset);
    }

    switch (node) {
        .empty_decl => {},
        .global_asm => |@"asm"| {
            try tree.dumpNode(@"asm".asm_str, level + delta, config, w);
        },
        .static_assert => |assert| {
            try w.splatByteAll(' ', level + 1);
            try w.writeAll("condition:\n");
            try tree.dumpNode(assert.cond, level + delta, config, w);
            if (assert.message) |some| {
                try w.splatByteAll(' ', level + 1);
                try w.writeAll("diagnostic:\n");
                try tree.dumpNode(some, level + delta, config, w);
            }
        },
        .function => |function| {
            try w.splatByteAll(' ', level + half);

            try config.setColor(w, ATTRIBUTE);
            if (function.static) try w.writeAll("static ");
            if (function.@"inline") try w.writeAll("inline ");

            try config.setColor(w, .reset);
            try w.writeAll("name: ");
            try config.setColor(w, NAME);
            try w.print("{s}\n", .{tree.tokSlice(function.name_tok)});
            try config.setColor(w, .reset);

            if (function.body) |body| {
                try w.splatByteAll(' ', level + half);
                try w.writeAll("body:\n");
                try tree.dumpNode(body, level + delta, config, w);
            }
            if (function.definition) |definition| {
                try w.splatByteAll(' ', level + half);
                try w.writeAll("definition: ");
                try config.setColor(w, NAME);
                try w.print("0x{X}\n", .{@intFromEnum(definition)});
                try config.setColor(w, .reset);
            }
        },
        .typedef => |typedef| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("name: ");
            try config.setColor(w, NAME);
            try w.print("{s}\n", .{tree.tokSlice(typedef.name_tok)});
            try config.setColor(w, .reset);
        },
        .param => |param| {
            try w.splatByteAll(' ', level + half);

            switch (param.storage_class) {
                .auto => {},
                .register => {
                    try config.setColor(w, ATTRIBUTE);
                    try w.writeAll("register ");
                    try config.setColor(w, .reset);
                },
            }

            try w.writeAll("name: ");
            try config.setColor(w, NAME);
            try w.print("{s}\n", .{tree.tokSlice(param.name_tok)});
            try config.setColor(w, .reset);
        },
        .variable => |variable| {
            try w.splatByteAll(' ', level + half);

            try config.setColor(w, ATTRIBUTE);
            switch (variable.storage_class) {
                .auto => {},
                .static => try w.writeAll("static "),
                .@"extern" => try w.writeAll("extern "),
                .register => try w.writeAll("register "),
            }
            if (variable.thread_local) try w.writeAll("thread_local ");
            try config.setColor(w, .reset);

            try w.writeAll("name: ");
            try config.setColor(w, NAME);
            try w.print("{s}\n", .{tree.tokSlice(variable.name_tok)});
            try config.setColor(w, .reset);

            if (variable.initializer) |some| {
                try config.setColor(w, .reset);
                try w.splatByteAll(' ', level + half);
                try w.writeAll("init:\n");
                try tree.dumpNode(some, level + delta, config, w);
            }
            if (variable.definition) |definition| {
                try w.splatByteAll(' ', level + half);
                try w.writeAll("definition: ");
                try config.setColor(w, NAME);
                try w.print("0x{X}\n", .{@intFromEnum(definition)});
                try config.setColor(w, .reset);
            }
        },
        .enum_field => |field| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("name: ");
            try config.setColor(w, NAME);
            try w.print("{s}\n", .{tree.tokSlice(field.name_tok)});
            try config.setColor(w, .reset);
            if (field.init) |some| {
                try w.splatByteAll(' ', level + half);
                try w.writeAll("init:\n");
                try tree.dumpNode(some, level + delta, config, w);
            }
        },
        .record_field => |field| {
            const name_tok_id = tree.tokens.items(.id)[field.name_or_first_tok];
            if (name_tok_id == .identifier or name_tok_id == .extended_identifier) {
                try w.splatByteAll(' ', level + half);
                try w.writeAll("name: ");
                try config.setColor(w, NAME);
                try w.print("{s}\n", .{tree.tokSlice(field.name_or_first_tok)});
                try config.setColor(w, .reset);
            }
            if (field.bit_width) |some| {
                try w.splatByteAll(' ', level + half);
                try w.writeAll("bits:\n");
                try tree.dumpNode(some, level + delta, config, w);
            }
        },
        .compound_stmt => |compound| {
            for (compound.body, 0..) |stmt, i| {
                if (i != 0) try w.writeByte('\n');
                try tree.dumpNode(stmt, level + delta, config, w);
            }
        },
        .enum_decl => |decl| {
            for (decl.fields, 0..) |field, i| {
                if (i != 0) try w.writeByte('\n');
                try tree.dumpNode(field, level + delta, config, w);
            }
        },
        .struct_decl, .union_decl => |decl| {
            const fields = switch (node_index.qt(tree).base(tree.comp).type) {
                .@"struct", .@"union" => |record| record.fields,
                else => unreachable,
            };

            var field_i: u32 = 0;
            for (decl.fields, 0..) |field_node, i| {
                if (i != 0) try w.writeByte('\n');
                try tree.dumpNode(field_node, level + delta, config, w);

                if (field_node.get(tree) != .record_field) continue;
                if (fields.len == 0) continue;

                const field_attributes = fields[field_i].attributes(tree.comp);
                field_i += 1;

                if (field_attributes.len == 0) continue;

                try config.setColor(w, ATTRIBUTE);
                try tree.dumpFieldAttributes(field_attributes, level + delta + half, w);
                try config.setColor(w, .reset);
            }
        },
        .array_init_expr, .struct_init_expr => |init| {
            for (init.items, 0..) |item, i| {
                if (i != 0) try w.writeByte('\n');
                try tree.dumpNode(item, level + delta, config, w);
            }
        },
        .union_init_expr => |init| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("field index: ");
            try config.setColor(w, LITERAL);
            try w.print("{d}\n", .{init.field_index});
            try config.setColor(w, .reset);
            if (init.initializer) |some| {
                try tree.dumpNode(some, level + delta, config, w);
            }
        },
        .compound_literal_expr => |literal| {
            if (literal.storage_class != .auto or literal.thread_local) {
                try w.splatByteAll(' ', level + half - 1);

                try config.setColor(w, ATTRIBUTE);
                switch (literal.storage_class) {
                    .auto => {},
                    .static => try w.writeAll(" static"),
                    .register => try w.writeAll(" register"),
                }
                if (literal.thread_local) try w.writeAll(" thread_local");
                try w.writeByte('\n');
                try config.setColor(w, .reset);
            }

            try tree.dumpNode(literal.initializer, level + half, config, w);
        },
        .labeled_stmt => |labeled| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("label: ");
            try config.setColor(w, LITERAL);
            try w.print("{s}\n", .{tree.tokSlice(labeled.label_tok)});

            try config.setColor(w, .reset);
            try w.splatByteAll(' ', level + half);
            try w.writeAll("stmt:\n");
            try tree.dumpNode(labeled.body, level + delta, config, w);
        },
        .case_stmt => |case| {
            try w.splatByteAll(' ', level + half);

            if (case.end) |some| {
                try w.writeAll("range start:\n");
                try tree.dumpNode(case.start, level + delta, config, w);

                try w.splatByteAll(' ', level + half);
                try w.writeAll("range end:\n");
                try tree.dumpNode(some, level + delta, config, w);
            } else {
                try w.writeAll("value:\n");
                try tree.dumpNode(case.start, level + delta, config, w);
            }

            try w.splatByteAll(' ', level + half);
            try w.writeAll("stmt:\n");
            try tree.dumpNode(case.body, level + delta, config, w);
        },
        .default_stmt => |default| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("stmt:\n");
            try tree.dumpNode(default.body, level + delta, config, w);
        },
        .binary_cond_expr, .cond_expr, .builtin_choose_expr => |conditional| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("cond:\n");
            try tree.dumpNode(conditional.cond, level + delta, config, w);

            try w.splatByteAll(' ', level + half);
            try w.writeAll("then:\n");
            try tree.dumpNode(conditional.then_expr, level + delta, config, w);

            try w.splatByteAll(' ', level + half);
            try w.writeAll("else:\n");
            try tree.dumpNode(conditional.else_expr, level + delta, config, w);
        },
        .builtin_types_compatible_p => |call| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("lhs: ");
            try config.setColor(w, TYPE);
            try call.lhs.dump(tree.comp, w);
            try w.writeByte('\n');
            try config.setColor(w, .reset);

            try w.splatByteAll(' ', level + half);
            try w.writeAll("rhs: ");
            try config.setColor(w, TYPE);
            try call.rhs.dump(tree.comp, w);
            try w.writeByte('\n');
            try config.setColor(w, .reset);
        },
        .builtin_convertvector => |convert| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("operand:\n");
            try tree.dumpNode(convert.operand, level + delta, config, w);
        },
        .builtin_shufflevector => |shuffle| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("lhs:\n");
            try tree.dumpNode(shuffle.lhs, level + delta, config, w);

            try w.splatByteAll(' ', level + half);
            try w.writeAll("rhs:\n");
            try tree.dumpNode(shuffle.rhs, level + delta, config, w);

            if (shuffle.indexes.len > 0) {
                try w.splatByteAll(' ', level + half);
                try w.writeAll("indexes:\n");
                for (shuffle.indexes) |index| {
                    try tree.dumpNode(index, level + delta, config, w);
                }
            }
        },
        .if_stmt => |@"if"| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("cond:\n");
            try tree.dumpNode(@"if".cond, level + delta, config, w);

            try w.splatByteAll(' ', level + half);
            try w.writeAll("then:\n");
            try tree.dumpNode(@"if".then_body, level + delta, config, w);

            if (@"if".else_body) |some| {
                try w.splatByteAll(' ', level + half);
                try w.writeAll("else:\n");
                try tree.dumpNode(some, level + delta, config, w);
            }
        },
        .switch_stmt => |@"switch"| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("cond:\n");
            try tree.dumpNode(@"switch".cond, level + delta, config, w);

            try w.splatByteAll(' ', level + half);
            try w.writeAll("body:\n");
            try tree.dumpNode(@"switch".body, level + delta, config, w);
        },
        .while_stmt => |@"while"| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("cond:\n");
            try tree.dumpNode(@"while".cond, level + delta, config, w);

            try w.splatByteAll(' ', level + half);
            try w.writeAll("body:\n");
            try tree.dumpNode(@"while".body, level + delta, config, w);
        },
        .do_while_stmt => |do| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("cond:\n");
            try tree.dumpNode(do.cond, level + delta, config, w);

            try w.splatByteAll(' ', level + half);
            try w.writeAll("body:\n");
            try tree.dumpNode(do.body, level + delta, config, w);
        },
        .for_stmt => |@"for"| {
            switch (@"for".init) {
                .decls => |decls| {
                    try w.splatByteAll(' ', level + half);
                    try w.writeAll("decl:\n");
                    for (decls) |decl| {
                        try tree.dumpNode(decl, level + delta, config, w);
                        try w.writeByte('\n');
                    }
                },
                .expr => |expr| if (expr) |some| {
                    try w.splatByteAll(' ', level + half);
                    try w.writeAll("init:\n");
                    try tree.dumpNode(some, level + delta, config, w);
                },
            }
            if (@"for".cond) |some| {
                try w.splatByteAll(' ', level + half);
                try w.writeAll("cond:\n");
                try tree.dumpNode(some, level + delta, config, w);
            }
            if (@"for".incr) |some| {
                try w.splatByteAll(' ', level + half);
                try w.writeAll("incr:\n");
                try tree.dumpNode(some, level + delta, config, w);
            }
            try w.splatByteAll(' ', level + half);
            try w.writeAll("body:\n");
            try tree.dumpNode(@"for".body, level + delta, config, w);
        },
        .addr_of_label => |addr| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("label: ");
            try config.setColor(w, LITERAL);
            try w.print("{s}\n", .{tree.tokSlice(addr.label_tok)});
            try config.setColor(w, .reset);
        },
        .goto_stmt => |goto| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("label: ");
            try config.setColor(w, LITERAL);
            try w.print("{s}\n", .{tree.tokSlice(goto.label_tok)});
            try config.setColor(w, .reset);
        },
        .computed_goto_stmt => |goto| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("expr:\n");
            try tree.dumpNode(goto.expr, level + delta, config, w);
        },
        .continue_stmt, .break_stmt, .null_stmt => {},
        .return_stmt => |ret| {
            switch (ret.operand) {
                .expr => |expr| {
                    try w.splatByteAll(' ', level + half);
                    try w.writeAll("expr:\n");
                    try tree.dumpNode(expr, level + delta, config, w);
                },
                .implicit => {},
                .none => {},
            }
        },
        .asm_stmt => |@"asm"| {
            try tree.dumpNode(@"asm".asm_str, level + delta, config, w);

            const write_operand = struct {
                fn write_operand(
                    _w: *std.Io.Writer,
                    _level: u32,
                    _config: std.Io.tty.Config,
                    _tree: *const Tree,
                    operands: []const Node.AsmStmt.Operand,
                ) std.Io.tty.Config.SetColorError!void {
                    for (operands) |operand| {
                        if (operand.name != 0) {
                            try _w.splatByteAll(' ', _level + delta);
                            try _w.writeAll("asm name: ");
                            try _config.setColor(_w, NAME);
                            try _w.writeAll(_tree.tokSlice(operand.name));
                            try _w.writeByte('\n');
                            try _config.setColor(_w, .reset);
                        }

                        try _w.splatByteAll(' ', _level + delta);
                        try _w.writeAll("constraint: ");
                        const constraint_val = _tree.value_map.get(operand.constraint).?;
                        try _config.setColor(_w, LITERAL);
                        _ = try constraint_val.print(operand.constraint.qt(_tree), _tree.comp, _w);
                        try _w.writeByte('\n');

                        try _tree.dumpNode(operand.expr, _level + delta, _config, _w);
                    }
                    try _config.setColor(_w, .reset);
                }
            }.write_operand;

            if (@"asm".outputs.len > 0) {
                try w.splatByteAll(' ', level + half);
                try w.writeAll("ouputs:\n");

                try write_operand(w, level, config, tree, @"asm".outputs);
            }
            if (@"asm".inputs.len > 0) {
                try w.splatByteAll(' ', level + half);
                try w.writeAll("inputs:\n");

                try write_operand(w, level, config, tree, @"asm".inputs);
            }
            if (@"asm".clobbers.len > 0) {
                try w.splatByteAll(' ', level + half);
                try w.writeAll("clobbers:\n");
                try config.setColor(w, LITERAL);
                for (@"asm".clobbers) |clobber| {
                    const clobber_val = tree.value_map.get(clobber).?;

                    try w.splatByteAll(' ', level + delta);
                    _ = try clobber_val.print(clobber.qt(tree), tree.comp, w);
                    try w.writeByte('\n');
                }
                try config.setColor(w, .reset);
            }
            if (@"asm".labels.len > 0) {
                try w.splatByteAll(' ', level + half);
                try w.writeAll("labels:\n");
                for (@"asm".labels) |label| {
                    try tree.dumpNode(label, level + delta, config, w);
                }
            }
        },
        .call_expr => |call| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("callee:\n");
            try tree.dumpNode(call.callee, level + delta, config, w);

            if (call.args.len > 0) {
                try w.splatByteAll(' ', level + half);
                try w.writeAll("args:\n");
                for (call.args) |arg| {
                    try tree.dumpNode(arg, level + delta, config, w);
                }
            }
        },
        .builtin_call_expr => |call| {
            try w.splatByteAll(' ', level + half);
            try w.writeAll("name: ");
            try config.setColor(w, NAME);
            try w.print("{s}\n", .{tree.tokSlice(call.builtin_tok)});
            try config.setColor(w, .reset);

            if (call.args.len > 0) {
                try w.splatByteAll(' ', level + half);
                try w.writeAll("args:\n");
                for (call.args) |arg| {
                    try tree.dumpNode(arg, level + delta, config, w);
                }
            }
        },
        .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,
        .comma_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,
        => |bin| {
            try w.splatByteAll(' ', level + 1);
            try w.writeAll("lhs:\n");
            try tree.dumpNode(bin.lhs, level + delta, config, w);

            try w.splatByteAll(' ', level + 1);
            try w.writeAll("rhs:\n");
            try tree.dumpNode(bin.rhs, level + delta, config, w);
        },
        .cast => |cast| try tree.dumpNode(cast.operand, level + delta, config, w),
        .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,
        => |un| {
            try w.splatByteAll(' ', level + 1);
            try w.writeAll("operand:\n");
            try tree.dumpNode(un.operand, level + delta, config, w);
        },
        .decl_ref_expr, .enumeration_ref => |dr| {
            try w.splatByteAll(' ', level + 1);
            try w.writeAll("name: ");
            try config.setColor(w, NAME);
            try w.print("{s}\n", .{tree.tokSlice(dr.name_tok)});
            try config.setColor(w, .reset);
        },
        .builtin_ref => |dr| {
            try w.splatByteAll(' ', level + 1);
            try w.writeAll("name: ");
            try config.setColor(w, NAME);
            try w.print("{s}\n", .{tree.tokSlice(dr.name_tok)});
            try config.setColor(w, .reset);
        },
        .bool_literal,
        .nullptr_literal,
        .int_literal,
        .char_literal,
        .float_literal,
        .string_literal_expr,
        => {},
        .member_access_expr, .member_access_ptr_expr => |access| {
            try w.splatByteAll(' ', level + 1);
            try w.writeAll("lhs:\n");
            try tree.dumpNode(access.base, level + delta, config, w);

            var base_qt = access.base.qt(tree);
            if (base_qt.get(tree.comp, .pointer)) |some| base_qt = some.child;
            const fields = (base_qt.getRecord(tree.comp) orelse return).fields;

            try w.splatByteAll(' ', level + 1);
            try w.writeAll("name: ");
            try config.setColor(w, NAME);
            try w.print("{s}\n", .{fields[access.member_index].name.lookup(tree.comp)});
            try config.setColor(w, .reset);
        },
        .array_access_expr => |access| {
            try w.splatByteAll(' ', level + 1);
            try w.writeAll("base:\n");
            try tree.dumpNode(access.base, level + delta, config, w);

            try w.splatByteAll(' ', level + 1);
            try w.writeAll("index:\n");
            try tree.dumpNode(access.index, level + delta, config, w);
        },
        .sizeof_expr, .alignof_expr => |type_info| {
            if (type_info.expr) |some| {
                try w.splatByteAll(' ', level + 1);
                try w.writeAll("expr:\n");
                try tree.dumpNode(some, level + delta, config, w);
            } else {
                try w.splatByteAll(' ', level + half);
                try w.writeAll("operand type: ");
                try config.setColor(w, TYPE);
                try type_info.operand_qt.dump(tree.comp, w);
                try w.writeByte('\n');
                try config.setColor(w, .reset);
            }
        },
        .generic_expr => |generic| {
            try w.splatByteAll(' ', level + 1);
            try w.writeAll("controlling:\n");
            try tree.dumpNode(generic.controlling, level + delta, config, w);
            try w.splatByteAll(' ', level + 1);
            try w.writeAll("chosen:\n");
            try tree.dumpNode(generic.chosen, level + delta, config, w);

            if (generic.rest.len > 0) {
                try w.splatByteAll(' ', level + 1);
                try w.writeAll("rest:\n");
                for (generic.rest) |expr| {
                    try tree.dumpNode(expr, level + delta, config, w);
                }
            }
        },
        .generic_association_expr => |assoc| {
            try tree.dumpNode(assoc.expr, level + delta, config, w);
        },
        .generic_default_expr => |default| {
            try tree.dumpNode(default.expr, level + delta, config, w);
        },
        .array_filler_expr => |filler| {
            try w.splatByteAll(' ', level + 1);
            try w.writeAll("count: ");
            try config.setColor(w, LITERAL);
            try w.print("{d}\n", .{filler.count});
            try config.setColor(w, .reset);
        },
        .struct_forward_decl,
        .union_forward_decl,
        .enum_forward_decl,
        .default_init_expr,
        .cond_dummy_expr,
        .compound_assign_dummy_expr,
        => {},
    }
}
